Skip to content

Conversation

@byroot
Copy link
Member

@byroot byroot commented Nov 27, 2024

Profiling shows a lot of time spent in various encoding check functions. I'm working on optimizing them on the Ruby side, but if we assume most strings are one of the simple 3 encodings, we can skip a lot of overhead.

require 'strscan'
require 'benchmark/ips'

source = 10_000.times.map { rand(9999999).to_s }.join(",").force_encoding(Encoding::UTF_8).freeze

def scan_to_i(source)
  scanner = StringScanner.new(source)
  while number = scanner.scan(/\d+/)
    number.to_i
    scanner.skip(",")
  end
end

def scan_integer(source)
  scanner = StringScanner.new(source)
  while scanner.scan_integer
    scanner.skip(",")
  end
end

Benchmark.ips do |x|
  x.report("scan.to_i") { scan_to_i(source) }
  x.report("scan_integer") { scan_integer(source) }
  x.compare!
end

Before:

ruby 3.3.4 (2024-07-09 revision be1089c8ec) +YJIT [arm64-darwin23]
Warming up --------------------------------------
           scan.to_i    93.000 i/100ms
        scan_integer   232.000 i/100ms
Calculating -------------------------------------
           scan.to_i    933.191 (± 0.2%) i/s    (1.07 ms/i) -      4.743k in   5.082597s
        scan_integer      2.326k (± 0.8%) i/s  (429.99 μs/i) -     11.832k in   5.087974s

Comparison:
        scan_integer:     2325.6 i/s
           scan.to_i:      933.2 i/s - 2.49x  slower

After:

ruby 3.3.4 (2024-07-09 revision be1089c8ec) +YJIT [arm64-darwin23]
Warming up --------------------------------------
           scan.to_i    96.000 i/100ms
        scan_integer   274.000 i/100ms
Calculating -------------------------------------
           scan.to_i    969.489 (± 0.2%) i/s    (1.03 ms/i) -      4.896k in   5.050114s
        scan_integer      2.756k (± 0.1%) i/s  (362.88 μs/i) -     13.974k in   5.070837s

Comparison:
        scan_integer:     2755.8 i/s
           scan.to_i:      969.5 i/s - 2.84x  slower

