Code Monkey home page Code Monkey logo

fast-ruby's Introduction

Fast Ruby Benchmarks

In Erik Michaels-Ober's great talk, 'Writing Fast Ruby': Video @ Baruco 2014, Slide, he presented us with many idioms that lead to faster running Ruby code. He inspired me to document these to let more people know. I try to link to real commits so people can see that this can really have benefits in the real world. This does not mean you can always blindly replace one with another. It depends on the context (e.g. gsub versus tr). Friendly reminder: Use with caution!

Each idiom has a corresponding code example that resides in code.

All results listed in README.md are running with Ruby 2.2.0p0 on OS X 10.10.1. Machine information: MacBook Pro (Retina, 15-inch, Mid 2014), 2.5 GHz Intel Core i7, 16 GB 1600 MHz DDR3. Your results may vary, but you get the idea. : )

You can checkout the GitHub Actions build for these benchmark results ran against different Ruby implementations.

Let's write faster code, together! <3

Analyze your code

Checkout the fasterer project - it's a static analysis that checks speed idioms written in this repo.

Measurement Tool

Use benchmark-ips (2.0+).

Template

require "benchmark/ips"

def fast
end

def slow
end

Benchmark.ips do |x|
  x.report("fast code description") { fast }
  x.report("slow code description") { slow }
  x.compare!
end

Idioms

Index

General

attr_accessor vs getter and setter code

https://www.omniref.com/ruby/2.2.0/files/method.h?#annotation=4081781&line=47

$ ruby -v code/general/attr-accessor-vs-getter-and-setter.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Calculating -------------------------------------
   getter_and_setter    61.240k i/100ms
       attr_accessor    66.535k i/100ms
-------------------------------------------------
   getter_and_setter      1.660M (± 9.7%) i/s -      8.267M
       attr_accessor      1.865M (± 9.2%) i/s -      9.248M

Comparison:
       attr_accessor:  1865408.4 i/s
   getter_and_setter:  1660021.9 i/s - 1.12x slower
begin...rescue vs respond_to? for Control Flow code
$ ruby -v code/general/begin-rescue-vs-respond-to.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]

Calculating -------------------------------------
      begin...rescue    29.452k i/100ms
         respond_to?   106.528k i/100ms
-------------------------------------------------
      begin...rescue    371.591k (± 5.4%) i/s -      1.855M
         respond_to?      3.277M (± 7.5%) i/s -     16.299M

Comparison:
         respond_to?:  3276972.3 i/s
      begin...rescue:   371591.0 i/s - 8.82x slower
define_method vs module_eval for Defining Methods code
$ ruby -v code/general/define_method-vs-module-eval.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]

Calculating -------------------------------------
module_eval with string 125.000  i/100ms
       define_method    138.000  i/100ms
-------------------------------------------------
module_eval with string   1.130k (±20.3%) i/s -      5.500k
       define_method      1.346k (±25.9%) i/s -      6.348k

Comparison:
       define_method:        1345.6 i/s
module_eval with string:     1129.7 i/s - 1.19x slower
String#constantize vs a comparison for inflection code

ActiveSupport's String#constantize "resolves the constant reference expression in its receiver".

Read the rationale here

ruby 2.7.3p183 (2021-04-05 revision 6847ee089d) [x86_64-darwin20]

Calculating -------------------------------------
using an if statement
                          8.124M (± 1.8%) i/s -     41.357M in   5.092437s
  String#constantize      2.462M (± 2.4%) i/s -     12.315M in   5.004089s

Comparison:
using an if statement:  8123851.3 i/s
  String#constantize:  2462371.2 i/s - 3.30x  (± 0.00) slower
raise vs E2MM#Raise for raising (and defining) exceptions code

Ruby's Exception2MessageMapper module allows one to define and raise exceptions with predefined messages.

$ ruby -v code/general/raise-vs-e2mmap.rb
ruby 2.2.3p173 (2015-08-18 revision 51636) [x86_64-darwin14]

Calculating -------------------------------------
Ruby exception: E2MM#Raise
                         2.865k i/100ms
Ruby exception: Kernel#raise
                        42.215k i/100ms
-------------------------------------------------
Ruby exception: E2MM#Raise
                         27.270k (± 8.8%) i/s -    137.520k
Ruby exception: Kernel#raise
                        617.446k (± 7.9%) i/s -      3.082M

Comparison:
Ruby exception: Kernel#raise:   617446.2 i/s
Ruby exception: E2MM#Raise:    27269.8 i/s - 22.64x slower

Calculating -------------------------------------
Custom exception: E2MM#Raise
                         2.807k i/100ms
Custom exception: Kernel#raise
                        45.313k i/100ms
-------------------------------------------------
Custom exception: E2MM#Raise
                         29.005k (± 7.2%) i/s -    145.964k
Custom exception: Kernel#raise
                        589.149k (± 7.8%) i/s -      2.945M

Comparison:
Custom exception: Kernel#raise:   589148.7 i/s
Custom exception: E2MM#Raise:    29004.8 i/s - 20.31x slower
loop vs while true code
$ ruby -v code/general/loop-vs-while-true.rb
ruby 2.2.3p173 (2015-08-18 revision 51636) [x86_64-linux]

Calculating -------------------------------------
          While Loop     1.000  i/100ms
         Kernel loop     1.000  i/100ms
-------------------------------------------------
          While Loop      0.536  (± 0.0%) i/s -      3.000  in   5.593042s
         Kernel loop      0.223  (± 0.0%) i/s -      2.000  in   8.982355s

Comparison:
          While Loop:        0.5 i/s
         Kernel loop:        0.2 i/s - 2.41x slower
ancestors.include? vs <= code
$ ruby -vW0 code/general/inheritance-check.rb
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-linux]
Warming up --------------------------------------
  less than or equal    66.992k i/100ms
  ancestors.include?    16.943k i/100ms
Calculating -------------------------------------
  less than or equal      1.250M (± 6.4%) i/s -      6.230M in   5.006896s
  ancestors.include?    192.603k (± 4.8%) i/s -    965.751k in   5.025917s

Comparison:
  less than or equal:  1249606.0 i/s
  ancestors.include?:   192602.9 i/s - 6.49x  slower

Method Invocation

call vs send vs method_missing code
$ ruby -v code/method/call-vs-send-vs-method_missing.rb
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14]

Calculating -------------------------------------
                call   115.094k i/100ms
                send   105.258k i/100ms
      method_missing   100.762k i/100ms
-------------------------------------------------
                call      3.811M (± 5.9%) i/s -     18.991M
                send      3.244M (± 7.2%) i/s -     16.210M
      method_missing      2.729M (± 9.8%) i/s -     13.401M

Comparison:
                call:  3811183.4 i/s
                send:  3244239.1 i/s - 1.17x slower
      method_missing:  2728893.0 i/s - 1.40x slower
Normal way to apply method vs &method(...) code
$ ruby -v code/general/block-apply-method.rb
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14]

Calculating -------------------------------------
              normal    85.749k i/100ms
             &method    35.529k i/100ms
-------------------------------------------------
              normal      1.867M (± 7.6%) i/s -      9.347M
             &method    467.095k (± 6.4%) i/s -      2.345M

Comparison:
              normal:  1866669.5 i/s
             &method:   467095.4 i/s - 4.00x slower
Function with single Array argument vs splat arguments code
$ ruby -v code/general/array-argument-vs-splat-arguments.rb
ruby 2.1.7p400 (2015-08-18 revision 51632) [x86_64-linux-gnu]
Calculating -------------------------------------
Function with single Array argument
                       157.231k i/100ms
Function with splat arguments
                         4.983k i/100ms
-------------------------------------------------
Function with single Array argument
                          5.581M (± 2.0%) i/s -     27.987M
Function with splat arguments
                         54.428k (± 3.3%) i/s -    274.065k

Comparison:
Function with single Array argument:  5580972.6 i/s
Function with splat arguments:    54427.7 i/s - 102.54x slower

Hash vs OpenStruct on access assuming you already have a Hash or an OpenStruct code
$ ruby -v code/general/hash-vs-openstruct-on-access.rb
ruby 2.2.3p173 (2015-08-18 revision 51636) [x86_64-darwin14]

Calculating -------------------------------------
                Hash   128.344k i/100ms
          OpenStruct   110.723k i/100ms
-------------------------------------------------
                Hash      5.279M (± 7.0%) i/s -     26.311M
          OpenStruct      3.048M (± 7.0%) i/s -     15.169M

Comparison:
                Hash:  5278844.0 i/s
          OpenStruct:  3048139.8 i/s - 1.73x slower
Hash vs OpenStruct (creation) code
$ ruby -v code/general/hash-vs-openstruct.rb
ruby 2.2.3p173 (2015-08-18 revision 51636) [x86_64-darwin14]

Calculating -------------------------------------
                Hash    75.510k i/100ms
          OpenStruct     9.126k i/100ms
-------------------------------------------------
                Hash      1.604M (±11.0%) i/s -      7.929M
          OpenStruct     96.855k (± 9.9%) i/s -    483.678k

Comparison:
                Hash:  1604259.1 i/s
          OpenStruct:    96855.3 i/s - 16.56x slower
Kernel#format vs Float#round().to_s code
$ ruby -v code/general/format-vs-round-and-to-s.rb
ruby 2.3.3p222 (2016-11-21 revision 56859) [x86_64-darwin15]
Warming up --------------------------------------
         Float#round   106.645k i/100ms
       Kernel#format    84.304k i/100ms
            String#%    78.635k i/100ms
Calculating -------------------------------------
         Float#round      1.570M (± 3.2%) i/s - 7.892M in   5.030672s
       Kernel#format      1.144M (± 3.0%) i/s - 5.733M in   5.015621s
            String#%      1.047M (± 4.2%) i/s - 5.269M in   5.042970s

Comparison:
         Float#round:  1570411.4 i/s
       Kernel#format:  1144036.6 i/s - 1.37x  slower
            String#%:  1046689.1 i/s - 1.50x  slower

Array

Array#bsearch vs Array#find code

WARNING: bsearch ONLY works on sorted array. More details please see #29.

$ ruby -v code/array/bsearch-vs-find.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]

Calculating -------------------------------------
                find     1.000  i/100ms
             bsearch    42.216k i/100ms
-------------------------------------------------
                find      0.184  (± 0.0%) i/s -      1.000  in   5.434758s
             bsearch    577.301k (± 6.6%) i/s -      2.913M

Comparison:
             bsearch:   577300.7 i/s
                find:        0.2 i/s - 3137489.63x slower
Array#length vs Array#size vs Array#count code

Use #length when you only want to know how many elements in the array, #count could also achieve this. However #count should be use for counting specific elements in array. Note #size is an alias of #length.

$ ruby -v code/array/length-vs-size-vs-count.rb
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14]

Calculating -------------------------------------
        Array#length   172.998k i/100ms
          Array#size   168.130k i/100ms
         Array#count   164.911k i/100ms
-------------------------------------------------
        Array#length     11.394M (± 6.1%) i/s -     56.743M
          Array#size     11.303M (± 6.5%) i/s -     56.324M
         Array#count      9.195M (± 8.6%) i/s -     45.680M

Comparison:
        Array#length: 11394036.7 i/s
          Array#size: 11302701.1 i/s - 1.01x slower
         Array#count:  9194976.2 i/s - 1.24x slower
Array#shuffle.first vs Array#sample code

Array#shuffle allocates an extra array.
Array#sample indexes into the array without allocating an extra array.
This is the reason why Array#sample exists.
—— @sferik rails/rails#17245

$ ruby -v code/array/shuffle-first-vs-sample.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]

Calculating -------------------------------------
 Array#shuffle.first    25.406k i/100ms
        Array#sample   125.101k i/100ms
-------------------------------------------------
 Array#shuffle.first    304.341k (± 4.3%) i/s -      1.524M
        Array#sample      5.727M (± 8.6%) i/s -     28.523M

Comparison:
        Array#sample:  5727032.0 i/s
 Array#shuffle.first:   304341.1 i/s - 18.82x slower
Array#[](0) vs Array#first code
$ ruby -v code/array/array-first-vs-index.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]

Calculating -------------------------------------
           Array#[0]   152.751k i/100ms
         Array#first   148.088k i/100ms
-------------------------------------------------
           Array#[0]      8.614M (± 7.0%) i/s -     42.923M
         Array#first      7.465M (±10.7%) i/s -     36.874M

Comparison:
           Array#[0]:  8613583.7 i/s
         Array#first:  7464526.6 i/s - 1.15x slower
Array#[](-1) vs Array#last code
$ ruby -v code/array/array-last-vs-index.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]

Calculating -------------------------------------
            Array#[-1]   151.940k i/100ms
          Array#last   153.371k i/100ms
-------------------------------------------------
            Array#[-1]      8.582M (± 4.6%) i/s -     42.847M
          Array#last      7.639M (± 5.7%) i/s -     38.189M

Comparison:
            Array#[-1]:  8582074.3 i/s
          Array#last:  7639254.5 i/s - 1.12x slower
Array#insert vs Array#unshift code
$ ruby -v code/array/insert-vs-unshift.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin10.0]
Calculating -------------------------------------
       Array#unshift     4.000  i/100ms
        Array#insert     1.000  i/100ms
-------------------------------------------------
       Array#unshift     44.947  (± 6.7%) i/s -    224.000
        Array#insert      0.171  (± 0.0%) i/s -      1.000  in   5.841595s

Comparison:
       Array#unshift:       44.9 i/s
        Array#insert:        0.2 i/s - 262.56x slower

Array#concat vs Array#+ code

Array#+ returns a new array built by concatenating the two arrays together to produce a third array. Array#concat appends the elements of the other array to self. This means that the + operator will create a new array each time it is called (which is expensive), while concat only appends the new element.

$ ruby -v code/array/array-concat-vs-+.rb
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin18]
Warming up --------------------------------------
        Array#concat    23.000  i/100ms
             Array#+     1.000  i/100ms
Calculating -------------------------------------
        Array#concat    217.669  (±15.2%) i/s -      1.058k in   5.016952s
             Array#+      1.475  (± 0.0%) i/s -      8.000  in   5.467642s

Comparison:
        Array#concat:      217.7 i/s
             Array#+:        1.5 i/s - 147.54x  slower
Array#new vs Fixnum#times + map code

Typical slowdown is 40-60% depending on the size of the array. See the corresponding pull request for performance characteristics.

ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-darwin15]
Calculating -------------------------------------
           Array#new    63.875k i/100ms
  Fixnum#times + map    48.010k i/100ms
-------------------------------------------------
           Array#new      1.070M (± 2.2%) i/s -      5.365M
  Fixnum#times + map    678.097k (± 2.7%) i/s -      3.409M

Comparison:
           Array#new:  1069837.0 i/s
  Fixnum#times + map:   678097.4 i/s - 1.58x slower
Array#sort.reverse vs Array#sort_by + block code
$ ruby -v code/array/sort-reverse-vs-sort_by.rb
ruby 2.5.2p104 (2018-10-18 revision 65133) [x86_64-darwin13]
Warming up --------------------------------------
Array#sort.reverse
                        16.231k i/100ms
Array#sort_by &:-@
                         5.406k i/100ms
Calculating -------------------------------------
Array#sort.reverse
                        149.492k (±11.0%) i/s -    746.626k in   5.070375s
Array#sort_by &:-@
                         51.981k (± 8.8%) i/s -    259.488k in   5.041625s

Comparison:
Array#sort.reverse:   149492.2 i/s
Array#sort_by &:-@:    51980.6 i/s - 2.88x  (± 0.00) slower

Enumerable

Enumerable#each + push vs Enumerable#map code
$ ruby -v code/enumerable/each-push-vs-map.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]

Calculating -------------------------------------
   Array#each + push     9.025k i/100ms
           Array#map    13.947k i/100ms
-------------------------------------------------
   Array#each + push     99.634k (± 3.2%) i/s -    505.400k
           Array#map    158.091k (± 4.2%) i/s -    794.979k

Comparison:
           Array#map:   158090.9 i/s
   Array#each + push:    99634.2 i/s - 1.59x slower
Enumerable#each vs for loop code
$ ruby -v code/enumerable/each-vs-for-loop.rb
ruby 2.2.0preview1 (2014-09-17 trunk 47616) [x86_64-darwin14]

Calculating -------------------------------------
            For loop    17.111k i/100ms
               #each    18.464k i/100ms
-------------------------------------------------
            For loop    198.517k (± 5.3%) i/s -    992.438k
               #each    208.157k (± 5.0%) i/s -      1.052M

Comparison:
               #each:   208157.4 i/s
            For loop:   198517.3 i/s - 1.05x slower
Enumerable#each_with_index vs while loop code

rails/rails#12065

$ ruby -v code/enumerable/each_with_index-vs-while-loop.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]

Calculating -------------------------------------
          While Loop    22.553k i/100ms
     each_with_index    11.963k i/100ms
-------------------------------------------------
          While Loop    240.752k (± 7.1%) i/s -      1.218M
     each_with_index    126.753k (± 5.9%) i/s -    634.039k

Comparison:
          While Loop:   240752.1 i/s
     each_with_index:   126753.4 i/s - 1.90x slower
Enumerable#map...Array#flatten vs Enumerable#flat_map code

-- @sferik rails/rails@3413b88, Replace map.flatten with flat_map, Replace map.flatten(1) with flat_map

$ ruby -v code/enumerable/map-flatten-vs-flat_map.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]

Calculating -------------------------------------
Array#map.flatten(1)     3.315k i/100ms
   Array#map.flatten     3.283k i/100ms
      Array#flat_map     5.350k i/100ms
-------------------------------------------------
Array#map.flatten(1)     33.801k (± 4.3%) i/s -    169.065k
   Array#map.flatten     34.530k (± 6.0%) i/s -    173.999k
      Array#flat_map     55.980k (± 5.0%) i/s -    283.550k

Comparison:
      Array#flat_map:    55979.6 i/s
   Array#map.flatten:    34529.6 i/s - 1.62x slower
Array#map.flatten(1):    33800.6 i/s - 1.66x slower
Enumerable#reverse.each vs Enumerable#reverse_each code

Enumerable#reverse allocates an extra array.
Enumerable#reverse_each yields each value without allocating an extra array.
This is the reason why Enumerable#reverse_each exists.
-- @sferik rails/rails#17244

$ ruby -v code/enumerable/reverse-each-vs-reverse_each.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]

Calculating -------------------------------------
  Array#reverse.each    16.746k i/100ms
  Array#reverse_each    18.590k i/100ms
-------------------------------------------------
  Array#reverse.each    190.729k (± 4.8%) i/s -    954.522k
  Array#reverse_each    216.060k (± 4.3%) i/s -      1.078M

Comparison:
  Array#reverse_each:   216060.5 i/s
  Array#reverse.each:   190729.1 i/s - 1.13x slower
Enumerable#sort_by.first vs Enumerable#min_by code

Enumerable#sort_by performs a sort of the enumerable and allocates a new array the size of the enumerable. Enumerable#min_by doesn't perform a sort or allocate an array the size of the enumerable. Similar comparisons hold for Enumerable#sort_by.last vs Enumerable#max_by, Enumerable#sort.first vs Enumerable#min, and Enumerable#sort.last vs Enumerable#max.

$ ruby -v code/enumerable/sort_by-first-vs-min_by.rb
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17]
Warming up --------------------------------------
   Enumerable#min_by    15.170k i/100ms
Enumerable#sort_by...first
                        10.413k i/100ms
Calculating -------------------------------------
   Enumerable#min_by    157.877k (± 0.9%) i/s -    804.010k in   5.093048s
Enumerable#sort_by...first
                        106.831k (± 1.3%) i/s -    541.476k in   5.069403s

Comparison:
   Enumerable#min_by:   157877.0 i/s
Enumerable#sort_by...first:   106831.1 i/s - 1.48x  slower
Enumerable#detect vs Enumerable#select.first code
$ ruby -v code/enumerable/select-first-vs-detect.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]

Calculating -------------------------------------
Enumerable#select.first  8.515k i/100ms
   Enumerable#detect    33.885k i/100ms
-------------------------------------------------
Enumerable#select.first  89.757k (± 5.0%) i/s -      1.797M
   Enumerable#detect    434.304k (± 5.2%) i/s -      8.675M

Comparison:
   Enumerable#detect:   434304.2 i/s
Enumerable#select.first:    89757.4 i/s - 4.84x slower
Enumerable#select.last vs Enumerable#reverse.detect code
$ ruby -v code/enumerable/select-last-vs-reverse-detect.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]

Calculating -------------------------------------
Enumerable#reverse.detect 62.636k i/100ms
Enumerable#select.last    11.687k i/100ms
-------------------------------------------------
Enumerable#reverse.detect 1.263M (± 8.2%) i/s -      6.326M
Enumerable#select.last  119.387k (± 5.7%) i/s -    596.037k

Comparison:
Enumerable#reverse.detect:  1263100.2 i/s
Enumerable#select.last:     119386.8 i/s - 10.58x slower
Enumerable#sort vs Enumerable#sort_by code
$ ruby -v code/enumerable/sort-vs-sort_by.rb
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14]

Calculating -------------------------------------
Enumerable#sort_by (Symbol#to_proc) 2.680k i/100ms
  Enumerable#sort_by                2.462k i/100ms
     Enumerable#sort                1.320k i/100ms
-------------------------------------------------
Enumerable#sort_by (Symbol#to_proc) 25.916k (± 4.4%) i/s -    131.320k
  Enumerable#sort_by                24.650k (± 5.1%) i/s -    125.562k
     Enumerable#sort                14.018k (± 5.6%) i/s -     69.960k

Comparison:
Enumerable#sort_by (Symbol#to_proc):    25916.1 i/s
  Enumerable#sort_by:                   24650.2 i/s - 1.05x slower
     Enumerable#sort:                   14018.3 i/s - 1.85x slower
Enumerable#inject Symbol vs Enumerable#inject Proc code

Of note, to_proc for 1.8.7 is considerable slower than the block format

$ ruby -v code/enumerable/inject-symbol-vs-block.rb
ruby 2.2.4p230 (2015-12-16 revision 53155) [x86_64-darwin14]
Warming up --------------------------------------
       inject symbol     1.893k i/100ms
      inject to_proc     1.583k i/100ms
        inject block     1.390k i/100ms
Calculating -------------------------------------
       inject symbol     19.001k (± 3.8%) i/s -     96.543k
      inject to_proc     15.958k (± 3.5%) i/s -     80.733k
        inject block     14.063k (± 3.9%) i/s -     70.890k

Comparison:
       inject symbol:    19001.5 i/s
      inject to_proc:    15958.3 i/s - 1.19x slower
        inject block:    14063.1 i/s - 1.35x slower

Date

Date.iso8601 vs Date.parse code

When expecting well-formatted data from e.g. an API, iso8601 is faster and will raise an ArgumentError on malformed input.

$ ruby -v code/date/iso8601-vs-parse.rb
ruby 2.4.3p205 (2017-12-14 revision 61247) [x86_64-darwin17]
Warming up --------------------------------------
        Date.iso8601    28.880k i/100ms
          Date.parse    15.805k i/100ms
Calculating -------------------------------------
        Date.iso8601    328.035k (± 4.7%) i/s -      1.646M in   5.029287s
          Date.parse    175.546k (± 3.8%) i/s -    885.080k in   5.049444s

Comparison:
        Date.iso8601:   328035.3 i/s
          Date.parse:   175545.9 i/s - 1.87x  slower

Hash

Hash#[] vs Hash#fetch code

If you use Ruby 2.2, Symbol could be more performant than String as Hash keys. Read more regarding this: Symbol GC in Ruby 2.2 and Unraveling String Key Performance in Ruby 2.2.

$ ruby -v code/hash/bracket-vs-fetch.rb
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14]

Calculating -------------------------------------
     Hash#[], symbol   143.850k i/100ms
  Hash#fetch, symbol   137.425k i/100ms
     Hash#[], string   143.083k i/100ms
  Hash#fetch, string   120.417k i/100ms
-------------------------------------------------
     Hash#[], symbol      7.531M (± 6.6%) i/s -     37.545M
  Hash#fetch, symbol      6.644M (± 8.2%) i/s -     32.982M
     Hash#[], string      6.657M (± 7.7%) i/s -     33.195M
  Hash#fetch, string      3.981M (± 8.7%) i/s -     19.748M

Comparison:
     Hash#[], symbol:  7531355.8 i/s
     Hash#[], string:  6656818.8 i/s - 1.13x slower
  Hash#fetch, symbol:  6643665.5 i/s - 1.13x slower
  Hash#fetch, string:  3981166.5 i/s - 1.89x slower
Hash#dig vs Hash#[] vs Hash#fetch code

Ruby 2.3 introduced Hash#dig which is a readable and performant option for retrieval from a nested hash, returning nil if an extraction step fails. See #102 (comment) for more info.

$ ruby -v code/hash/dig-vs-\[\]-vs-fetch.rb
ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-darwin15]

Calculating -------------------------------------
            Hash#dig      5.719M (± 6.1%) i/s -     28.573M in   5.013997s
             Hash#[]      6.066M (± 6.9%) i/s -     30.324M in   5.025614s
          Hash#[] ||      5.366M (± 6.5%) i/s -     26.933M in   5.041403s
          Hash#[] &&      2.782M (± 4.8%) i/s -     13.905M in   5.010328s
          Hash#fetch      4.101M (± 6.1%) i/s -     20.531M in   5.024945s
 Hash#fetch fallback      2.975M (± 5.5%) i/s -     14.972M in   5.048880s

Comparison:
             Hash#[]:  6065791.0 i/s
            Hash#dig:  5719290.9 i/s - same-ish: difference falls within error
          Hash#[] ||:  5366226.5 i/s - same-ish: difference falls within error
          Hash#fetch:  4101102.1 i/s - 1.48x slower
 Hash#fetch fallback:  2974906.9 i/s - 2.04x slower
          Hash#[] &&:  2781646.6 i/s - 2.18x slower
Hash[] vs Hash#dup code

Source: http://tenderlovemaking.com/2015/02/11/weird-stuff-with-hashes.html

Does this mean that you should switch to Hash[]? Only if your benchmarks can prove that it’s a bottleneck. Please please please don’t change all of your code because this shows it’s faster. Make sure to measure your app performance first.

$ ruby -v code/hash/bracket-vs-dup.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]

Calculating -------------------------------------
              Hash[]    29.403k i/100ms
            Hash#dup    16.195k i/100ms
-------------------------------------------------
              Hash[]    343.987k (± 8.7%) i/s -      1.735M
            Hash#dup    163.516k (±10.2%) i/s -    825.945k

Comparison:
              Hash[]:   343986.5 i/s
            Hash#dup:   163516.3 i/s - 2.10x slower
Hash#fetch with argument vs Hash#fetch + block code

Note that the speedup in the block version comes from avoiding repeated
construction of the argument. If the argument is a constant, number symbol or
something of that sort the argument version is actually slightly faster
See also #39 (comment)

$ ruby -v code/hash/fetch-vs-fetch-with-block.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin13]
Calculating -------------------------------------
  Hash#fetch + const   129.868k i/100ms
  Hash#fetch + block   125.254k i/100ms
    Hash#fetch + arg   121.155k i/100ms
-------------------------------------------------
  Hash#fetch + const      7.031M (± 7.0%) i/s -     34.934M
  Hash#fetch + block      6.815M (± 4.2%) i/s -     34.069M
    Hash#fetch + arg      4.753M (± 5.6%) i/s -     23.746M

Comparison:
  Hash#fetch + const:  7030600.4 i/s
  Hash#fetch + block:  6814826.7 i/s - 1.03x slower
    Hash#fetch + arg:  4752567.2 i/s - 1.48x slower
Hash#each_key instead of Hash#keys.each code

Hash#keys.each allocates an array of keys;
Hash#each_key iterates through the keys without allocating a new array.
This is the reason why Hash#each_key exists.
—— @sferik rails/rails#17099

$ ruby -v code/hash/keys-each-vs-each_key.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]

Calculating -------------------------------------
      Hash#keys.each    56.690k i/100ms
       Hash#each_key    59.658k i/100ms
-------------------------------------------------
      Hash#keys.each    869.262k (± 5.0%) i/s -      4.365M
       Hash#each_key      1.049M (± 6.0%) i/s -      5.250M

Comparison:
       Hash#each_key:  1049161.6 i/s
      Hash#keys.each:   869262.3 i/s - 1.21x slower

Hash#key? instead of Hash#keys.include? code

Hash#keys.include? allocates an array of keys and performs an O(n) search;
Hash#key? performs an O(1) hash lookup without allocating a new array.

$ ruby -v code/hash/keys-include-vs-key.rb
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17]

Calculating -------------------------------------
  Hash#keys.include?      8.612k (± 2.5%) i/s -     43.248k in   5.024749s
           Hash#key?      6.366M (± 5.5%) i/s -     31.715M in   5.002276s

Comparison:
           Hash#key?:  6365855.5 i/s
  Hash#keys.include?:     8612.4 i/s - 739.15x  slower
Hash#value? instead of Hash#values.include? code

Hash#values.include? allocates an array of values and performs an O(n) search;
Hash#value? performs an O(n) search without allocating a new array.

$ ruby -v code/hash/values-include-vs-value.rb
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17]

Calculating -------------------------------------
Hash#values.include?     23.187k (± 4.3%) i/s -    117.720k in   5.086976s
         Hash#value?     38.395k (± 1.0%) i/s -    194.361k in   5.062696s

Comparison:
         Hash#value?:    38395.0 i/s
Hash#values.include?:    23186.8 i/s - 1.66x  slower
Hash#merge! vs Hash#[]= code
$ ruby -v code/hash/merge-bang-vs-\[\]=.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]

Calculating -------------------------------------
         Hash#merge!     1.023k i/100ms
            Hash#[]=     2.844k i/100ms
-------------------------------------------------
         Hash#merge!     10.653k (± 4.9%) i/s -     53.196k
            Hash#[]=     28.287k (±12.4%) i/s -    142.200k

Comparison:
            Hash#[]=:    28287.1 i/s
         Hash#merge!:    10653.3 i/s - 2.66x slower
Hash#update vs Hash#[]= code
$ ruby -v code/hash/update-vs-\[\]=.rb
ruby 2.6.6p146 (2020-03-31 revision 67876) [x86_64-darwin18]

Warming up --------------------------------------
            Hash#[]=     7.453k i/100ms
         Hash#update     4.311k i/100ms
Calculating -------------------------------------
            Hash#[]=     74.764k (± 1.9%) i/s -    380.103k in   5.085962s
         Hash#update     43.220k (± 0.8%) i/s -    219.861k in   5.087364s

Comparison:
            Hash#[]=:    74764.0 i/s
         Hash#update:    43220.1 i/s - 1.73x  (± 0.00) slower
Hash#merge vs Hash#**other code
$ ruby -v code/hash/merge-vs-double-splat-operator.rb
ruby 2.3.3p222 (2016-11-21 revision 56859) [x86_64-darwin15]
Warming up --------------------------------------
        Hash#**other    64.624k i/100ms
          Hash#merge    38.827k i/100ms
Calculating -------------------------------------
        Hash#**other    798.397k (± 6.9%) i/s -      4.007M in   5.053516s
          Hash#merge    434.171k (± 4.5%) i/s -      2.174M in   5.018927s

Comparison:
        Hash#**other:   798396.6 i/s
          Hash#merge:   434170.8 i/s - 1.84x  slower
Hash#merge vs Hash#merge! code
$ ruby -v code/hash/merge-vs-merge-bang.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]

Calculating -------------------------------------
          Hash#merge    39.000  i/100ms
         Hash#merge!     1.008k i/100ms
-------------------------------------------------
          Hash#merge    409.610  (± 7.6%) i/s -      2.067k
         Hash#merge!      9.830k (± 5.8%) i/s -     49.392k

Comparison:
         Hash#merge!:     9830.3 i/s
          Hash#merge:      409.6 i/s - 24.00x slower
{}#merge!(Hash) vs Hash#merge({}) vs Hash#dup#merge!({}) code

When we don't want to modify the original hash, and we want duplicates to be created
See #42 for more details.

$ ruby -v code/hash/merge-bang-vs-merge-vs-dup-merge-bang.rb
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux]

Calculating -------------------------------------
{}#merge!(Hash) do end     2.006k i/100ms
        Hash#merge({})   762.000  i/100ms
   Hash#dup#merge!({})   736.000  i/100ms
-------------------------------------------------
{}#merge!(Hash) do end     20.055k (± 2.0%) i/s -    100.300k in   5.003322s
        Hash#merge({})      7.676k (± 1.2%) i/s -     38.862k in   5.063382s
   Hash#dup#merge!({})      7.440k (± 1.1%) i/s -     37.536k in   5.045851s

Comparison:
{}#merge!(Hash) do end:    20054.8 i/s
        Hash#merge({}):     7676.3 i/s - 2.61x slower
   Hash#dup#merge!({}):     7439.9 i/s - 2.70x slower
Hash#sort_by vs Hash#sort code

To sort hash by key.

$ ruby -v code/hash/hash-key-sort_by-vs-sort.rb
ruby 2.2.1p85 (2015-02-26 revision 49769) [x86_64-darwin14]

Calculating -------------------------------------
      sort_by + to_h    11.468k i/100ms
         sort + to_h     8.107k i/100ms
-------------------------------------------------
      sort_by + to_h    122.176k (± 6.0%) i/s -    619.272k
         sort + to_h     81.973k (± 4.7%) i/s -    413.457k

Comparison:
      sort_by + to_h:   122176.2 i/s
         sort + to_h:    81972.8 i/s - 1.49x slower
Native Hash#slice vs other slice implementations before native code

Since ruby 2.5, Hash comes with a slice method to select hash members by keys.

$ ruby -v code/hash/slice-native-vs-before-native.rb
ruby 2.5.3p105 (2018-10-18 revision 65156) [x86_64-linux]
Warming up --------------------------------------
Hash#native-slice      178.077k i/100ms
Array#each             124.311k i/100ms
Array#each_w/_object   110.818k i/100ms
Hash#select-include     66.972k i/100ms
Calculating -------------------------------------
Hash#native-slice         2.540M (± 1.5%) i/s -     12.822M in   5.049955s
Array#each                1.614M (± 1.0%) i/s -      8.080M in   5.007925s
Array#each_w/_object      1.353M (± 2.6%) i/s -      6.760M in   5.000441s
Hash#select-include     760.944k (± 0.9%) i/s -      3.817M in   5.017123s

Comparison:
Hash#native-slice   :  2539515.5 i/s
Array#each          :  1613665.5 i/s - 1.57x  slower
Array#each_w/_object:  1352851.8 i/s - 1.88x  slower
Hash#select-include :   760944.2 i/s - 3.34x  slower

Proc & Block

Block vs Symbol#to_proc code

Symbol#to_proc is considerably more concise than using block syntax.
...In some cases, it reduces the number of lines of code.
—— @sferik rails/rails#16833

$ ruby -v code/proc-and-block/block-vs-to_proc.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]

Calculating -------------------------------------
               Block     4.632k i/100ms
      Symbol#to_proc     5.225k i/100ms
-------------------------------------------------
               Block     47.914k (± 6.3%) i/s -    240.864k
      Symbol#to_proc     54.791k (± 4.1%) i/s -    276.925k

Comparison:
      Symbol#to_proc:    54791.1 i/s
               Block:    47914.3 i/s - 1.14x slower
Proc#call and block arguments vs yield code

In MRI Ruby before 2.5, block arguments are converted to Procs, which incurs a heap allocation.

$ ruby -v code/proc-and-block/proc-call-vs-yield.rb
ruby 2.4.4p296 (2018-03-28 revision 63013) [x86_64-darwin18]
Calculating -------------------------------------
        block.call      1.967M (± 2.0%) i/s -      9.871M in   5.019328s
     block + yield      2.147M (± 3.3%) i/s -     10.814M in   5.044319s
      unused block      2.265M (± 1.9%) i/s -     11.333M in   5.004522s
             yield     10.436M (± 1.6%) i/s -     52.260M in   5.008851s

Comparison:
             yield: 10436414.0 i/s
      unused block:  2265399.0 i/s - 4.61x  slower
     block + yield:  2146619.0 i/s - 4.86x  slower
        block.call:  1967300.9 i/s - 5.30x  slower

MRI Ruby 2.5 implements Lazy Proc allocation for block parameters, which speeds things up by about 3x.:

$ ruby -v code/proc-and-block/proc-call-vs-yield.rb
ruby 2.5.3p105 (2018-10-18 revision 65156) [x86_64-darwin18]
Calculating -------------------------------------
        block.call      1.970M (± 2.3%) i/s -      9.863M in   5.009599s
     block + yield      9.075M (± 2.6%) i/s -     45.510M in   5.018369s
      unused block     11.176M (± 2.7%) i/s -     55.977M in   5.012741s
             yield     10.588M (± 1.9%) i/s -     53.108M in   5.017755s

Comparison:
      unused block: 11176355.0 i/s
             yield: 10588342.3 i/s - 1.06x  slower
     block + yield:  9075355.5 i/s - 1.23x  slower
        block.call:  1969834.0 i/s - 5.67x  slower

MRI Ruby 2.6 implements an optimization for block.call where a block parameter is passed:

$ ruby -v code/proc-and-block/proc-call-vs-yield.rb
ruby 2.6.1p33 (2019-01-30 revision 66950) [x86_64-darwin18]
Calculating -------------------------------------
        block.call     10.587M (± 1.2%) i/s -     52.969M in   5.003808s
     block + yield     12.630M (± 0.3%) i/s -     63.415M in   5.020910s
      unused block     15.981M (± 0.8%) i/s -     80.255M in   5.022305s
             yield     15.352M (± 3.1%) i/s -     76.816M in   5.009404s

Comparison:
      unused block: 15980789.4 i/s
             yield: 15351931.0 i/s - 1.04x  slower
     block + yield: 12630378.1 i/s - 1.27x  slower
        block.call: 10587315.1 i/s - 1.51x  slower

String

String#dup vs String#+ code

Note that String.new is not the same as the options compared, since it is always ASCII-8BIT encoded instead of the script encoding (usually UTF-8).

$ ruby -v code/string/dup-vs-unary-plus.rb
ruby 2.4.3p205 (2017-12-14 revision 61247) [x86_64-darwin17]

Calculating -------------------------------------
           String#+@      7.697M (± 1.4%) i/s -     38.634M in   5.020313s
          String#dup      3.566M (± 1.0%) i/s -     17.860M in   5.008377s

Comparison:
           String#+@:  7697108.3 i/s
          String#dup:  3566485.7 i/s - 2.16x  slower
String#casecmp vs String#casecmp? vs String#downcase + == code

String#casecmp? is available on Ruby 2.4 or later. Note that String#casecmp only works on characters A-Z/a-z, not all of Unicode.

$ ruby -v code/string/casecmp-vs-downcase-\=\=.rb
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin19]
Warming up --------------------------------------
     String#casecmp?   395.796k i/100ms
String#downcase + ==   543.958k i/100ms
      String#casecmp   730.028k i/100ms
Calculating -------------------------------------
     String#casecmp?      3.687M (±10.9%) i/s -     18.602M in   5.158065s
String#downcase + ==      5.017M (±11.3%) i/s -     25.022M in   5.089175s
      String#casecmp      6.948M (± 6.0%) i/s -     35.041M in   5.062714s

Comparison:
      String#casecmp:  6948231.0 i/s
String#downcase + ==:  5017089.5 i/s - 1.38x  (± 0.00) slower
     String#casecmp?:  3686650.7 i/s - 1.88x  (± 0.00) slower
String Concatenation code
$ ruby -v code/string/concatenation.rb
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux]

Warming up --------------------------------------
            String#+   149.298k i/100ms
       String#concat   151.505k i/100ms
       String#append   153.389k i/100ms
         "foo" "bar"   195.552k i/100ms
  "#{'foo'}#{'bar'}"   193.784k i/100ms
Calculating -------------------------------------
            String#+      2.977M (± 1.1%) i/s -     14.930M in   5.015179s
       String#concat      3.017M (± 1.3%) i/s -     15.150M in   5.023063s
       String#append      3.076M (± 1.2%) i/s -     15.492M in   5.037683s
         "foo" "bar"      5.370M (± 1.0%) i/s -     26.986M in   5.026271s
  "#{'foo'}#{'bar'}"      5.182M (± 4.6%) i/s -     25.967M in   5.022093s

Comparison:
         "foo" "bar":  5369594.5 i/s
  "#{'foo'}#{'bar'}":  5181745.7 i/s - same-ish: difference falls within error
       String#append:  3075719.2 i/s - 1.75x slower
       String#concat:  3016703.5 i/s - 1.78x slower
            String#+:  2977282.7 i/s - 1.80x slower
String#match vs String.match? vs String#start_with?/String#end_with? code (start) code (end)

The regular expression approaches become slower as the tested string becomes longer. For short strings, String#match? performs similarly to String#start_with?/String#end_with?.

⚠️
Sometimes you cant replace regexp with start_with?,
for example: "a\nb" =~ /^b/ #=> 2 but "a\nb" =~ /\Ab/ #=> nil.
⚠️

$ ruby -v code/string/start-string-checking-match-vs-start_with.rb
ruby 2.4.3p205 (2017-12-14 revision 61247) [x86_64-darwin17]

Calculating -------------------------------------
           String#=~      1.088M (± 4.0%) i/s -      5.471M in   5.034404s
       String#match?      5.138M (± 5.0%) i/s -     25.669M in   5.008810s
  String#start_with?      6.314M (± 4.3%) i/s -     31.554M in   5.007207s

Comparison:
  String#start_with?:  6314182.0 i/s
       String#match?:  5138115.1 i/s - 1.23x  slower
           String#=~:  1088461.5 i/s - 5.80x  slower
$ ruby -v code/string/end-string-checking-match-vs-end_with.rb
  ruby 2.4.3p205 (2017-12-14 revision 61247) [x86_64-darwin17]

  Calculating -------------------------------------
             String#=~    918.101k (± 6.0%) i/s -      4.650M in   5.084079s
         String#match?      3.009M (± 6.8%) i/s -     14.991M in   5.005691s
      String#end_with?      4.548M (± 9.3%) i/s -     22.684M in   5.034115s

  Comparison:
      String#end_with?:  4547871.0 i/s
         String#match?:  3008554.5 i/s - 1.51x  slower
             String#=~:   918100.5 i/s - 4.95x  slower
String#start_with? vs String#[].== code
$ ruby -v code/string/end-string-checking-match-vs-end_with.rb
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14]

Calculating -------------------------------------
  String#start_with?      2.047M (± 4.5%) i/s -     10.242M in   5.015146s
    String#[0, n] ==    711.802k (± 7.3%) i/s -      3.551M in   5.019543s
   String#[RANGE] ==    651.751k (± 6.2%) i/s -      3.296M in   5.078772s
   String#[0...n] ==    427.207k (± 5.7%) i/s -      2.136M in   5.019245s

Comparison:
  String#start_with?:  2046618.9 i/s
    String#[0, n] ==:   711802.3 i/s - 2.88x slower
   String#[RANGE] ==:   651751.2 i/s - 3.14x slower
   String#[0...n] ==:   427206.8 i/s - 4.79x slower
Regexp#=== vs Regexp#match vs Regexp#match? vs String#match vs String#=~ vs String#match? code

String#match? and Regexp#match? are available on Ruby 2.4 or later. ActiveSupport provides a forward compatible extension of Regexp for older Rubies without the speed improvement.

⚠️
Sometimes you can't replace match with match?,
This is only useful for cases where you are checking
for a match and not using the resultant match object.
⚠️
Regexp#=== is also faster than String#match but you need to switch the order of arguments.

$ ruby -v code/string/===-vs-=~-vs-match.rb
ruby 2.4.1p111 (2017-03-22 revision 58053) [x86_64-darwin16]
Calculating -------------------------------------
       Regexp#match?      6.994M (± 3.0%) i/s -     35.144M in   5.029647s
       String#match?      6.909M (± 3.3%) i/s -     34.663M in   5.023177s
           String#=~      2.784M (± 5.2%) i/s -     13.996M in   5.043168s
          Regexp#===      2.702M (± 4.5%) i/s -     13.631M in   5.056215s
        Regexp#match      2.607M (± 4.9%) i/s -     13.025M in   5.009071s
        String#match      2.362M (± 5.7%) i/s -     11.817M in   5.020344s

Comparison:
       Regexp#match?:  6994107.7 i/s
       String#match?:  6909055.7 i/s - same-ish: difference falls within error
           String#=~:  2783577.8 i/s - 2.51x  slower
          Regexp#===:  2702030.0 i/s - 2.59x  slower
        Regexp#match:  2607484.0 i/s - 2.68x  slower
        String#match:  2362314.8 i/s - 2.96x  slower

See #59 and #62 for discussions.

String#gsub vs String#sub vs String#[]= code
$ ruby -v code/string/gsub-vs-sub.rb
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux]

Warming up --------------------------------------
         String#gsub    48.360k i/100ms
          String#sub    45.739k i/100ms
String#dup["string"]=   59.896k i/100ms
Calculating -------------------------------------
         String#gsub    647.666k (± 3.3%) i/s -      3.240M in   5.008504s
          String#sub    756.665k (± 2.0%) i/s -      3.796M in   5.019235s
String#dup["string"]=   917.873k (± 1.8%) i/s -      4.612M in   5.026253s

Comparison:
String#dup["string"]=:   917873.1 i/s
          String#sub:    756664.7 i/s - 1.21x slower
         String#gsub:    647665.6 i/s - 1.42x slower


String#gsub vs String#tr code

rails/rails#17257

$ ruby -v code/string/gsub-vs-tr.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]

Calculating -------------------------------------
         String#gsub    38.268k i/100ms
           String#tr    83.210k i/100ms
-------------------------------------------------
         String#gsub    516.604k (± 4.4%) i/s -      2.602M
           String#tr      1.862M (± 4.0%) i/s -      9.320M

Comparison:
           String#tr:  1861860.4 i/s
         String#gsub:   516604.2 i/s - 3.60x slower
String#gsub vs String#tr vs String#delete code
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-linux]

Calculating -------------------------------------
         String#gsub      1.342M (± 1.3%) i/s -      6.816M in   5.079675s
           String#tr      2.627M (± 1.0%) i/s -     13.387M in   5.096083s
       String#delete      2.924M (± 0.7%) i/s -     14.889M in   5.093070s
 String#delete const      3.136M (± 2.6%) i/s -     15.866M in   5.064043s

Comparison:
 String#delete const:  3135559.1 i/s
       String#delete:  2923531.8 i/s - 1.07x  slower
           String#tr:  2627150.5 i/s - 1.19x  slower
         String#gsub:  1342013.4 i/s - 2.34x  slower
Mutable vs Immutable code
$ ruby -v code/string/mutable_vs_immutable_strings.rb
ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin14]

Calculating -------------------------------------
      Without Freeze      7.279M (± 6.6%) i/s -     36.451M in   5.029785s
         With Freeze      9.329M (± 7.9%) i/s -     46.370M in   5.001345s

Comparison:
         With Freeze:  9329054.3 i/s
      Without Freeze:  7279203.1 i/s - 1.28x slower
String#sub! vs String#gsub! vs String#[]= code

Note that String#[] will throw an IndexError when given string or regexp not matched.

$ ruby -v code/string/sub\!-vs-gsub\!-vs-\[\]\=.rb
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14]

Calculating -------------------------------------
  String#['string']=    74.512k i/100ms
 String#sub!'string'    52.801k i/100ms
String#gsub!'string'    34.480k i/100ms
  String#[/regexp/]=    55.325k i/100ms
 String#sub!/regexp/    45.770k i/100ms
String#gsub!/regexp/    27.665k i/100ms
-------------------------------------------------
  String#['string']=      1.215M (± 6.2%) i/s -      6.110M
 String#sub!'string'    752.731k (± 6.2%) i/s -      3.749M
String#gsub!'string'    481.183k (± 4.4%) i/s -      2.414M
  String#[/regexp/]=    840.615k (± 5.3%) i/s -      4.205M
 String#sub!/regexp/    663.075k (± 7.8%) i/s -      3.295M
String#gsub!/regexp/    342.004k (± 7.5%) i/s -      1.715M

Comparison:
  String#['string']=:  1214845.5 i/s
  String#[/regexp/]=:   840615.2 i/s - 1.45x slower
 String#sub!'string':   752731.4 i/s - 1.61x slower
 String#sub!/regexp/:   663075.3 i/s - 1.83x slower
String#gsub!'string':   481183.5 i/s - 2.52x slower
String#gsub!/regexp/:   342003.8 i/s - 3.55x slower
String#sub vs String#delete_prefix code

Ruby 2.5 introduced String#delete_prefix. Note that this can only be used for removing characters from the start of a string.

$ ruby -v code/string/sub-vs-delete_prefix.rb
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-darwin17]
Calculating -------------------------------------
String#delete_prefix      4.112M (± 1.8%) i/s -     20.707M in   5.037928s
          String#sub    814.725k (± 1.4%) i/s -      4.088M in   5.018962s

Comparison:
String#delete_prefix:  4111531.1 i/s
          String#sub:   814725.3 i/s - 5.05x  slower
String#sub vs String#chomp vs String#delete_suffix code

Ruby 2.5 introduced String#delete_suffix as a counterpart to delete_prefix. The performance gain over chomp is small and during some runs the difference falls within the error margin. Note that this can only be used for removing characters from the end of a string.

$ ruby -v code/string/sub-vs-chomp-vs-delete_suffix.rb
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-darwin17]
Calculating -------------------------------------
        String#sub    838.415k (± 1.7%) i/s -      4.214M in   5.027412s
      String#chomp      3.951M (± 2.1%) i/s -     19.813M in   5.017089s
String#delete_suffix    4.202M (± 2.1%) i/s -     21.075M in   5.017429s

Comparison:
String#delete_suffix:  4202201.7 i/s
        String#chomp:  3950921.9 i/s - 1.06x  slower
          String#sub:   838415.3 i/s - 5.01x  slower
String#unpack1 vs String#unpack[0] code

Ruby 2.4.0 introduced unpack1 to skip creating the intermediate array object.

$ ruby -v code/string/unpack1-vs-unpack\[0\].rb
ruby 2.4.3p205 (2017-12-14 revision 61247) [x86_64-darwin17]
Warming up --------------------------------------
      String#unpack1   224.291k i/100ms
    String#unpack[0]   201.870k i/100ms
Calculating -------------------------------------
      String#unpack1      4.864M (± 4.2%) i/s -     24.448M in   5.035203s
    String#unpack[0]      3.778M (± 4.0%) i/s -     18.976M in   5.031253s

Comparison:
      String#unpack1:  4864467.2 i/s
    String#unpack[0]:  3777815.6 i/s - 1.29x  slower
Remove extra spaces (or other contiguous characters) code

The code is tested against contiguous spaces but should work for other chars too.

$ ruby -v code/string/remove-extra-spaces-or-other-chars.rb
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-linux]
Warming up --------------------------------------
 String#gsub/regex+/     1.644k i/100ms
      String#squeeze    24.681k i/100ms
Calculating -------------------------------------
 String#gsub/regex+/     14.668k (± 5.1%) i/s -     73.980k in   5.056887s
      String#squeeze    372.910k (± 8.4%) i/s -      1.851M in   5.011881s

Comparison:
      String#squeeze:   372910.3 i/s
 String#gsub/regex+/:    14668.1 i/s - 25.42x  slower

Time

Time.iso8601 vs Time.parse code

When expecting well-formatted data from e.g. an API, iso8601 is faster and will raise an ArgumentError on malformed input.

$ ruby -v code/time/iso8601-vs-parse.rb
ruby 2.4.3p205 (2017-12-14 revision 61247) [x86_64-darwin17]
Warming up --------------------------------------
        Time.iso8601    10.234k i/100ms
          Time.parse     4.228k i/100ms
Calculating -------------------------------------
        Time.iso8601    114.485k (± 3.5%) i/s -    573.104k in   5.012008s
          Time.parse     43.711k (± 4.1%) i/s -    219.856k in   5.038349s

Comparison:
        Time.iso8601:   114485.1 i/s
          Time.parse:    43710.9 i/s - 2.62x  slower

Range

cover? vs include? code

cover? only check if it is within the start and end, include? needs to traverse the whole range.

$ ruby -v code/range/cover-vs-include.rb
ruby 2.2.3p173 (2015-08-18 revision 51636) [x86_64-linux]

Calculating -------------------------------------
        range#cover?    85.467k i/100ms
      range#include?     7.720k i/100ms
       range#member?     7.783k i/100ms
       plain compare   102.189k i/100ms
-------------------------------------------------
        range#cover?      1.816M (± 5.6%) i/s -      9.060M
      range#include?     83.344k (± 5.0%) i/s -    416.880k
       range#member?     82.654k (± 5.0%) i/s -    412.499k
       plain compare      2.581M (± 6.2%) i/s -     12.876M

Comparison:
       plain compare:  2581211.8 i/s
        range#cover?:  1816038.5 i/s - 1.42x slower
      range#include?:    83343.9 i/s - 30.97x slower
       range#member?:    82654.1 i/s - 31.23x slower

Less idiomatic but with significant performance ruby

Checkout: https://github.com/fastruby/fast-ruby/wiki/Less-idiomatic-but-with-significant-performance-difference

Submit New Entry

Please! Edit this README.md then Submit a Awesome Pull Request!

Something went wrong

Code example is wrong? 😢 Got better example? 😍 Excellent!

Please open an issue or Open a Pull Request to fix it.

Thank you in advance! 😉 🍺

One more thing

Share this with your #Rubyfriends! <3

Brought to you by @JuanitoFatas

Feel free to talk with me on Twitter! <3

Also Checkout

License

CC-BY-SA

This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Code License

CC0 1.0 Universal

To the extent possible under law, @JuanitoFatas has waived all copyright and related or neighboring rights to "fast-ruby".

This work belongs to the community.

fast-ruby's People

Contributors

284km avatar aisayo avatar arcovion avatar arielj avatar avellable avatar bdewater avatar bejmuller avatar bquorning avatar damirsvrtan avatar dideler avatar drenmi avatar etagwerker avatar ixti avatar jacobevelyn avatar juanitofatas avatar kindoflew avatar lubc avatar lvl0nax avatar majjoha avatar mateusdeap avatar narkoz avatar nateberkopec avatar nirvdrum avatar nmeylan avatar parkerfinch avatar sshaw avatar styd avatar wasaylor avatar ydah avatar yous avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fast-ruby's Issues

Hash#fetch with second argument is slower than Hash#fetch with block is not FAIR

Hi! First of all thanks for the nice gem! You are doing great work, making thousand of ruby programs run faster!

Now.. the issue)

Offense Hash#fetch with second argument is slower than Hash#fetch with blockis not fair.

You have this benchamrk: https://github.com/JuanitoFatas/fast-ruby/blob/master/code/hash/fetch-vs-fetch-with-block.rb

require "benchmark/ips"

HASH = { writing: :fast_ruby }
DEFAULT = "fast ruby"

Benchmark.ips do |x|
  x.report("Hash#fetch + const") { HASH.fetch(:writing, DEFAULT) }
  x.report("Hash#fetch + block") { HASH.fetch(:writing) { "fast ruby" } }
  x.report("Hash#fetch + arg")   { HASH.fetch(:writing, "fast ruby") }
  x.compare!
end

But it's assuming that key is always present. What is not really? Otherwise why one passes default value?

Here is my benchmark with hit and miss cases:

require 'benchmark'

N = 10_000_000

hash = { a: 10, b: 20, c: 30, e: 40 }

Benchmark.bm(15, "rescue/condition") do |x|
  x.report("with 2nd arg (hit) ") do
    N.times { hash.fetch(:a, false) }
  end

  x.report("with block (hit)   ") do
    N.times { hash.fetch(:a) { false } }
  end

  x.report("with 2nd arg (miss)") do
    N.times { hash.fetch(:x, false) }
  end

  x.report("with block (miss)  ") do
    N.times { hash.fetch(:x) { false } }
  end
end

Here is the output (ruby 2.1.5p273):

                      user     system      total        real
with 2nd arg (hit)   1.050000   0.000000   1.050000 (  1.041781)
with block (hit)     1.030000   0.000000   1.030000 (  1.031664)
with 2nd arg (miss)  1.010000   0.000000   1.010000 (  1.010886)
with block (miss)    1.760000   0.000000   1.760000 (  1.755389)

You see that with 2nd arg (hit) and with 2nd arg (hit) is almost the same, but diff between with 2nd arg (miss) and with block (miss) is quite big.

So, IMHO, this offense is not fair and it should be removed.

Thanks!

enumerable sort vs sort_by

noticed that in the test for enumerable sort vs sort_by sort_by uses symbol.to_proc while sort uses a block. wouldn't this effect the results since another tests shows symbol to proc is faster than block?

Fix warning from code/general/assignment example on Travis

https://travis-ci.org/JuanitoFatas/fast-ruby/jobs/58037357

$ ruby -v code/general/assignment.rb
ruby 2.2.1p85 (2015-02-26 revision 49769) [x86_64-linux]
code/general/assignment.rb:4: warning: assigned but unused variable - a
code/general/assignment.rb:4: warning: assigned but unused variable - b
code/general/assignment.rb:4: warning: assigned but unused variable - c
code/general/assignment.rb:4: warning: assigned but unused variable - d
code/general/assignment.rb:4: warning: assigned but unused variable - e
code/general/assignment.rb:4: warning: assigned but unused variable - f
code/general/assignment.rb:4: warning: assigned but unused variable - g
code/general/assignment.rb:4: warning: assigned but unused variable - h
code/general/assignment.rb:8: warning: assigned but unused variable - a
code/general/assignment.rb:9: warning: assigned but unused variable - b
code/general/assignment.rb:10: warning: assigned but unused variable - c
code/general/assignment.rb:11: warning: assigned but unused variable - d
code/general/assignment.rb:12: warning: assigned but unused variable - e
code/general/assignment.rb:13: warning: assigned but unused variable - f
code/general/assignment.rb:14: warning: assigned but unused variable - g
code/general/assignment.rb:15: warning: assigned but unused variable - h
Calculating -------------------------------------
 Parallel Assignment    78.517k i/100ms
Sequential Assignment
                       102.299k i/100ms
-------------------------------------------------
 Parallel Assignment      3.422M (± 6.6%) i/s -     17.117M
Sequential Assignment
                          5.711M (±15.1%) i/s -     27.723M
Comparison:
Sequential Assignment:  5710524.8 i/s
 Parallel Assignment:  3421542.3 i/s - 1.67x slower

Not correct comparing

I consider it's not correct to compare bsearch with find on sorted array. There will be not so much difference between searching in shuffled array.

reverse.detect vs select { ... }.last

Third approach?

require 'benchmark/ips'

ARRAY = [*1..100]

def faster
  ARRAY.reverse_each { |x| break x if (x % 10).zero? }
end

def fast
  ARRAY.reverse.detect { |x| (x % 10).zero? }
end

def slow
  ARRAY.select { |x| (x % 10).zero? }.last
end

Benchmark.ips do |x|
  x.report('Enumerable#reverse_each + break') { faster }
  x.report('Enumerable#reverse.detect') { fast }
  x.report('Enumerable#select.last')    { slow }
  x.compare!
end
% ruby reverse.rb
Calculating -------------------------------------
Enumerable#reverse_each + break
                       115.335k i/100ms
Enumerable#reverse.detect
                        72.531k i/100ms
Enumerable#select.last
                        11.243k i/100ms
-------------------------------------------------
Enumerable#reverse_each + break
                          3.311M (± 7.2%) i/s -     16.493M
Enumerable#reverse.detect
                          1.255M (± 9.3%) i/s -      6.238M
Enumerable#select.last
                        129.592k (± 3.4%) i/s -    652.094k

Comparison:
Enumerable#reverse_each + break:  3310776.9 i/s
Enumerable#reverse.detect:  1255082.9 i/s - 2.64x slower
Enumerable#select.last:   129592.0 i/s - 25.55x slower

Remove `String#casecmp` part

The String#casecmp method does not work with Unicode (even in Ruby 2.4.1), this is written in the documentation.

Example:

'Привет'.casecmp('привет') # => -1

There is a method String#casecmp?, which works with Unicode.

Example:

'Привет'.casecmp?('привет') # => true

But String.casecmp? is slower:

Warming up --------------------------------------
String#downcase + ==   233.440k i/100ms
      String#casecmp   274.247k i/100ms
     String#casecmp?   219.906k i/100ms
Calculating -------------------------------------
String#downcase + ==      5.746M (± 1.7%) i/s -     28.947M in   5.039252s
      String#casecmp      6.942M (± 1.8%) i/s -     34.829M in   5.019073s
     String#casecmp?      4.517M (± 2.6%) i/s -     22.650M in   5.017864s

Comparison:
      String#casecmp:  6941676.9 i/s
String#downcase + ==:  5745893.8 i/s - 1.21x  slower
     String#casecmp?:  4517314.3 i/s - 1.54x  slower
Code

require 'benchmark/ips'

SLUG = 'ABCD'

def slow
  SLUG.downcase == 'abcd'
end

def fast
  SLUG.casecmp('abcd') == 0
end

def another
  SLUG.casecmp?('abcd')
end

Benchmark.ips do |x|
  x.report('String#downcase + ==') { slow }
  x.report('String#casecmp')       { fast }
  x.report('String#casecmp?')      { another }
  x.compare!
end

So, String#downcase + == is good compromise.

&method(...) section misleading

I found the &method(...) section a bit misleading, as I never found myself writing code that way, but alarmed me as I use Symbol#to_proc a lot. So I ran a different benchmark:

require "benchmark/ips"

def fast
  [1, 2, 3].map { |n| n.to_s }
end

def slow
  [1, 2, 3].map(&:to_s)
end

Benchmark.ips do |x|
  x.report("normal")  { fast }
  x.report("&method") { slow }
  x.compare!
end

Calculating -------------------------------------
              normal    59.170k i/100ms
             &method    57.531k i/100ms
-------------------------------------------------
              normal      1.118M (±11.5%) i/s -      5.562M
             &method      1.067M (±12.4%) i/s -      5.293M

Comparison:
              normal:  1118235.4 i/s
             &method:  1067078.5 i/s - 1.05x slower

Although I later found something targeting specifically to_proc I think it's worth making it clear that we're not talking about Symbol#to_proc syntax here. Thoughts?

Update all examples and results for fast comes first

Update template:

require 'benchmark/ips'

def fast
end

def slow
end

Benchmark.ips do |x|
  x.report('fast') { fast }
  x.report('slow') { slow }
  x.compare!
end

and reorganize all stuff according to 👆 👆 👆

Since x.compare! will show fast then slow, this may have a better reading experience when reading README IMO. Also document in CONTRIBUTING.md to give people notice when submits a new entry.

Refactoring code

I noticed that many examples in code/ do not match the template.

require "benchmark/ips"

def fast
end

def slow
end

Benchmark.ips do |x|
  x.report("fast code description") { fast }
  x.report("slow code description") { slow }
  x.compare!
end

Suggestion! Add comment on each example why one method is faster than another

Hi, JuanitoFatas. Thank you very much for your repo and I get a lot from it. The benchmarks can show which method is much faster clearly. However, I think if we add some comments to explain why it is much faster than another. How ruby implement the method in the source. I think that it would be much better to help users to understand it. Of course, I would like to help in this process.

Methods defined with `define_method` are slower to call

While define_method indeed defines methods slighly faster than module_eval, I think it should be noted that calling methods defined with define_method is slower than calling module_eval ones (because define_method creates a closure):

require "benchmark/ips"

object = Class.new {
  module_eval "def evaled_method; end"
  define_method(:defined_method) {}
}.new

Benchmark.ips do |x|
  x.report("module_eval")   { object.evaled_method }
  x.report("define_method") { object.defined_method }
  x.compare!
end
Calculating -------------------------------------
         module_eval    98.664k i/100ms
       define_method    93.809k i/100ms
-------------------------------------------------
         module_eval      6.794M (± 8.1%) i/s -     33.743M
       define_method      4.588M (± 6.7%) i/s -     22.889M

Comparison:
         module_eval:  6793763.3 i/s
       define_method:  4587570.1 i/s - 1.48x slower

I think you benefit more from a method being faster to call than faster to define.

Benchmark with memory allocations

Hi all,
I've made a version of benchmark-ips that also shows memory allocations: https://github.com/jondot/benchmark-ipsa

It extends benchmark-ips by hijacking the job object and getting the call list. It does not have any performance implication over the original test session.

This is one step closer to the go bench tool which allows you to embed allocation info in addition to the iterations stats.

x.between?(a,b) is faster (MRI 2.2.3)

require "benchmark/ips"
require "date"

BEGIN_OF_JULY = Date.new(2015, 7, 1)
END_OF_JULY = Date.new(2015, 7, 31)
DAY_IN_JULY = Date.new(2015, 7, 15)

Benchmark.ips do |x|
x.report('range#cover?') { (BEGIN_OF_JULY..END_OF_JULY).cover? DAY_IN_JULY }
x.report('range#include?') { (BEGIN_OF_JULY..END_OF_JULY).include? DAY_IN_JULY }
x.report('range#member?') { (BEGIN_OF_JULY..END_OF_JULY).member? DAY_IN_JULY }
x.report('plain compare') { BEGIN_OF_JULY < DAY_IN_JULY && DAY_IN_JULY < END_OF_JULY }
x.report('value.between?') { DAY_IN_JULY.between?(BEGIN_OF_JULY, END_OF_JULY) }

