From 60486abe684a637baa12b047b0f2895f925e7588 Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Wed, 26 Feb 2020 21:52:23 +0900 Subject: [PATCH] Improve `String#casecmp?` and `Symbol#casecmp?` performance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Background `String#casecmp?` method was suggested to RuboCop Performance to use as a fast method. https://github.com/rubocop-hq/rubocop-performance/issues/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 #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 ``` --- string.c | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/string.c b/string.c index 0607726a076f49..d5a511847a9ea4 100644 --- a/string.c +++ b/string.c @@ -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