StringValue(pattern);
rb_encoding *enc = rb_enc_check(p->str, pattern);
if (S_RESTLEN(p) < RSTRING_LEN(pattern)) {
if (!(strscan_ascii_compat_fastpath(p->str) && strscan_ascii_compat_fastpath(pattern))) {
Copy link
Member

@eregon eregon Nov 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this equivalent though?

> Encoding.compatible? "é".b, "é"
=> nil

So these are not compatible but they have both one of the 3 simple encodings

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you're right. rb_enc_check end up in enc_compatible_latter, and if it returns 0 it raises: https://github.com/ruby/ruby/blob/43b059b6a3b5c49b7d883c49dd1200580c1f92be/encoding.c#L1068-L1128

So I'm missing a check for both encindex to be equal.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I replaced it to just a fastpath if encindex are the same, which I think would most often be the case.

@byroot byroot force-pushed the optimize-encoding-checks branch from 8eb2198 to e92e6d1 Compare November 27, 2024 13:11
Profiling shows a lot of time spent in various encoding check functions.
I'm working on optimizing them on the Ruby side, but if we assume most
strings are one of the simple 3 encodings, we can skip a lot of overhead.

```ruby
require 'strscan'
require 'benchmark/ips'

source = 10_000.times.map { rand(9999999).to_s }.join(",").force_encoding(Encoding::UTF_8).freeze

def scan_to_i(source)
  scanner = StringScanner.new(source)
  while number = scanner.scan(/\d+/)
    number.to_i
    scanner.skip(",")
  end
end

def scan_integer(source)
  scanner = StringScanner.new(source)
  while scanner.scan_integer
    scanner.skip(",")
  end
end

Benchmark.ips do |x|
  x.report("scan.to_i") { scan_to_i(source) }
  x.report("scan_integer") { scan_integer(source) }
  x.compare!
end
```

Before:

```
ruby 3.3.4 (2024-07-09 revision be1089c8ec) +YJIT [arm64-darwin23]
Warming up --------------------------------------
           scan.to_i    93.000 i/100ms
        scan_integer   232.000 i/100ms
Calculating -------------------------------------
           scan.to_i    933.191 (± 0.2%) i/s    (1.07 ms/i) -      4.743k in   5.082597s
        scan_integer      2.326k (± 0.8%) i/s  (429.99 μs/i) -     11.832k in   5.087974s

Comparison:
        scan_integer:     2325.6 i/s
           scan.to_i:      933.2 i/s - 2.49x  slower
```

After:

```
ruby 3.3.4 (2024-07-09 revision be1089c8ec) +YJIT [arm64-darwin23]
Warming up --------------------------------------
           scan.to_i    96.000 i/100ms
        scan_integer   274.000 i/100ms
Calculating -------------------------------------
           scan.to_i    969.489 (± 0.2%) i/s    (1.03 ms/i) -      4.896k in   5.050114s
        scan_integer      2.756k (± 0.1%) i/s  (362.88 μs/i) -     13.974k in   5.070837s

Comparison:
        scan_integer:     2755.8 i/s
           scan.to_i:      969.5 i/s - 2.84x  slower
```
@byroot byroot force-pushed the optimize-encoding-checks branch from e92e6d1 to f3115ce Compare November 27, 2024 13:13
@kou kou merged commit c02b1ce into ruby:master Nov 28, 2024
37 checks passed
@kou
Copy link
Member

kou commented Nov 28, 2024

Thanks.

hsbt pushed a commit to hsbt/ruby that referenced this pull request Dec 2, 2024
(ruby/strscan#117)

Profiling shows a lot of time spent in various encoding check functions.
I'm working on optimizing them on the Ruby side, but if we assume most
strings are one of the simple 3 encodings, we can skip a lot of
overhead.

```ruby
require 'strscan'
require 'benchmark/ips'

source = 10_000.times.map { rand(9999999).to_s }.join(",").force_encoding(Encoding::UTF_8).freeze

def scan_to_i(source)
  scanner = StringScanner.new(source)
  while number = scanner.scan(/\d+/)
    number.to_i
    scanner.skip(",")
  end
end

def scan_integer(source)
  scanner = StringScanner.new(source)
  while scanner.scan_integer
    scanner.skip(",")
  end
end

Benchmark.ips do |x|
  x.report("scan.to_i") { scan_to_i(source) }
  x.report("scan_integer") { scan_integer(source) }
  x.compare!
end
```

Before:

```
ruby 3.3.4 (2024-07-09 revision ruby/strscan@be1089c8ec) +YJIT [arm64-darwin23]
Warming up --------------------------------------
           scan.to_i    93.000 i/100ms
        scan_integer   232.000 i/100ms
Calculating -------------------------------------
           scan.to_i    933.191 (± 0.2%) i/s    (1.07 ms/i) -      4.743k in   5.082597s
        scan_integer      2.326k (± 0.8%) i/s  (429.99 μs/i) -     11.832k in   5.087974s

Comparison:
        scan_integer:     2325.6 i/s
           scan.to_i:      933.2 i/s - 2.49x  slower
```

After:

```
ruby 3.3.4 (2024-07-09 revision ruby/strscan@be1089c8ec) +YJIT [arm64-darwin23]
Warming up --------------------------------------
           scan.to_i    96.000 i/100ms
        scan_integer   274.000 i/100ms
Calculating -------------------------------------
           scan.to_i    969.489 (± 0.2%) i/s    (1.03 ms/i) -      4.896k in   5.050114s
        scan_integer      2.756k (± 0.1%) i/s  (362.88 μs/i) -     13.974k in   5.070837s

Comparison:
        scan_integer:     2755.8 i/s
           scan.to_i:      969.5 i/s - 2.84x  slower
```

ruby/strscan@c02b1ce684
hsbt pushed a commit to ruby/ruby that referenced this pull request Dec 2, 2024
(ruby/strscan#117)

Profiling shows a lot of time spent in various encoding check functions.
I'm working on optimizing them on the Ruby side, but if we assume most
strings are one of the simple 3 encodings, we can skip a lot of
overhead.

```ruby
require 'strscan'
require 'benchmark/ips'

source = 10_000.times.map { rand(9999999).to_s }.join(",").force_encoding(Encoding::UTF_8).freeze

def scan_to_i(source)
  scanner = StringScanner.new(source)
  while number = scanner.scan(/\d+/)
    number.to_i
    scanner.skip(",")
  end
end

def scan_integer(source)
  scanner = StringScanner.new(source)
  while scanner.scan_integer
    scanner.skip(",")
  end
end

Benchmark.ips do |x|
  x.report("scan.to_i") { scan_to_i(source) }
  x.report("scan_integer") { scan_integer(source) }
  x.compare!
end
```

Before:

```
ruby 3.3.4 (2024-07-09 revision ruby/strscan@be1089c8ec) +YJIT [arm64-darwin23]
Warming up --------------------------------------
           scan.to_i    93.000 i/100ms
        scan_integer   232.000 i/100ms
Calculating -------------------------------------
           scan.to_i    933.191 (± 0.2%) i/s    (1.07 ms/i) -      4.743k in   5.082597s
        scan_integer      2.326k (± 0.8%) i/s  (429.99 μs/i) -     11.832k in   5.087974s

Comparison:
        scan_integer:     2325.6 i/s
           scan.to_i:      933.2 i/s - 2.49x  slower
```

After:

```
ruby 3.3.4 (2024-07-09 revision ruby/strscan@be1089c8ec) +YJIT [arm64-darwin23]
Warming up --------------------------------------
           scan.to_i    96.000 i/100ms
        scan_integer   274.000 i/100ms
Calculating -------------------------------------
           scan.to_i    969.489 (± 0.2%) i/s    (1.03 ms/i) -      4.896k in   5.050114s
        scan_integer      2.756k (± 0.1%) i/s  (362.88 μs/i) -     13.974k in   5.070837s

Comparison:
        scan_integer:     2755.8 i/s
           scan.to_i:      969.5 i/s - 2.84x  slower
```

ruby/strscan@c02b1ce684
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants