Skip to content

Commit

Permalink
Improve String#casecmp? and Symbol#casecmp? performance
Browse files Browse the repository at this point in the history
## Background

`String#casecmp?` method was suggested to RuboCop Performance
to use as a fast method.
rubocop/rubocop-performance#100

However `String#casecmp?` is always slower than `String#casecmp("arg").zero?`.

## Summary

This PR improves `String#casecmp?` and `Symbol#casecmp?` performance
when comparing ASCII characters.
OTOH, in the case of Non-ASCII characters, performance is slightly
degraded by added condition.

And I found ruby#1668 which is essentially the same.

## Benchmark

The following is a benchmark case for `String#casecmp?` with similar cases.

```ruby
require 'benchmark/ips'

puts '* ASCII'

Benchmark.ips do |x|
  x.report('upcase')        { 'String'.upcase == 'string' }
  x.report('downcase')      { 'String'.downcase == 'string' }
  x.report('casecmp.zero?') { 'String'.casecmp('string').zero? }
  x.report('casecmp?')      { 'String'.casecmp?('string') }
  x.compare!
end

puts '* Non-ASCII'

Benchmark.ips do |x|
  x.report('upcase')        { 'äö '.upcase == ('ÄÖÜ') }
  x.report('downcase')      { 'äö '.downcase == ('ÄÖÜ') }
  x.report('casecmp.zero?') { 'äö '.casecmp('ÄÖÜ').zero? }
  x.report('casecmp?')      { 'äö '.casecmp?('ÄÖÜ') }
  x.compare!
end
```

### Before

```console
* ASCII
Warming up --------------------------------------
              upcase   200.428k i/100ms
            downcase   197.503k i/100ms
       casecmp.zero?   216.244k i/100ms
            casecmp?   171.257k i/100ms
Calculating -------------------------------------
              upcase      4.326M (± 2.9%) i/s -     21.646M in       5.008511s
            downcase      4.350M (± 2.3%) i/s -     21.923M in       5.042694s
       casecmp.zero?      4.998M (± 3.7%) i/s -     25.084M in       5.025253s
            casecmp?      3.090M (± 2.9%) i/s -     15.584M in       5.047497s

Comparison:
       casecmp.zero?:  4998357.4 i/s
            downcase:  4349766.0 i/s - 1.15x  slower
              upcase:  4325765.3 i/s - 1.16x  slower
            casecmp?:  3090194.2 i/s - 1.62x  slower

* Non-ASCII
Warming up --------------------------------------
              upcase   137.352k i/100ms
            downcase   136.735k i/100ms
       casecmp.zero?   206.341k i/100ms
            casecmp?    96.326k i/100ms
Calculating -------------------------------------
              upcase      2.215M (± 2.7%) i/s -     11.126M in       5.027582s
            downcase      2.199M (± 2.5%) i/s -     11.076M in       5.038781s
       casecmp.zero?      4.709M (± 1.8%) i/s -     23.729M in       5.041362s
            casecmp?      1.315M (± 1.6%) i/s -      6.646M in       5.054479s

Comparison:
       casecmp.zero?:  4708502.7 i/s
              upcase:  2214590.5 i/s - 2.13x  slower
            downcase:  2199478.9 i/s - 2.14x  slower
            casecmp?:  1315326.8 i/s - 3.58x  slower
```

### After

`casecmp?` performance for ASCII characters will be improved.

```console
* ASCII
Warming up --------------------------------------
              upcase   203.640k i/100ms
            downcase   203.605k i/100ms
       casecmp.zero?   217.033k i/100ms
            casecmp?   227.628k i/100ms
Calculating -------------------------------------
              upcase      4.284M (± 1.4%) i/s -     21.586M in       5.039916s
            downcase      4.228M (± 1.6%) i/s -     21.175M in       5.009956s
       casecmp.zero?      5.202M (± 2.7%) i/s -     26.044M in       5.010598s
            casecmp?      5.308M (± 7.4%) i/s -     26.405M in       5.005934s

Comparison:
            casecmp?:  5308087.4 i/s
       casecmp.zero?:  5201808.4 i/s - same-ish: difference falls within
            error
              upcase:  4283916.1 i/s - 1.24x  slower
            downcase:  4227698.3 i/s - 1.26x  slower

* Non-ASCII
Warming up --------------------------------------
              upcase   134.896k i/100ms
            downcase   133.699k i/100ms
       casecmp.zero?   204.155k i/100ms
            casecmp?    90.405k i/100ms
Calculating -------------------------------------
              upcase      2.160M (± 5.7%) i/s -     10.792M in       5.013980s
            downcase      2.013M (±12.2%) i/s -      9.894M in       4.999781s
       casecmp.zero?      4.457M (± 4.8%) i/s -     22.253M in       5.004385s
            casecmp?      1.257M (± 3.2%) i/s -      6.328M in       5.038371s

Comparison:
       casecmp.zero?:  4457430.6 i/s
              upcase:  2160130.1 i/s - 2.06x  slower
            downcase:  2012931.9 i/s - 2.21x  slower
            casecmp?:  1257370.8 i/s - 3.55x  slower
```

`Symbol#casecmp?` have similar results.

## Other Information

This is an off-topic for this PR.
I inform RuboCop Performance that `String#casecmp` and `String#casecmp?`
behave differently when using Non-ASCII characters.

```ruby
'äöü'.casecmp('ÄÖÜ').zero? #=> false
'äöü'.casecmp?('ÄÖÜ')      #=> true
```
  • Loading branch information
koic committed Feb 28, 2020
1 parent 9d6d531 commit 60486ab
Showing 1 changed file with 11 additions and 6 deletions.
17 changes: 11 additions & 6 deletions string.c
Original file line number Diff line number Diff line change
Expand Up @@ -3484,15 +3484,20 @@ str_casecmp_p(VALUE str1, VALUE str2)
VALUE folded_str1, folded_str2;
VALUE fold_opt = sym_fold;

enc = rb_enc_compatible(str1, str2);
if (!enc) {
return Qnil;
if (rb_enc_str_asciionly_p(str1)) {
return FIXNUM_ZERO_P(str_casecmp(str1, str2)) ? Qtrue : Qfalse;
}
else {
enc = rb_enc_compatible(str1, str2);
if (!enc) {
return Qnil;
}

folded_str1 = rb_str_downcase(1, &fold_opt, str1);
folded_str2 = rb_str_downcase(1, &fold_opt, str2);
folded_str1 = rb_str_downcase(1, &fold_opt, str1);
folded_str2 = rb_str_downcase(1, &fold_opt, str2);

return rb_str_eql(folded_str1, folded_str2);
return rb_str_eql(folded_str1, folded_str2);
}
}

static long
Expand Down

0 comments on commit 60486ab

Please sign in to comment.