x.compare!
end

Some benchmarks are measured with wrong approach

tl;dr When we measure small things we have to remove all overheads which have impact. Wrapping with a method and block call is quite big overhead for most of benchmarks in this repository.

Suppose you want compare performance of "2 + 3" vs "2 * 3" calls. If you do it with a approach used in this repository you will get this results:

require "benchmark/ips"

def slow
  2 * 2
end

def fast
  2 + 2
end

Benchmark.ips do |x|
  x.report("2 * 2") { slow }
  x.report("2 + 2") { fast }
  x.compare!
end

(simplified output)

Comparison:
               2 + 2:  8304760.0 i/s
               2 * 2:  7535516.6 i/s - 1.10x slower

But there is one problem. Calling a method + surrounding block ({ slow }) has bigger overhead than calling Fixnum#+ or Fixnum#* itself. Thea easiest way to observe it is to repeat benchmarked operations in one call. Like this:

require "benchmark/ips"

def slow
  2*2; 2*2; 2*2; 2*2; 2*2; 2*2; 2*2; 2*2; 2*2; 2*2;
end

def fast
  2+2; 2+2; 2+2; 2+2; 2+2; 2+2; 2+2; 2+2; 2+2; 2+2;
end

Benchmark.ips do |x|
  x.report("2 * 2") { slow }
  x.report("2 + 2") { fast }
  x.compare!
end
Comparison:
               2 + 2:  4680545.3 i/s
               2 * 2:  3468681.3 i/s - 1.35x slower

See how results changed? Writing our benchmarks in this way would be quite problematic. Fortunately benchmark-ips gem has answer for that. Benchmark::IPS::Job#report method allows to pass string which will be compiled before benchmark is run. Passing right string allows to measure it properly:

require "benchmark/ips"

Benchmark.ips do |x|
  x.report("2 * 2", "2 * 2;" * 1_000)
  x.report("2 + 2", "2 + 2;" * 1_000)
  x.compare!
end
Comparison:
               2 + 2:    91567.5 i/s
               2 * 2:    57994.4 i/s - 1.58x slower

This is greatly explained here: https://docs.omniref.com/ruby/2.2.1/symbols/Benchmark/bm#annotation=4095926&line=182

How does this affect fast-ruby benchmarks? All benchmarks that call small things are flawed. One of them is Array#length vs Array#size vs Array#count benchmark. Here is the original code and result obtained on my computer:

require 'benchmark/ips'

ARRAY = [*1..100]

Benchmark.ips do |x|
  x.report("Array#length") { ARRAY.length }
  x.report("Array#size") { ARRAY.size }
  x.report("Array#count") { ARRAY.count }
  x.compare!
end
Comparison:
          Array#size:  8679483.2 i/s
        Array#length:  8664450.7 i/s - 1.00x slower
         Array#count:  7237299.5 i/s - 1.20x slower

The same benchmark measure with described approach gives different numbers:

require 'benchmark/ips'

ARRAY = [*1..100]

Benchmark.ips do |x|
  x.report("Array#length", "ARRAY.length;" * 1_000)
  x.report("Array#size",   "ARRAY.size;"   * 1_000)
  x.report("Array#count",  "ARRAY.count;"  * 1_000)
  x.compare!
end
Comparison:
          Array#size:   113902.4 i/s
        Array#length:   113655.9 i/s - 1.00x slower
         Array#count:    28753.4 i/s - 3.96x slower

Difference: 1.20x slower vs 3.96x slower.

My guess is that it affects most of benchmarks.

JRuby warmup

This was sort of touched upon in #23 in the sense that JRuby may end up doing something different from MRI. The one major problem I see with these benchmarks is they don't include a warm up phase which the JVM sometimes uses for optimisation. There are potentially other things that we would want to enable specifically for JRuby.

Hash#fetch with argument vs Hash#fetch + block

This one seems a bit misleading. Simply switching the default to e.g. a symbol or constant-defined string makes both versions perform essentially identically (the argument version is actually slightly faster).

The difference in the current version is (AFAIK) exclusively due to the time taken to allocate the string (which is elided in the block version). This makes the displayed time comparison quite misleading (at least from my perspective), as the ratio can be increased/decreased at will be changing the runtime of the default expression.

each with index vs while loop returning different values

From: https://github.com/JuanitoFatas/fast-ruby/blob/master/code/enumerable/each_with_index-vs-while-loop.rb
In the example code, the return types are returning different objects and may be effecting the overall Benchmarking time. I came across this in another gem. I'll be adding code to see if this is true or not.

2.2.2 :001 > ARRAY = [*1..100]
 => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100] 
2.2.2 :002 >   ARRAY.each_with_index do |number, index|
2.2.2 :003 >         number + index
2.2.2 :004?>     end
 => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100] 
2.2.2 :005 >   index = 0
 => 0 
2.2.2 :006 >   while index < ARRAY.size
2.2.2 :007?>       ARRAY[index] + index
2.2.2 :008?>       index += 1
2.2.2 :009?>     end
 => nil 

"begin...rescue vs respond_to? for Control Flow" inaccurate if method exists

The section "begin...rescue vs respond_to? for Control Flow" is correct when the receiver does not respond to the method (because rescuing an exception is expensive), but when the receiver does respond to the method the begin...rescue idiom is much faster—in my tests about 15x faster. This makes sense since, if the method does exist, begin...rescue has no overhead, but respond_to? has the overhead of an additional method call whether the method exists or not.

You can see my tests and their results here: https://gist.github.com/jrunning/0e18236c6363af15c520 I tested a number of different scenarios: implicit receiver ("LocalVariableOrMethod"), explicit receiver, a long inheritance chain, and implicit receiver with "global" methods, but in all cases it was pretty much the same:

Comparison:
test_rescue_global_does_exist:  7198903.1 i/s
test_respond_to_global_doesnt_exist:  4493565.9 i/s - 1.60x slower
test_respond_to_global_does_exist:  4476828.4 i/s - 1.61x slower
test_rescue_global_doesnt_exist:   440868.3 i/s - 16.33x slower

In other words, rescue is fastest when the method does exist, respond_to? is 1.42–1.71x slower whether the method exists or not, and rescue is 16–18x slower when the method doesn't exist.

In light of this I think it's harder to give definitive advice on rescue vs. respond_to?. If someone is looking for the fastest idiom then it depends on whether they think the method is more likely to exist or not to exist.

"case...when" VS hash

I would love to see a fair comparison between "case...when" and hashes in cases like this:

def function(param)
  case param
    when :one then 1
    when :two then 2
  end
end

def function2(param)
  {
    one: 1,
    two: 2
  }[param]
end

String concat contains dummy comparision.

You are doing your test without any randomization and length variety, so results are incorrect completely.

You simply just testing how fast are 'foo' and 'bar' concatenation, and only

Live case scenario looks more like this:

# strings length is a ( 100 + rand( 100 ) ), i.e. short strings max difference around double 

 String#append:   439150.7 i/s
            String#+:   419672.1 i/s - same-ish: difference falls within error
  "#{'foo'}#{'bar'}":   365799.6 i/s - 1.20x  slower
       String#concat:   353912.5 i/s - 1.24x  slower

 # strings length is a  (1000 + rand( 1000) ) , i.e. longer strings max difference around double

            String#+:   298633.5 i/s
   String#append:   284566.8 i/s - same-ish: difference falls within error
  "#{'foo'}#{'bar'}":   267548.4 i/s - 1.12x  slower
    String#concat:   215419.4 i/s - 1.39x  slower

# string length is a ( 100 + rand(1000) ), i.e. highly variable length
             String#+:   388205.2 i/s
   String#append:   357669.3 i/s - 1.09x  slower
  "#{'foo'}#{'bar'}":   331617.7 i/s - 1.17x  slower
    String#concat:   286717.8 i/s - 1.35x  slower

# string length is a 1 + rand 1000, i.e. random string
             String#+:   385180.8 i/s
   String#append:   373926.8 i/s - same-ish: difference falls within error
  "#{'foo'}#{'bar'}":   341496.6 i/s - 1.13x  slower
   String#concat:   297988.1 i/s - 1.29x  slower

OK

# This is completely oposite to
         "foo" "bar":  5369594.5 i/s
 "#{'foo'}#{'bar'}":  5181745.7 i/s - same-ish: difference falls within error
   String#append:  3075719.2 i/s - 1.75x slower
    String#concat:  3016703.5 i/s - 1.78x slower
            String#+:  2977282.7 i/s - 1.80x slower

Also you didn't test on multiple string concat, where actually "#{'foo'} #{'bar'} #{'shines'}".

taking issue with Hash#fetch with argument vs Hash#fetch + block code

I have a bit of an issue with the

Hash#fetch with argument vs Hash#fetch + block code example.

My issue is that in order for the call to fetch NOT using the block to take place, the second argument needs to be evaluated. Depending on the complexity of the second argument ([*1.100]), you can get HIGH degree of variation in performance. An example like creating a new large array object, vs. a simple integer or string will change the results considerably, as it would with any method call where the parameters need to be evaluated BEFORE the call regardless of there usage during the method execution. A lot has to do with GC and recreation of objects vs primitives, vs recurring strings/symbols.

I suggest a more common example (my inference) would be to use something simpler for the second argument to make it an apples-to-apples comparison. This all stems from the fact that fetch does not USE the second argument or the block - and your example is biased that way, but any method call requires pre-evaluation of the arguments. SO either always use the argument AND block. or make the arguments simple enough that the work for the argument creation is not a timing element.

I do agree that not executing the block vs executing the block is comparative, but the measurements are extremely dependent on that second arguments evaluation, given the example is one that does NOT use the block call.

Move away from comparative speed method names

This is a proposal to move away from method names like slow, fast, and fastest and instead use something named after the operation. The convention is a bit error prone when authoring the benchmark to begin with, since you can't know a priori what the relative speeds are until you've written the benchmarks. Thus, it requires a renaming of the methods after the benchmark is run. It may introduce confirmation bias as well.

The real problem, I think, is it risks the entire repository becoming bit rotted. Ruby's performance is not fixed in time. If Ruby 3 manages to hit its 3x performance gain, it may very well come at the cost of reshuffling the relative performance of methods involved in these benchmarks. Then we'd be looking at the unenviable case of the fast method being faster than the fastest method and so on.

Going along with performance being implementation-specific across MRI versions, the problem is exacerbated when running the benchmarks on alternative Ruby implementations. In that case, the relative method values do not hold in many cases.

Block vs. Symbol#to_proc

Your example shows that the Block variant is slower. I remembered measuring the allegedly same with different results, so I looked again at the code files. Turns out I used an Array instead of a Range.

If I change the Range to an Array in line 3 of code/proc-and-block/block-vs-to_proc.rb, the benchmark yields inverted results:

$ ruby -v code/proc-and-block/block-vs-to_proc.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-linux]

Calculating -------------------------------------
               Block    51.849k i/100ms
      Symbol#to_proc    48.724k i/100ms
-------------------------------------------------
               Block      1.075M (± 0.7%) i/s -      5.392M
      Symbol#to_proc    937.576k (± 0.8%) i/s -      4.726M

Comparison:
               Block:  1075189.7 i/s
      Symbol#to_proc:   937575.7 i/s - 1.15x slower

So Block vs. Symbol#to_proc efficiency seems to depend on the object operated on.

Hash fetch with symbols vs strings.

Description is not really correct. Difference between :symbol key and "string" keys is that Symbols are immutable constants, while in case of using a String key you creating that string over and over again. Starting with Ruby 2.1, you can mark string as frozen and it will be created only once:

10.times.map { "foobar".object_id }.uniq.count # => 10
10.times.map { "foobar".freeze.object_id }.uniq.count # => 10

See:

Needs an article on JRUBY vs MRI

I'm running the benchmarks under JRUBY. I will analyse the results and compare them with MRI, because the community needs to know if some of the recommendations for MRI are bad for JRuby code and vice versa.

Can I add a Wiki page? Would that be the best?

(arr1 - arr2) != arr1 vs (arr1 - arr2).length != arr1.length

Hi, this could be a silly question, as it looks like comparison by length should be faster... I just want to make sure :)

For example you need to check if arr1 includes any elements of arr2. So you can check either if (arr1 - arr2) != arr1 or (arr1 - arr2).length != arr1.length.

Array#concat vs Array#+

require "benchmark/ips"

RANGE = (0..100_000)

def fast
  array = []
  RANGE.each { |number| array.concat([number]) }
end

def slow
  array = []
  RANGE.each { |number| array += [number] }
end

Benchmark.ips do |x|
  x.report("Array#concat") { fast }
  x.report("Array#+")      { slow }
  x.compare!
end
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin18]

Warming up --------------------------------------
        Array#concat     5.000  i/100ms
             Array#+     1.000  i/100ms
Calculating -------------------------------------
        Array#concat     62.530  (±11.2%) i/s -    310.000  in   5.037308s
             Array#+      0.194  (± 0.0%) i/s -      1.000  in   5.155399s

Comparison:
        Array#concat:       62.5 i/s
             Array#+:       0.2 i/s - 322.37x  slower

Happy to make a PR if you want this added

ends_with? advice incorrect

The following text in the regex vs. starts_with?/ends_with? bit (which has a ! next to it) is flatly incorrect:

You can combine start_with? and end_with? to replace error.path =~ /^#{path}(\.rb)?$/ to this 
error.path.start_with?(path) && error.path.end_with?('.rb', '')
—— @igas rails/rails#17316

error.path.end_with?('.rb', ‘’) is equivalent to error.path.end_with?(‘’) which always returns true and is therefore useless. In addition, the combination of starts_with? and ends_with? does not test that nothing else is between path and the end except .rb like the regexp does.

(Notably the cited PR was rejected and it was noted that some of the other suggested replacements were wrong.)

This paragraph should be just deleted, I think.

inspect vs to_json for simple arrays

If in array contain just strings can I use inspect instead of to_json

require 'benchmark/ips'

Benchmark.ips do |x|
x.report("inspect") do 
dd=['2', 4, 8, 4.8, 'yuunnnd', 89, 'rrrrrr', 'fsdfsdafasdfasdfasdfas']
dd.inspect
end
x.report('to_json') do
dd=['2', 4, 8, 4.8, 'yuunnnd', 89, 'rrrrrr', 'fsdfsdafasdfasdfasdfas']
dd.to_json
end
x.compare!
end
Warming up --------------------------------------
             inspect    13.918k i/100ms
             to_json     3.447k i/100ms
Calculating -------------------------------------
             inspect    201.462k (±14.1%) i/s -    988.178k
             to_json     39.050k (± 8.7%) i/s -    196.479k

Comparison:
             inspect:   201462.2 i/s
             to_json:    39049.5 i/s - 5.16x slower

=> #<Benchmark::IPS::Report:0x007fc689d1fe28 @entries=[#<Benchmark::IPS::Report::Entry:0x007fc6917d93a8 @label="inspect", @microseconds=5001375.913619995, @iterations=988178, @ips=201462.15039984178, @ips_sd=28314, @measurement_cycle=13918, @show_total_time=false>, #<Benchmark::IPS::Report::Entry:0x007fc693974558 @label="to_json", @microseconds=5068800.210952759, @iterations=196479, @ips=39049.53170380274, @ips_sd=3409, @measurement_cycle=3447, @show_total_time=false>], @data=nil>

Can I? Using inspect looks like unnatural but very fast, can some one explain about cons.

Enumerable#sort_by is not always faster than #sort

Per the Ruby 2.4.0 docs:

The current implementation of sort_by generates an array of tuples containing the original collection element and the mapped value. This makes sort_by fairly expensive when the keysets are simple.

Here's a concrete example showing sort to be 2.70x faster in Ruby 1.9.3, 2.3.3 and 2.4.0:

require 'benchmark/ips'

Benchmark.ips do |x|
  x.time = 5
  x.warmup = 2

  ARRAY = %w{apple pear fig}

  x.report("sort_by") do
    ARRAY.sort_by(&:length)
  end

  x.report("sort") do
    ARRAY.sort { |a, b| a.length <=> b.length}
  end

  x.compare!
end
Warming up --------------------------------------
             sort_by    56.348k i/100ms
                sort   111.946k i/100ms
Calculating -------------------------------------
             sort_by    635.646k (±16.1%) i/s -      3.099M in   5.074036s
                sort      1.713M (±16.3%) i/s -      8.284M in   5.023904s

Comparison:
                sort:  1713232.1 i/s
             sort_by:   635645.9 i/s - 2.70x slower

Remove "raise vs E2MM#Raise" benchmark from repository

It's nice that someone found that this library is so slow but I think most Ruby devs don't know that e2mmap even exists (actually I learned about this library from this repo). IMHO it would be better to not tell other developers that it exists. I even grepped all gems from ~/.rbenv/ (more that 2000 gems) and only rdoc & yard gems use it (actually they include file ruby_lex.rb). All other benchmarks are more or less used in a real code but this one is completely impractical.

Parallel assignment is equal to sequential assignment

This one is probably the most confusing benchmark I've ever seen. Original idea was that sequential assignment is faster than parallel (https://speakerdeck.com/sferik/writing-fast-ruby?slide=45). Later @charliesome found (#50) that this benchmark was incorrect because Ruby creates new array with all variables when parallel assignment is last expression in a method. After recalculation parallel assignment became faster than sequential.

Which one is actually faster?

I claim that none of them. Parallel assignment is actually equal to sequential. In the first comment we can read: "In fact, this is even faster than splitting these assignments out over multiple lines because the compiler does not need to emit a per-line trace instruction for each assignment."

I wonder why nobody broaden this topic. "trace" instruction is emitted for every line of your ruby program. Not only for assignments, but for every line. Does it mean we should write our programs as a oneliners? Absolutely not! "trace" has very low overhead. But suppose you are paranoid... Surprise! Ruby VM (MRI) allows you to disable emitting this instructions completely. Here is the trick:

RubyVM::InstructionSequence.compile_option = {
  trace_instruction: false
}

Put this one in a file "disable_trace.rb" and load it before loading your program (you can't load this in the same script because it's too late, your program is already compiled to bytecode).

$ ruby -r./disable_trace assignment.rb
Calculating -------------------------------------
 Parallel Assignment   149.276k i/100ms
Sequential Assignment
                       149.712k i/100ms
-------------------------------------------------
 Parallel Assignment      7.963M (± 5.2%) i/s -     39.707M
Sequential Assignment
                          7.947M (± 5.2%) i/s -     39.674M

Comparison:
 Parallel Assignment:  7962929.1 i/s
Sequential Assignment:  7946966.7 i/s - 1.00x slower

This is how your bytecode actually looks like when you disable emitting "trace" instruction:

puts RubyVM::InstructionSequence.compile("a = 1;\nb = 2;", nil, nil, nil, trace_instruction: false).disasm
== disasm: #<ISeq:<compiled>@<compiled>>================================
local table (size: 3, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 3] a          [ 2] b
0000 putobject_OP_INT2FIX_O_1_C_                                      (   1)
0001 setlocal_OP__WC__0 3
0003 putobject        2                                               (   2)
0005 dup
0006 setlocal_OP__WC__0 2
0008 leave

See? No trace instructions.

Summary:

  • Sequential is not slower or faster than parallel, they are equal.
  • Ruby adds "trace" instruction for every line of code, not only for assignments.
  • Usually when you use multiple assignments you do it in multiple lines. This is why you might think that sequential assignment is slower than parallel. But remember: "Correlation does not imply causation".
  • You can disable emitting "trace" instruction if you are paranoid.
  • In practice it won't make too much difference if your program does something more seriously.

Also, small digression. It would be better to educate people WHY and WHEN something can be slower/faster rather than making such a strong statements like "X is faster than Y".

Enumerable#reverse_each.detect is even faster than Enumerable#reverse.detect

This also avoids an extra array allocation for #reverse

require 'benchmark/ips'

ARRAY = [*1..100]

def really_fast
    ARRAY.reverse_each.detect { |x| (x % 10).zero? }
end

def fast
  ARRAY.reverse.detect { |x| (x % 10).zero? }
end

def slow
  ARRAY.select { |x| (x % 10).zero? }.last
end

Benchmark.ips do |x|
  x.report('Enumerable#reverse_each.detect') { really_fast }
  x.report('Enumerable#reverse.detect') { fast }
  x.report('Enumerable#select.last')    { slow }
  x.compare!
end
Calculating -------------------------------------
Enumerable#reverse_each.detect
                        46.838k i/100ms
Enumerable#reverse.detect
                        42.859k i/100ms
Enumerable#select.last
                        11.124k i/100ms
-------------------------------------------------
Enumerable#reverse_each.detect
                        933.887k (± 1.9%) i/s -      4.684M
Enumerable#reverse.detect
                        808.326k (± 3.1%) i/s -      4.072M
Enumerable#select.last
                        127.865k (± 4.4%) i/s -    645.192k

Comparison:
Enumerable#reverse_each.detect:   933887.0 i/s
Enumerable#reverse.detect:   808325.8 i/s - 1.16x slower
Enumerable#select.last:   127865.2 i/s - 7.30x slower

string/concatenation.rb tests are misleading

Hi, the tests in string/concatenation.rb are quite misleading.

The fast method consists of this

def fast
  'foo' 'bar'
end

That's not concatenating during calling of fast but on parsing the code. If you write another method just returning foobar, it is as fast as this method.

So I think this is not fair comparison and what you usually want is to concatenate two variables during runtime.

For this use case concat and << are calling the same code, so they have the same performance and both are fine if you want to change the string on the left and not just get two strings concatenated. If you want a new string you can use +.

Some better test could be to compare + and String interpolation

Benchmark.ips do |x|
  foo = 'foo'
  bar = 'bar'

  x.report('String#+') do
    foo + bar
  end

  x.report('String interpolation') do
    "#{foo}#{bar}"
    end

  x.compare!
end

This still has the difference that interpolation can handle nil values, while + cannot.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.