diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml new file mode 100644 index 00000000..29e4270b --- /dev/null +++ b/.github/workflows/benchmarks.yml @@ -0,0 +1,81 @@ +name: Benchmarks + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + rake: + runs-on: ubuntu-latest + + strategy: + matrix: + ruby-version: [ + 'ruby-head', '3.2', '3.1', '3.0', '2.7', '2.6', '2.5', '2.4', + 'jruby-head', + 'truffleruby-head' + ] + + steps: + - name: Set Share Env + if: github.ref_name == 'main' + run: | + echo "SHARE=1" >> $GITHUB_ENV + - uses: actions/checkout@v3 + - name: Set up Ruby ${{ matrix.ruby-version }} + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + - name: Install dependencies + run: bundle install + - name: Run benchmarks + run: bundle exec rake + + rake_old: + runs-on: ubuntu-22.04 + + strategy: + matrix: + ruby-version: [ + 'jruby-9.1', + 'truffleruby-22' + ] + + steps: + - name: Set Share Env + if: github.ref_name == 'main' + run: | + echo "SHARE=1" >> $GITHUB_ENV + - uses: actions/checkout@v3 + - name: Set up Ruby ${{ matrix.ruby-version }} + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + - name: Install dependencies + run: bundle install + - name: Run benchmarks + run: bundle exec rake + + rake_older: + runs-on: ubuntu-20.04 + + strategy: + matrix: + ruby-version: ['2.3', '2.2', '2.1'] + + steps: + - name: Set Share Env + if: github.ref_name == 'main' + run: | + echo "SHARE=1" >> $GITHUB_ENV + - uses: actions/checkout@v3 + - name: Set up Ruby ${{ matrix.ruby-version }} + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + - name: Install dependencies + run: bundle install + - name: Run benchmarks + run: bundle exec rake diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..1cd9aadb --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.bundle/ +*.bundle +/Gemfile.lock diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9761f7f0..00000000 --- a/.travis.yml +++ /dev/null @@ -1,24 +0,0 @@ -sudo: false -cache: bundler -bundler_args: --retry=3 --jobs=3 -language: ruby -rvm: - - 2.0 - - 2.1 - - 2.2.2 - - 2.2.1 - - 2.2.0 - - ruby-head - - jruby - - jruby-head - - rbx-2 -matrix: - allow_failures: - - rvm: 2.2.1 - - rvm: 2.2.0 - - rvm: ruby-head - - rvm: jruby - - rvm: juby-head - - rvm: rbx-2 - fast_finish: true - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba4a1f01..1d698903 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ If you find any typos, errors, or have an better example. Just raise a new issue <3 -These idioms list here are trying to satisfiy following goals: +These idioms list here are trying to satisfy following goals: [![GOALS](/images/Goals.png)](https://speakerdeck.com/sferik/writing-fast-ruby?slide=11) diff --git a/Gemfile b/Gemfile index 1a42963e..00d364d9 100644 --- a/Gemfile +++ b/Gemfile @@ -2,4 +2,7 @@ source 'https://rubygems.org' gem 'benchmark-ips', '>= 2.0' +gem 'activesupport', '>= 2.2.1' +gem 'e2mmap' + gem 'rake' diff --git a/README.md b/README.md index 6834d9b8..cecf3ce9 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,21 @@ -Fast Ruby [![Build Status](https://travis-ci.org/JuanitoFatas/fast-ruby.svg?branch=travis)](https://travis-ci.org/JuanitoFatas/fast-ruby) +Fast Ruby [![Benchmarks](https://github.com/fastruby/fast-ruby/actions/workflows/benchmarks.yml/badge.svg)](https://github.com/fastruby/fast-ruby/actions/workflows/benchmarks.yml) ======================================================================================================================================================================= In [Erik Michaels-Ober](https://github.com/sferik)'s great talk, 'Writing Fast Ruby': [Video @ Baruco 2014](https://www.youtube.com/watch?v=fGFM_UrSp70), [Slide](https://speakerdeck.com/sferik/writing-fast-ruby), 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](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. : ) +All results listed in README.md are running with Ruby 4.0.0 on macOS 15.6.1. Machine information: MacBook Pro (14-inch, Nov 2024), Apple M4 Pro, 48 GB RAM. Your results may vary, but you get the idea. : ) -You can checkout [the travis build](https://travis-ci.org/JuanitoFatas/fast-ruby) for these benchmark results ran against different Ruby implementations. +You can checkout [the GitHub Actions build](https://github.com/fastruby/fast-ruby/actions) for these benchmark results ran against different Ruby implementations. **Let's write faster code, together! <3** +Analyze your code +----------------- + +Checkout the [fasterer](https://github.com/DamirSvrtan/fasterer) project - it's a static analysis that checks speed idioms written in this repo. + Measurement Tool ----------------- @@ -37,162 +42,325 @@ end Idioms ------ +### Index + +- [General](#general) +- [Array](#array) +- [Date](#date) +- [Enumerable](#enumerable) +- [Hash](#hash) +- [Proc & Block](#proc--block) +- [String](#string) +- [Time](#time) +- [Range](#range) + ### General -##### Parallel Assignment vs Sequential Assignment [code](code/general/assignment.rb) +##### `attr_accessor` vs `getter and setter` [code](code/general/attr-accessor-vs-getter-and-setter.rb) -> Parallel Assignment allocates an extra array. +> https://www.omniref.com/ruby/2.2.0/files/method.h?#annotation=4081781&line=47 ``` -$ ruby -v code/general/assignment.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] - +$ ruby -v code/general/attr-accessor-vs-getter-and-setter.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + getter_and_setter 1.122M i/100ms + attr_accessor 1.303M i/100ms Calculating ------------------------------------- - Parallel Assignment 99.146k i/100ms -Sequential Assignment 127.143k i/100ms -------------------------------------------------- - Parallel Assignment 2.522M (± 7.5%) i/s - 12.592M -Sequential Assignment 5.686M (± 8.6%) i/s - 28.226M + getter_and_setter 10.999M (± 2.1%) i/s (90.92 ns/i) - 56.083M in 5.101077s + attr_accessor 12.517M (± 1.5%) i/s (79.89 ns/i) - 63.841M in 5.101477s Comparison: -Sequential Assignment: 5685750.0 i/s - Parallel Assignment: 2521708.9 i/s - 2.25x slower + attr_accessor: 12517008.0 i/s + getter_and_setter: 10999275.5 i/s - 1.14x slower ``` ##### `begin...rescue` vs `respond_to?` for Control Flow [code](code/general/begin-rescue-vs-respond-to.rb) ``` $ ruby -v code/general/begin-rescue-vs-respond-to.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] - +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + begin...rescue 230.601k i/100ms + respond_to? 1.915M i/100ms 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 + begin...rescue 2.390M (± 0.9%) i/s (418.40 ns/i) - 11.991M in 5.017466s + respond_to? 18.745M (± 1.8%) i/s (53.35 ns/i) - 93.843M in 5.007990s Comparison: - respond_to?: 3276972.3 i/s - begin...rescue: 371591.0 i/s - 8.82x slower + respond_to?: 18744998.8 i/s + begin...rescue: 2390076.1 i/s - 7.84x slower ``` ##### `define_method` vs `module_eval` for Defining Methods [code](code/general/define_method-vs-module-eval.rb) ``` $ ruby -v code/general/define_method-vs-module-eval.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- +module_eval with string + 630.000 i/100ms + define_method 685.000 i/100ms +Calculating ------------------------------------- +module_eval with string + 5.633k (±18.1%) i/s (177.53 μs/i) - 27.090k in 5.009884s + define_method 7.239k (±17.6%) i/s (138.14 μs/i) - 34.250k in 5.040522s + +Comparison: + define_method: 7238.9 i/s +module_eval with string: 5632.8 i/s - same-ish: difference falls within error +``` + +##### `String#constantize` vs a comparison for inflection [code](code/general/constantize-vs-comparison.rb) + +ActiveSupport's [String#constantize](https://guides.rubyonrails.org/active_support_core_extensions.html#constantize) "resolves the constant reference expression in its receiver". + +[Read the rationale here](https://github.com/fastruby/fast-ruby/pull/200) + +``` +$ ruby -v code/general/constantize-vs-comparison.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- +using an if statement + 1.507M i/100ms + String#constantize 1.048M i/100ms +Calculating ------------------------------------- +using an if statement + 15.518M (± 1.2%) i/s (64.44 ns/i) - 78.355M in 5.050041s + String#constantize 10.556M (± 1.0%) i/s (94.73 ns/i) - 53.448M in 5.063570s + +Comparison: +using an if statement: 15517955.2 i/s + String#constantize: 10556362.4 i/s - 1.47x slower +``` + +##### `raise` vs `E2MM#Raise` for raising (and defining) exceptions [code](code/general/raise-vs-e2mmap.rb) + +Ruby's [Exception2MessageMapper module](http://ruby-doc.org/stdlib-2.2.0/libdoc/e2mmap/rdoc/index.html) allows one to define and raise exceptions with predefined messages. + +``` +$ ruby -v code/general/raise-vs-e2mmap.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- +Ruby exception: E2MM#Raise + 8.751k i/100ms +Ruby exception: Kernel#raise + 254.268k i/100ms +Calculating ------------------------------------- +Ruby exception: E2MM#Raise + 88.269k (± 0.5%) i/s (11.33 μs/i) - 446.301k in 5.056287s +Ruby exception: Kernel#raise + 2.571M (± 1.0%) i/s (389.01 ns/i) - 12.968M in 5.044976s + +Comparison: +Ruby exception: Kernel#raise: 2570660.6 i/s +Ruby exception: E2MM#Raise: 88268.9 i/s - 29.12x slower + +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- +Custom exception: E2MM#Raise + 8.837k i/100ms +Custom exception: Kernel#raise + 263.896k i/100ms +Calculating ------------------------------------- +Custom exception: E2MM#Raise + 88.322k (± 0.6%) i/s (11.32 μs/i) - 441.850k in 5.002885s +Custom exception: Kernel#raise + 2.599M (± 2.1%) i/s (384.78 ns/i) - 13.195M in 5.079300s + +Comparison: +Custom exception: Kernel#raise: 2598894.6 i/s +Custom exception: E2MM#Raise: 88322.1 i/s - 29.43x slower +``` + +##### `loop` vs `while true` [code](code/general/loop-vs-while-true.rb) + +``` +$ ruby -v code/general/loop-vs-while-true.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + While Loop 1.000 i/100ms + Kernel loop 1.000 i/100ms +Calculating ------------------------------------- + While Loop 1.331 (± 0.0%) i/s (751.41 ms/i) - 7.000 in 5.260296s + Kernel loop 0.528 (± 0.0%) i/s (1.89 s/i) - 3.000 in 5.709880s + +Comparison: + While Loop: 1.3 i/s + Kernel loop: 0.5 i/s - 2.52x slower +``` + +##### `ancestors.include?` vs `<=` [code](code/general/inheritance-check.rb) +``` +$ ruby -v code/general/inheritance-check.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + less than or equal 1.150M i/100ms + ancestors.include? 179.611k i/100ms 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 + less than or equal 11.426M (± 1.4%) i/s (87.52 ns/i) - 57.493M in 5.032894s + ancestors.include? 1.880M (± 1.0%) i/s (531.84 ns/i) - 9.519M in 5.063302s Comparison: - define_method: 1345.6 i/s -module_eval with string: 1129.7 i/s - 1.19x slower + less than or equal: 11425608.6 i/s + ancestors.include?: 1880262.8 i/s - 6.08x slower ``` -#### Method Invocation +### Method Invocation ##### `call` vs `send` vs `method_missing` [code](code/method/call-vs-send-vs-method_missing.rb) ``` $ ruby -v code/method/call-vs-send-vs-method_missing.rb -ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14] - +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + call 1.670M i/100ms + send 1.182M i/100ms + method_missing 837.965k i/100ms 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 + call 16.799M (± 0.3%) i/s (59.53 ns/i) - 85.150M in 5.068737s + send 11.845M (± 0.3%) i/s (84.42 ns/i) - 60.281M in 5.089071s + method_missing 8.392M (± 0.3%) i/s (119.17 ns/i) - 42.736M in 5.092719s Comparison: - call: 3811183.4 i/s - send: 3244239.1 i/s - 1.17x slower - method_missing: 2728893.0 i/s - 1.40x slower + call: 16799285.6 i/s + send: 11845293.0 i/s - 1.42x slower + method_missing: 8391692.6 i/s - 2.00x slower ``` ##### Normal way to apply method vs `&method(...)` [code](code/general/block-apply-method.rb) ``` $ ruby -v code/general/block-apply-method.rb -ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14] +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + normal 698.536k i/100ms + &method 322.901k i/100ms +Calculating ------------------------------------- + normal 6.978M (± 0.9%) i/s (143.31 ns/i) - 34.927M in 5.005888s + &method 3.214M (± 0.5%) i/s (311.12 ns/i) - 16.145M in 5.023139s + +Comparison: + normal: 6977718.6 i/s + &method: 3214214.6 i/s - 2.17x slower +``` +##### Function with single Array argument vs splat arguments [code](code/general/array-argument-vs-splat-arguments.rb) + +``` +$ ruby -v code/general/array-argument-vs-splat-arguments.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- +Function with single Array argument + 1.910M i/100ms +Function with splat arguments + 18.841k i/100ms 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 +Function with single Array argument + 18.818M (± 1.2%) i/s (53.14 ns/i) - 95.479M in 5.074468s +Function with splat arguments + 217.060k (±11.2%) i/s (4.61 μs/i) - 1.093M in 5.099583s Comparison: - normal: 1866669.5 i/s - &method: 467095.4 i/s - 4.00x slower +Function with single Array argument: 18818207.9 i/s +Function with splat arguments: 217059.9 i/s - 86.70x slower ``` -##### `define_method` vs `module_eval` for Defining Methods [code](code/general/define_method-vs-module-eval.rb) +##### Hash vs OpenStruct on access assuming you already have a Hash or an OpenStruct [code](code/general/hash-vs-openstruct-on-access.rb) ``` -$ ruby -v code/general/define_method-vs-module-eval.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] +$ ruby -v code/general/hash-vs-openstruct-on-access.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Hash 2.028M i/100ms + OpenStruct 1.478M i/100ms +Calculating ------------------------------------- + Hash 20.290M (± 0.5%) i/s (49.29 ns/i) - 103.430M in 5.097815s + OpenStruct 14.807M (± 0.6%) i/s (67.54 ns/i) - 75.387M in 5.091599s + +Comparison: + Hash: 20289714.4 i/s + OpenStruct: 14806564.8 i/s - 1.37x slower +``` +##### Hash vs OpenStruct (creation) [code](code/general/hash-vs-openstruct.rb) + +``` +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Hash 2.652M i/100ms + OpenStruct 31.241k i/100ms 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 + Hash 26.301M (± 1.0%) i/s (38.02 ns/i) - 132.614M in 5.042769s + OpenStruct 313.539k (± 1.8%) i/s (3.19 μs/i) - 1.593M in 5.083245s Comparison: - define_method: 1345.6 i/s -module_eval with string: 1129.7 i/s - 1.19x slower + Hash: 26300561.3 i/s + OpenStruct: 313538.7 i/s - 83.88x slower +``` + +##### Kernel#format vs Float#round().to_s [code](code/general/format-vs-round-and-to-s.rb) + ``` +$ ruby -v code/general/format-vs-round-and-to-s.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Float#round 465.786k i/100ms + Kernel#format 618.743k i/100ms + String#% 562.895k i/100ms +Calculating ------------------------------------- + Float#round 4.780M (± 0.6%) i/s (209.21 ns/i) - 24.221M in 5.067359s + Kernel#format 6.200M (± 1.4%) i/s (161.29 ns/i) - 31.556M in 5.090736s + String#% 5.642M (± 1.1%) i/s (177.25 ns/i) - 28.708M in 5.088913s +Comparison: + Kernel#format: 6199975.0 i/s + String#%: 5641863.3 i/s - 1.10x slower + Float#round: 4779984.9 i/s - 1.30x slower +``` ### Array ##### `Array#bsearch` vs `Array#find` [code](code/array/bsearch-vs-find.rb) -**WARNING:** `bsearch` ONLY works on *sorted array*. More details please see [#29](https://github.com/JuanitoFatas/fast-ruby/issues/29). +**WARNING:** `bsearch` ONLY works on *sorted array*. More details please see [#29](https://github.com/fastruby/fast-ruby/issues/29). ``` $ ruby -v code/array/bsearch-vs-find.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] - +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + find 1.000 i/100ms + bsearch 189.387k i/100ms 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 + find 0.703 (± 0.0%) i/s (1.42 s/i) - 4.000 in 5.692110s + bsearch 1.893M (± 1.6%) i/s (528.21 ns/i) - 9.469M in 5.003152s Comparison: - bsearch: 577300.7 i/s - find: 0.2 i/s - 3137489.63x slower + bsearch: 1893194.5 i/s + find: 0.7 i/s - 2691605.95x slower ``` -##### `Array#count` vs `Array#size` [code](code/array/count-vs-size.rb) +##### `Array#length` vs `Array#size` vs `Array#count` [code](code/array/length-vs-size-vs-count.rb) -``` -$ ruby -v code/array/count-vs-size.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] +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`](https://github.com/ruby/ruby/blob/f8fb526ad9e9f31453bffbc908b6a986736e21a7/array.c#L5817-L5818). +``` +$ ruby -v code/array/length-vs-size-vs-count.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Array#length 5.024M i/100ms + Array#size 4.994M i/100ms + Array#count 3.781M i/100ms Calculating ------------------------------------- - #count 130.991k i/100ms - #size 135.312k i/100ms -------------------------------------------------- - #count 6.697M (± 7.1%) i/s - 33.403M - #size 7.562M (± 9.1%) i/s - 37.481M + Array#length 50.668M (± 0.8%) i/s (19.74 ns/i) - 256.237M in 5.057495s + Array#size 50.580M (± 0.7%) i/s (19.77 ns/i) - 254.704M in 5.035880s + Array#count 38.202M (± 0.8%) i/s (26.18 ns/i) - 192.843M in 5.048306s Comparison: - #size: 7562457.4 i/s - #count: 6696763.0 i/s - 1.13x slower + Array#length: 50667869.8 i/s + Array#size: 50580088.8 i/s - same-ish: difference falls within error + Array#count: 38201768.5 i/s - 1.33x slower ``` ##### `Array#shuffle.first` vs `Array#sample` [code](code/array/shuffle-first-vs-sample.rb) @@ -204,56 +372,125 @@ Comparison: ``` $ ruby -v code/array/shuffle-first-vs-sample.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] - +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Array#shuffle.first 88.991k i/100ms + Array#sample 2.503M i/100ms 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 + Array#shuffle.first 891.780k (± 0.5%) i/s (1.12 μs/i) - 4.539M in 5.089459s + Array#sample 25.133M (± 0.6%) i/s (39.79 ns/i) - 127.660M in 5.079516s Comparison: - Array#sample: 5727032.0 i/s - Array#shuffle.first: 304341.1 i/s - 18.82x slower + Array#sample: 25133099.8 i/s + Array#shuffle.first: 891779.8 i/s - 28.18x slower ``` ##### `Array#[](0)` vs `Array#first` [code](code/array/array-first-vs-index.rb) ``` $ ruby -v code/array/array-first-vs-index.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] - +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Array#[0] 3.922M i/100ms + Array#first 3.697M i/100ms 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 + Array#[0] 39.347M (± 0.7%) i/s (25.41 ns/i) - 200.006M in 5.083307s + Array#first 37.289M (± 1.1%) i/s (26.82 ns/i) - 188.546M in 5.056917s Comparison: - Array#[0]: 8613583.7 i/s - Array#first: 7464526.6 i/s - 1.15x slower + Array#[0]: 39347487.6 i/s + Array#first: 37288935.3 i/s - 1.06x slower ``` ##### `Array#[](-1)` vs `Array#last` [code](code/array/array-last-vs-index.rb) ``` $ ruby -v code/array/array-last-vs-index.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Array#[-1] 3.906M i/100ms + Array#last 3.727M i/100ms +Calculating ------------------------------------- + Array#[-1] 39.303M (± 0.7%) i/s (25.44 ns/i) - 199.228M in 5.069210s + Array#last 37.280M (± 1.1%) i/s (26.82 ns/i) - 190.075M in 5.099254s + +Comparison: + Array#[-1]: 39303493.5 i/s + Array#last: 37279563.5 i/s - 1.05x slower +``` + +##### `Array#insert` vs `Array#unshift` [code](code/array/insert-vs-unshift.rb) + +``` +$ ruby -v code/array/insert-vs-unshift.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Array#unshift 37.000 i/100ms + Array#insert 1.000 i/100ms +Calculating ------------------------------------- + Array#unshift 379.798 (± 1.1%) i/s (2.63 ms/i) - 1.924k in 5.066300s + Array#insert 1.717 (± 0.0%) i/s (582.49 ms/i) - 9.000 in 5.242429s + +Comparison: + Array#unshift: 379.8 i/s + Array#insert: 1.7 i/s - 221.23x slower +``` +##### `Array#concat` vs `Array#+` [code](code/array/array-concat-vs-+.rb) +`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 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Array#concat 126.000 i/100ms + Array#+ 1.000 i/100ms +Calculating ------------------------------------- + Array#concat 1.259k (± 1.1%) i/s (793.98 μs/i) - 6.300k in 5.002769s + Array#+ 5.170 (± 0.0%) i/s (193.42 ms/i) - 26.000 in 5.033770s +Comparison: + Array#concat: 1259.5 i/s + Array#+: 5.2 i/s - 243.60x slower +``` + +##### `Array#new` vs `Fixnum#times + map` [code](code/array/array-new-vs-fixnum-times-map.rb) + +Typical slowdown is 40-60% depending on the size of the array. See the corresponding +[pull request](https://github.com/fastruby/fast-ruby/pull/91/) for performance characteristics. + +``` +$ ruby -v code/array/array-new-vs-fixnum-times-map.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Array#new 424.914k i/100ms + Fixnum#times + map 187.997k i/100ms 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 + Array#new 4.251M (± 0.4%) i/s (235.25 ns/i) - 21.671M in 5.098122s + Fixnum#times + map 1.886M (± 0.4%) i/s (530.31 ns/i) - 9.588M in 5.084632s Comparison: - Array#[-1]: 8582074.3 i/s - Array#last: 7639254.5 i/s - 1.12x slower + Array#new: 4250785.8 i/s + Fixnum#times + map: 1885679.9 i/s - 2.25x slower +``` + +##### `Array#sort.reverse` vs `Array#sort_by` + block [code](code/array/sort-reverse-vs-sort_by-with-block.rb) + ``` +$ ruby -v code/array/sort-reverse-vs-sort_by-with-block.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Array#sort.reverse 55.279k i/100ms + Array#sort_by &:-@ 22.773k i/100ms +Calculating ------------------------------------- + Array#sort.reverse 560.570k (± 0.9%) i/s (1.78 μs/i) - 2.819M in 5.029648s + Array#sort_by &:-@ 229.324k (± 0.6%) i/s (4.36 μs/i) - 1.161M in 5.064717s +Comparison: + Array#sort.reverse: 560570.1 i/s + Array#sort_by &:-@: 229323.6 i/s - 2.44x slower +``` ### Enumerable @@ -261,36 +498,34 @@ Comparison: ``` $ ruby -v code/enumerable/each-push-vs-map.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] - +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Array#each + push 31.310k i/100ms + Array#map 51.371k i/100ms 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 + Array#each + push 320.334k (± 0.6%) i/s (3.12 μs/i) - 1.628M in 5.082743s + Array#map 511.854k (± 1.3%) i/s (1.95 μs/i) - 2.569M in 5.019061s Comparison: - Array#map: 158090.9 i/s - Array#each + push: 99634.2 i/s - 1.59x slower + Array#map: 511854.5 i/s + Array#each + push: 320334.1 i/s - 1.60x slower ``` ##### `Enumerable#each` vs `for` loop [code](code/enumerable/each-vs-for-loop.rb) ``` $ ruby -v code/enumerable/each-vs-for-loop.rb -ruby 2.2.0preview1 (2014-09-17 trunk 47616) [x86_64-darwin14] - +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + For loop 54.498k i/100ms + #each 63.984k i/100ms 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 + For loop 525.548k (± 8.8%) i/s (1.90 μs/i) - 2.670M in 5.128917s + #each 641.789k (± 0.3%) i/s (1.56 μs/i) - 3.263M in 5.084548s Comparison: - #each: 208157.4 i/s - For loop: 198517.3 i/s - 1.05x slower + #each: 641788.6 i/s + For loop: 525547.8 i/s - 1.22x slower ``` ##### `Enumerable#each_with_index` vs `while` loop [code](code/enumerable/each_with_index-vs-while-loop.rb) @@ -298,19 +533,18 @@ Comparison: > [rails/rails#12065](https://github.com/rails/rails/pull/12065) ``` -$ ruby -v code/array/each_with_index-vs-while-loop.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] - +$ ruby -v code/enumerable/each_with_index-vs-while-loop.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + While Loop 49.050k i/100ms + each_with_index 36.684k i/100ms Calculating ------------------------------------- - each_with_index 11.496k i/100ms - While Loop 20.179k i/100ms -------------------------------------------------- - each_with_index 128.855k (± 7.5%) i/s - 643.776k - While Loop 242.344k (± 4.5%) i/s - 1.211M + While Loop 501.430k (± 2.9%) i/s (1.99 μs/i) - 2.551M in 5.091049s + each_with_index 369.364k (± 0.3%) i/s (2.71 μs/i) - 1.871M in 5.065187s Comparison: - While Loop: 242343.6 i/s - each_with_index: 128854.9 i/s - 1.88x slower + While Loop: 501430.2 i/s + each_with_index: 369364.5 i/s - 1.36x slower ``` ##### `Enumerable#map`...`Array#flatten` vs `Enumerable#flat_map` [code](code/enumerable/map-flatten-vs-flat_map.rb) @@ -318,22 +552,21 @@ Comparison: > -- @sferik [rails/rails@3413b88](https://github.com/rails/rails/commit/3413b88), [Replace map.flatten with flat_map](https://github.com/rails/rails/commit/817fe31196dd59ee31f71ef1740122b6759cf16d), [Replace map.flatten(1) with flat_map](https://github.com/rails/rails/commit/b11ebf1d80e4fb124f0ce0448cea30988256da59) ``` -ruby -v code/enumerable/map-flatten-vs-flat_map.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] - +$ ruby -v code/enumerable/map-flatten-vs-flat_map.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- +Array#map.flatten(1) 19.072k i/100ms + Array#map.flatten 8.810k i/100ms + Array#flat_map 21.362k i/100ms 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 +Array#map.flatten(1) 196.444k (± 1.2%) i/s (5.09 μs/i) - 991.744k in 5.049225s + Array#map.flatten 89.917k (± 1.2%) i/s (11.12 μs/i) - 458.120k in 5.095661s + Array#flat_map 215.149k (± 0.6%) i/s (4.65 μs/i) - 1.089M in 5.063916s 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 + Array#flat_map: 215149.2 i/s +Array#map.flatten(1): 196443.8 i/s - 1.10x slower + Array#map.flatten: 89916.9 i/s - 2.39x slower ``` ##### `Enumerable#reverse.each` vs `Enumerable#reverse_each` [code](code/enumerable/reverse-each-vs-reverse_each.rb) @@ -345,104 +578,211 @@ Array#map.flatten(1): 33800.6 i/s - 1.66x slower ``` $ ruby -v code/enumerable/reverse-each-vs-reverse_each.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Array#reverse.each 58.610k i/100ms + Array#reverse_each 63.669k i/100ms +Calculating ------------------------------------- + Array#reverse.each 586.309k (± 0.7%) i/s (1.71 μs/i) - 2.989M in 5.098469s + Array#reverse_each 635.904k (± 2.3%) i/s (1.57 μs/i) - 3.183M in 5.009371s + +Comparison: + Array#reverse_each: 635904.1 i/s + Array#reverse.each: 586309.0 i/s - 1.08x slower +``` +##### `Enumerable#sort_by.first` vs `Enumerable#min_by` [code](code/enumerable/sort_by-first-vs-min_by.rb) +`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 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Enumerable#min_by 40.784k i/100ms +Enumerable#sort_by...first + 32.912k i/100ms 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 + Enumerable#min_by 408.114k (± 0.3%) i/s (2.45 μs/i) - 2.080M in 5.096613s +Enumerable#sort_by...first + 334.401k (± 0.7%) i/s (2.99 μs/i) - 1.679M in 5.019698s Comparison: - Array#reverse_each: 216060.5 i/s - Array#reverse.each: 190729.1 i/s - 1.13x slower + Enumerable#min_by: 408113.9 i/s +Enumerable#sort_by...first: 334400.9 i/s - 1.22x slower ``` ##### `Enumerable#detect` vs `Enumerable#select.first` [code](code/enumerable/select-first-vs-detect.rb) ``` -$ ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] - +$ ruby -v code/enumerable/select-first-vs-detect.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- +Enumerable#select.first + 37.402k i/100ms + Enumerable#detect 247.638k i/100ms 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 +Enumerable#select.first + 373.115k (± 0.6%) i/s (2.68 μs/i) - 7.480M in 20.049160s + Enumerable#detect 2.462M (± 0.8%) i/s (406.12 ns/i) - 49.280M in 20.014730s Comparison: - Enumerable#detect: 434304.2 i/s -Enumerable#select.first: 89757.4 i/s - 4.84x slower + Enumerable#detect: 2462330.2 i/s +Enumerable#select.first: 373115.1 i/s - 6.60x slower ``` ##### `Enumerable#select.last` vs `Enumerable#reverse.detect` [code](code/enumerable/select-last-vs-reverse-detect.rb) ``` $ ruby -v code/enumerable/select-last-vs-reverse-detect.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] - +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- +Enumerable#reverse.detect + 23.133k i/100ms +Enumerable#select.last + 562.000 i/100ms 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 +Enumerable#reverse.detect + 241.463k (± 5.5%) i/s (4.14 μs/i) - 1.226M in 5.092374s +Enumerable#select.last + 5.640k (± 0.9%) i/s (177.30 μs/i) - 28.662k in 5.082075s Comparison: -Enumerable#reverse.detect: 1263100.2 i/s -Enumerable#select.last: 119386.8 i/s - 10.58x slower +Enumerable#reverse.detect: 241463.1 i/s +Enumerable#select.last: 5640.3 i/s - 42.81x slower ``` ##### `Enumerable#sort` vs `Enumerable#sort_by` [code](code/enumerable/sort-vs-sort_by.rb) ``` $ ruby -v code/enumerable/sort-vs-sort_by.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- +Enumerable#sort_by (Symbol#to_proc) + 16.077k i/100ms + Enumerable#sort_by 15.265k i/100ms + Enumerable#sort 4.580k i/100ms +Calculating ------------------------------------- +Enumerable#sort_by (Symbol#to_proc) + 162.709k (± 0.9%) i/s (6.15 μs/i) - 819.927k in 5.039602s + Enumerable#sort_by 152.611k (± 0.7%) i/s (6.55 μs/i) - 763.250k in 5.001512s + Enumerable#sort 45.814k (± 1.4%) i/s (21.83 μs/i) - 233.580k in 5.099352s + +Comparison: +Enumerable#sort_by (Symbol#to_proc): 162709.4 i/s + Enumerable#sort_by: 152611.3 i/s - 1.07x slower + Enumerable#sort: 45814.3 i/s - 3.55x slower +``` +##### `Enumerable#inject Symbol` vs `Enumerable#inject Proc` [code](code/enumerable/inject-symbol-vs-block.rb) + +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 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + inject symbol 183.846k i/100ms + inject to_proc 4.100k i/100ms + inject block 3.882k i/100ms Calculating ------------------------------------- - Enumerable#sort 1.158k i/100ms - Enumerable#sort_by 2.401k i/100ms -------------------------------------------------- - Enumerable#sort 12.140k (± 4.9%) i/s - 61.374k - Enumerable#sort_by 24.169k (± 4.0%) i/s - 122.451k + inject symbol 1.841M (± 0.4%) i/s (543.31 ns/i) - 9.376M in 5.094226s + inject to_proc 41.165k (± 0.3%) i/s (24.29 μs/i) - 209.100k in 5.079636s + inject block 39.550k (± 0.6%) i/s (25.28 μs/i) - 197.982k in 5.006073s Comparison: - Enumerable#sort_by: 24168.9 i/s - Enumerable#sort: 12139.8 i/s - 1.99x slower + inject symbol: 1840578.1 i/s + inject to_proc: 41164.7 i/s - 44.71x slower + inject block: 39550.0 i/s - 46.54x slower ``` +### Date + +##### `Date.iso8601` vs `Date.parse` [code](code/date/iso8601-vs-parse.rb) + +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 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Date.iso8601 195.684k i/100ms + Date.parse 62.633k i/100ms +Calculating ------------------------------------- + Date.iso8601 2.008M (± 0.8%) i/s (497.99 ns/i) - 10.176M in 5.067673s + Date.parse 627.349k (± 0.7%) i/s (1.59 μs/i) - 3.194M in 5.092000s + +Comparison: + Date.iso8601: 2008075.4 i/s + Date.parse: 627349.2 i/s - 3.20x slower +``` ### Hash -#### `Hash#[]` vs `Hash.fetch` [code](code/hash/bracket-vs-fetch.rb) +##### `Hash#[]` vs `Hash#fetch` [code](code/hash/bracket-vs-fetch.rb) 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](http://www.sitepoint.com/symbol-gc-ruby-2-2/) and [Unraveling String Key Performance in Ruby 2.2](http://www.sitepoint.com/unraveling-string-key-performance-ruby-2-2/). ``` $ ruby -v code/hash/bracket-vs-fetch.rb -ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14] +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Hash#[], symbol 3.471M i/100ms + Hash#fetch, symbol 2.869M i/100ms + Hash#[], string 1.969M i/100ms + Hash#fetch, string 1.746M i/100ms +Calculating ------------------------------------- + Hash#[], symbol 34.983M (± 0.8%) i/s (28.59 ns/i) - 177.026M in 5.060743s + Hash#fetch, symbol 28.927M (± 1.1%) i/s (34.57 ns/i) - 146.331M in 5.059120s + Hash#[], string 19.711M (± 1.7%) i/s (50.73 ns/i) - 100.431M in 5.096713s + Hash#fetch, string 17.825M (± 1.4%) i/s (56.10 ns/i) - 90.775M in 5.093555s +Comparison: + Hash#[], symbol: 34982573.5 i/s + Hash#fetch, symbol: 28927450.2 i/s - 1.21x slower + Hash#[], string: 19710678.7 i/s - 1.77x slower + Hash#fetch, string: 17825222.3 i/s - 1.96x slower +``` + +##### `Hash#dig` vs `Hash#[]` vs `Hash#fetch` [code](code/hash/dig-vs-[]-vs-fetch.rb) + +[Ruby 2.3 introduced `Hash#dig`](http://ruby-doc.org/core-2.3.0/Hash.html#method-i-dig) which is a readable +and performant option for retrieval from a nested hash, returning `nil` if an extraction step fails. +See [#102 (comment)](https://github.com/fastruby/fast-ruby/pull/102#issuecomment-198827506) for more info. + +``` +$ ruby -v code/hash/dig-vs-[]-vs-fetch.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Hash#dig 1.939M i/100ms + Hash#[] 1.903M i/100ms + Hash#[] || 1.787M i/100ms + Hash#[] && 725.944k i/100ms + Hash#fetch 1.246M i/100ms + Hash#fetch fallback 803.269k i/100ms 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 + Hash#dig 19.434M (± 0.8%) i/s (51.46 ns/i) - 98.877M in 5.088095s + Hash#[] 19.005M (± 2.2%) i/s (52.62 ns/i) - 95.141M in 5.008511s + Hash#[] || 17.879M (± 0.7%) i/s (55.93 ns/i) - 91.138M in 5.097648s + Hash#[] && 7.303M (± 0.6%) i/s (136.93 ns/i) - 37.023M in 5.069773s + Hash#fetch 12.634M (± 2.0%) i/s (79.15 ns/i) - 63.570M in 5.033708s + Hash#fetch fallback 8.117M (± 1.0%) i/s (123.20 ns/i) - 40.967M in 5.047539s 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: 19434078.6 i/s + Hash#[]: 19004999.7 i/s - same-ish: difference falls within error + Hash#[] ||: 17879337.5 i/s - 1.09x slower + Hash#fetch: 12633982.2 i/s - 1.54x slower + Hash#fetch fallback: 8116930.3 i/s - 2.39x slower + Hash#[] &&: 7302991.9 i/s - 2.66x slower ``` -##### `Hash#[]` vs `Hash#dup` [code](code/hash/bracket-vs-dup.rb) +##### `Hash[]` vs `Hash#dup` [code](code/hash/bracket-vs-dup.rb) Source: http://tenderlovemaking.com/2015/02/11/weird-stuff-with-hashes.html @@ -453,36 +793,42 @@ Source: http://tenderlovemaking.com/2015/02/11/weird-stuff-with-hashes.html ``` $ ruby -v code/hash/bracket-vs-dup.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] - +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Hash[] 684.060k i/100ms + Hash#dup 612.214k i/100ms 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 + Hash[] 7.555M (± 2.8%) i/s (132.37 ns/i) - 38.307M in 5.074888s + Hash#dup 5.851M (± 2.1%) i/s (170.90 ns/i) - 29.386M in 5.024310s Comparison: - Hash[]: 343986.5 i/s - Hash#dup: 163516.3 i/s - 2.10x slower + Hash[]: 7554758.4 i/s + Hash#dup: 5851408.7 i/s - 1.29x slower ``` ##### `Hash#fetch` with argument vs `Hash#fetch` + block [code](code/hash/fetch-vs-fetch-with-block.rb) +> 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)](https://github.com/fastruby/fast-ruby/issues/39#issuecomment-103989335) + ``` $ ruby -v code/hash/fetch-vs-fetch-with-block.rb -ruby 2.2.1p85 (2015-02-26 revision 49769) [x86_64-darwin14] - +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Hash#fetch + const 3.136M i/100ms + Hash#fetch + block 3.313M i/100ms + Hash#fetch + arg 2.266M i/100ms Calculating ------------------------------------- - Hash#fetch + block 139.880k i/100ms - Hash#fetch + arg 119.645k i/100ms -------------------------------------------------- - Hash#fetch + block 6.116M (± 8.9%) i/s - 30.354M - Hash#fetch + arg 4.473M (± 9.9%) i/s - 22.134M + Hash#fetch + const 32.198M (± 1.0%) i/s (31.06 ns/i) - 163.090M in 5.065722s + Hash#fetch + block 33.319M (± 1.1%) i/s (30.01 ns/i) - 168.953M in 5.071324s + Hash#fetch + arg 22.838M (± 1.3%) i/s (43.79 ns/i) - 115.561M in 5.060964s Comparison: - Hash#fetch + block: 6116059.5 i/s - Hash#fetch + arg: 4472636.0 i/s - 1.37x slower + Hash#fetch + block: 33319415.6 i/s + Hash#fetch + const: 32198246.2 i/s - 1.03x slower + Hash#fetch + arg: 22837560.4 i/s - 1.46x slower ``` ##### `Hash#each_key` instead of `Hash#keys.each` [code](code/hash/keys-each-vs-each_key.rb) @@ -494,54 +840,150 @@ Comparison: ``` $ ruby -v code/hash/keys-each-vs-each_key.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Hash#keys.each 494.495k i/100ms + Hash#each_key 510.269k i/100ms +Calculating ------------------------------------- + Hash#keys.each 5.026M (± 0.5%) i/s (198.95 ns/i) - 25.219M in 5.017574s + Hash#each_key 5.118M (± 1.2%) i/s (195.39 ns/i) - 26.024M in 5.085610s + +Comparison: + Hash#each_key: 5117890.6 i/s + Hash#keys.each: 5026302.4 i/s - 1.02x slower +``` + +#### `Hash#key?` instead of `Hash#keys.include?` [code](code/hash/keys-include-vs-key.rb) + +> `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 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Hash#keys.include? 2.217k i/100ms + Hash#key? 2.683M i/100ms +Calculating ------------------------------------- + Hash#keys.include? 26.005k (± 5.6%) i/s (38.45 μs/i) - 130.803k in 5.047937s + Hash#key? 26.686M (± 1.3%) i/s (37.47 ns/i) - 134.156M in 5.028106s + +Comparison: + Hash#key?: 26685586.3 i/s + Hash#keys.include?: 26005.2 i/s - 1026.16x slower +``` + +##### `Hash#value?` instead of `Hash#values.include?` [code](code/hash/values-include-vs-value.rb) + +> `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 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- +Hash#values.include? 5.620k i/100ms + Hash#value? 7.474k i/100ms 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 +Hash#values.include? 62.053k (± 2.8%) i/s (16.12 μs/i) - 314.720k in 5.076009s + Hash#value? 71.151k (± 4.4%) i/s (14.05 μs/i) - 358.752k in 5.051774s Comparison: - Hash#each_key: 1049161.6 i/s - Hash#keys.each: 869262.3 i/s - 1.21x slower + Hash#value?: 71150.8 i/s +Hash#values.include?: 62052.8 i/s - 1.15x slower ``` ##### `Hash#merge!` vs `Hash#[]=` [code](code/hash/merge-bang-vs-\[\]=.rb) ``` -$ ruby -v code/hash/merge-bang-vs-\[\]=.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] +$ ruby -v code/hash/merge-bang-vs-[]=.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Hash#merge! 8.974k i/100ms + Hash#[]= 20.052k i/100ms +Calculating ------------------------------------- + Hash#merge! 91.391k (± 0.5%) i/s (10.94 μs/i) - 457.674k in 5.008010s + Hash#[]= 202.686k (± 0.9%) i/s (4.93 μs/i) - 1.023M in 5.045877s + +Comparison: + Hash#[]=: 202686.0 i/s + Hash#merge!: 91390.8 i/s - 2.22x slower +``` + +##### `Hash#update` vs `Hash#[]=` [code](code/hash/update-vs-\[\]=.rb) + +``` +$ ruby -v code/hash/update-vs-[]=.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Hash#[]= 19.739k i/100ms + Hash#update 9.126k i/100ms +Calculating ------------------------------------- + Hash#[]= 202.714k (± 1.1%) i/s (4.93 μs/i) - 1.026M in 5.064063s + Hash#update 91.615k (± 0.7%) i/s (10.92 μs/i) - 465.426k in 5.080450s + +Comparison: + Hash#[]=: 202713.9 i/s + Hash#update: 91615.2 i/s - 2.21x slower +``` +##### `Hash#merge` vs `Hash#**other` [code](code/hash/merge-vs-double-splat-operator.rb) + +``` +$ ruby -v code/hash/merge-vs-double-splat-operator.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Hash#**other 979.615k i/100ms + Hash#merge 798.516k i/100ms 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 + Hash#**other 9.846M (± 0.3%) i/s (101.57 ns/i) - 49.960M in 5.074265s + Hash#merge 7.968M (± 0.3%) i/s (125.50 ns/i) - 39.926M in 5.010730s Comparison: - Hash#[]=: 28287.1 i/s - Hash#merge!: 10653.3 i/s - 2.66x slower + Hash#**other: 9845909.6 i/s + Hash#merge: 7968122.7 i/s - 1.24x slower ``` ##### `Hash#merge` vs `Hash#merge!` [code](code/hash/merge-vs-merge-bang.rb) ``` $ ruby -v code/hash/merge-vs-merge-bang.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Hash#merge 3.800k i/100ms + Hash#merge! 9.310k i/100ms +Calculating ------------------------------------- + Hash#merge 38.042k (± 1.9%) i/s (26.29 μs/i) - 193.800k in 5.096234s + Hash#merge! 94.284k (± 0.4%) i/s (10.61 μs/i) - 474.810k in 5.036016s +Comparison: + Hash#merge!: 94284.1 i/s + Hash#merge: 38041.7 i/s - 2.48x slower +``` + +##### `{}#merge!(Hash)` vs `Hash#merge({})` vs `Hash#dup#merge!({})` [code](code/hash/merge-bang-vs-merge-vs-dup-merge-bang.rb) + +> When we don't want to modify the original hash, and we want duplicates to be created
+> See [#42](https://github.com/fastruby/fast-ruby/pull/42#issue-93502261) for more details. + +``` +$ ruby -v code/hash/merge-bang-vs-merge-vs-dup-merge-bang.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- +{}#merge!(Hash) do end + 11.079k i/100ms + Hash#merge({}) 9.290k i/100ms + Hash#dup#merge!({}) 6.957k i/100ms 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 +{}#merge!(Hash) do end + 111.999k (± 0.3%) i/s (8.93 μs/i) - 565.029k in 5.045023s + Hash#merge({}) 93.149k (± 0.4%) i/s (10.74 μs/i) - 473.790k in 5.086441s + Hash#dup#merge!({}) 69.802k (± 0.4%) i/s (14.33 μs/i) - 354.807k in 5.083113s Comparison: - Hash#merge!: 9830.3 i/s - Hash#merge: 409.6 i/s - 24.00x slower +{}#merge!(Hash) do end: 111998.6 i/s + Hash#merge({}): 93149.0 i/s - 1.20x slower + Hash#dup#merge!({}): 69802.3 i/s - 1.60x slower ``` ##### `Hash#sort_by` vs `Hash#sort` [code](code/hash/hash-key-sort_by-vs-sort.rb) @@ -550,20 +992,45 @@ 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] +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + sort_by + to_h 72.056k i/100ms + sort + to_h 26.595k i/100ms +Calculating ------------------------------------- + sort_by + to_h 739.508k (± 0.7%) i/s (1.35 μs/i) - 3.747M in 5.067024s + sort + to_h 269.700k (± 1.9%) i/s (3.71 μs/i) - 1.356M in 5.030958s + +Comparison: + sort_by + to_h: 739507.8 i/s + sort + to_h: 269700.3 i/s - 2.74x slower +``` + +##### Native `Hash#slice` vs other slice implementations before native [code](code/hash/slice-native-vs-before-native.rb) +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 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- +Hash#native-slice 1.024M i/100ms +Array#each 467.292k i/100ms +Array#each_w/_object 388.528k i/100ms +Hash#select-include 131.358k i/100ms 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 +Hash#native-slice 10.550M (± 1.4%) i/s (94.78 ns/i) - 53.251M in 5.048257s +Array#each 4.688M (± 0.5%) i/s (213.30 ns/i) - 23.832M in 5.083384s +Array#each_w/_object 3.890M (± 0.4%) i/s (257.07 ns/i) - 19.815M in 5.093848s +Hash#select-include 1.343M (± 1.7%) i/s (744.63 ns/i) - 6.831M in 5.087848s Comparison: - sort_by + to_h: 122176.2 i/s - sort + to_h: 81972.8 i/s - 1.49x slower +Hash#native-slice : 10550464.1 i/s +Array#each : 4688305.3 i/s - 2.25x slower +Array#each_w/_object: 3890033.7 i/s - 2.71x slower +Hash#select-include : 1342942.2 i/s - 7.86x slower ``` + ### Proc & Block ##### Block vs `Symbol#to_proc` [code](code/proc-and-block/block-vs-to_proc.rb) @@ -574,142 +1041,247 @@ Comparison: ``` $ ruby -v code/proc-and-block/block-vs-to_proc.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] - +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Block 20.147k i/100ms + Symbol#to_proc 22.231k i/100ms 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 + Block 202.937k (± 0.4%) i/s (4.93 μs/i) - 1.027M in 5.063220s + Symbol#to_proc 222.450k (± 0.9%) i/s (4.50 μs/i) - 1.134M in 5.097179s Comparison: - Symbol#to_proc: 54791.1 i/s - Block: 47914.3 i/s - 1.14x slower + Symbol#to_proc: 222449.5 i/s + Block: 202937.5 i/s - 1.10x slower ``` -##### `Proc#call` vs `yield` [code](code/proc-and-block/proc-call-vs-yield.rb) +##### `Proc#call` and block arguments vs `yield` [code](code/proc-and-block/proc-call-vs-yield.rb) ``` $ ruby -v code/proc-and-block/proc-call-vs-yield.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] - +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + block.call 2.261M i/100ms + block + yield 2.314M i/100ms + unused block 3.025M i/100ms + yield 2.971M i/100ms Calculating ------------------------------------- - block.call 70.663k i/100ms - yield 125.061k i/100ms -------------------------------------------------- - block.call 1.309M (± 5.7%) i/s - 6.572M - yield 6.103M (± 7.7%) i/s - 30.390M + block.call 22.057M (± 6.0%) i/s (45.34 ns/i) - 110.796M in 5.043129s + block + yield 23.280M (± 0.6%) i/s (42.96 ns/i) - 117.997M in 5.068779s + unused block 30.609M (± 1.3%) i/s (32.67 ns/i) - 154.268M in 5.040991s + yield 29.921M (± 0.6%) i/s (33.42 ns/i) - 151.512M in 5.063842s Comparison: - yield: 6102822.9 i/s - block.call: 1309452.1 i/s - 4.66x slower + unused block: 30608512.5 i/s + yield: 29921356.8 i/s - 1.02x slower + block + yield: 23279981.0 i/s - 1.31x slower + block.call: 22056758.6 i/s - 1.39x slower ``` - ### String -##### `String#casecmp` vs `String#downcase + ==` [code](code/string/casecmp-vs-downcase-==.rb) +##### `String#dup` vs `String#+` [code](code/string/dup-vs-unary-plus.rb) +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 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + String#+@ 2.439M i/100ms + String#dup 2.481M i/100ms +Calculating ------------------------------------- + String#+@ 24.328M (± 0.5%) i/s (41.10 ns/i) - 121.962M in 5.013305s + String#dup 24.553M (± 1.0%) i/s (40.73 ns/i) - 124.040M in 5.052462s + +Comparison: + String#dup: 24552887.2 i/s + String#+@: 24328187.6 i/s - same-ish: difference falls within error ``` -$ ruby -v code/string/casecmp-vs-downcase-\=\=.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] +##### `String#casecmp` vs `String#casecmp?` vs `String#downcase + ==` [code](code/string/casecmp-vs-downcase-==.rb) + +`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 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + String#casecmp? 1.053M i/100ms +String#downcase + == 1.467M i/100ms + String#casecmp 1.816M i/100ms Calculating ------------------------------------- -String#downcase + == 101.900k i/100ms - String#casecmp 109.828k i/100ms -------------------------------------------------- -String#downcase + == 2.915M (± 5.4%) i/s - 14.572M - String#casecmp 3.708M (± 6.1%) i/s - 18.561M + String#casecmp? 10.916M (± 1.4%) i/s (91.61 ns/i) - 54.769M in 5.018429s +String#downcase + == 14.673M (± 1.0%) i/s (68.15 ns/i) - 74.808M in 5.098814s + String#casecmp 18.210M (± 0.7%) i/s (54.91 ns/i) - 92.594M in 5.084879s Comparison: - String#casecmp: 3708258.7 i/s -String#downcase + ==: 2914767.7 i/s - 1.27x slower + String#casecmp: 18210413.9 i/s +String#downcase + ==: 14673089.8 i/s - 1.24x slower + String#casecmp?: 10915954.0 i/s - 1.67x slower ``` ##### String Concatenation [code](code/string/concatenation.rb) ``` -$ ruby code/string/concatenation.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] - +$ ruby -v code/string/concatenation.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + String#+ 1.214M i/100ms + String#concat 1.407M i/100ms + String#append 1.561M i/100ms + "foo" "bar" 2.723M i/100ms + "#{'foo'}#{'bar'}" 2.914M i/100ms Calculating ------------------------------------- - String#+ 96.314k i/100ms - String#concat 99.850k i/100ms - String#append 100.728k i/100ms - "foo" "bar" 121.936k i/100ms -------------------------------------------------- - String#+ 2.731M (± 4.6%) i/s - 13.677M - String#concat 2.847M (± 5.2%) i/s - 14.279M - String#append 2.972M (± 6.1%) i/s - 14.807M - "foo" "bar" 4.951M (± 6.2%) i/s - 24.753M + String#+ 12.616M (± 0.9%) i/s (79.27 ns/i) - 63.138M in 5.005092s + String#concat 14.555M (± 1.1%) i/s (68.71 ns/i) - 73.188M in 5.029155s + String#append 15.844M (± 0.7%) i/s (63.12 ns/i) - 79.634M in 5.026390s + "foo" "bar" 27.318M (± 0.8%) i/s (36.61 ns/i) - 138.888M in 5.084517s + "#{'foo'}#{'bar'}" 29.063M (± 0.5%) i/s (34.41 ns/i) - 145.707M in 5.013531s Comparison: - "foo" "bar": 4950955.3 i/s - String#append: 2972048.5 i/s - 1.67x slower - String#concat: 2846666.4 i/s - 1.74x slower - String#+: 2730980.7 i/s - 1.81x slower + "#{'foo'}#{'bar'}": 29063458.4 i/s + "foo" "bar": 27317882.5 i/s - 1.06x slower + String#append: 15843896.8 i/s - 1.83x slower + String#concat: 14554534.2 i/s - 2.00x slower + String#+: 12615859.7 i/s - 2.30x slower ``` -##### `String#match` vs `String#start_with?`/`String#end_with?` [code (start)](code/string/start-string-checking-match-vs-start_with.rb) [code (end)](code/string/end-string-checking-match-vs-start_with.rb) +##### `String#match` vs `String.match?` vs `String#start_with?`/`String#end_with?` [code (start)](code/string/start-string-checking-match-vs-start_with.rb) [code (end)](code/string/end-string-checking-match-vs-end_with.rb) + +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?`. > :warning:
> Sometimes you cant replace regexp with `start_with?`,
> for example: `"a\nb" =~ /^b/ #=> 2` but `"a\nb" =~ /\Ab/ #=> nil`.
> :warning:
-> 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](https://github.com/rails/rails/pull/17316) ``` $ ruby -v code/string/start-string-checking-match-vs-start_with.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + String#=~ 704.323k i/100ms + String#match? 1.696M i/100ms + String#start_with? 2.002M i/100ms +Calculating ------------------------------------- + String#=~ 7.155M (± 0.9%) i/s (139.76 ns/i) - 35.920M in 5.020673s + String#match? 17.088M (± 0.7%) i/s (58.52 ns/i) - 86.478M in 5.061156s + String#start_with? 20.186M (± 1.0%) i/s (49.54 ns/i) - 102.087M in 5.057969s +Comparison: + String#start_with?: 20185554.4 i/s + String#match?: 17087554.1 i/s - 1.18x slower + String#=~: 7155069.8 i/s - 2.82x slower +``` + +``` +$ ruby -v code/string/end-string-checking-match-vs-end_with.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + String#=~ 459.289k i/100ms + String#match? 808.294k i/100ms + String#end_with? 1.358M i/100ms Calculating ------------------------------------- - String#=~ 55.411k i/100ms - String#start_with? 113.854k i/100ms -------------------------------------------------- - String#=~ 910.625k (± 4.6%) i/s - 4.544M - String#start_with? 3.983M (± 5.5%) i/s - 19.924M + String#=~ 4.900M (± 0.7%) i/s (204.10 ns/i) - 24.802M in 5.062282s + String#match? 7.999M (± 3.2%) i/s (125.02 ns/i) - 40.415M in 5.058576s + String#end_with? 13.797M (± 0.9%) i/s (72.48 ns/i) - 69.248M in 5.019420s Comparison: - String#start_with?: 3983284.9 i/s - String#=~: 910625.0 i/s - 4.37x slower + String#end_with?: 13797085.6 i/s + String#match?: 7998569.4 i/s - 1.72x slower + String#=~: 4899556.6 i/s - 2.82x slower ``` +##### `String#start_with?` vs `String#[].==` [code](code/string/start_with-vs-substring-==.rb) + +``` +$ ruby -v code/string/start_with-vs-substring-==.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + String#start_with? 405.198k i/100ms + String#[0, n] == 185.548k i/100ms + String#[RANGE] == 183.537k i/100ms + String#[0...n] == 111.844k i/100ms +Calculating ------------------------------------- + String#start_with? 4.336M (± 1.7%) i/s (230.64 ns/i) - 21.881M in 5.048171s + String#[0, n] == 1.911M (± 1.8%) i/s (523.18 ns/i) - 9.648M in 5.049643s + String#[RANGE] == 1.835M (± 1.4%) i/s (544.95 ns/i) - 9.177M in 5.001963s + String#[0...n] == 1.100M (± 1.5%) i/s (909.34 ns/i) - 5.592M in 5.086419s + +Comparison: + String#start_with?: 4335677.0 i/s + String#[0, n] ==: 1911392.8 i/s - 2.27x slower + String#[RANGE] ==: 1835034.3 i/s - 2.36x slower + String#[0...n] ==: 1099697.5 i/s - 3.94x slower ``` -$ ruby -v code/string/end-string-checking-match-vs-start_with.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] +##### `Regexp#===` vs `Regexp#match` vs `Regexp#match?` vs `String#match` vs `String#=~` vs `String#match?` [code ](code/string/===-vs-=~-vs-match.rb) + +`String#match?` and `Regexp#match?` are available on Ruby 2.4 or later. +ActiveSupport [provides](http://guides.rubyonrails.org/v5.1/active_support_core_extensions.html#match-questionmark) +a forward compatible extension of `Regexp` for older Rubies without the speed +improvement. + +> :warning:
+> 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.
+> :warning:
+> `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 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Regexp#match? 2.240M i/100ms + String#match? 2.271M i/100ms + String#=~ 1.322M i/100ms + Regexp#=== 1.252M i/100ms + Regexp#match 1.187M i/100ms + String#match 1.024M i/100ms Calculating ------------------------------------- - String#=~ 52.811k i/100ms - String#end_with? 100.071k i/100ms -------------------------------------------------- - String#=~ 854.830k (± 5.8%) i/s - 4.278M - String#end_with? 2.837M (± 5.5%) i/s - 14.210M + Regexp#match? 22.395M (± 1.3%) i/s (44.65 ns/i) - 111.995M in 5.001594s + String#match? 22.544M (± 1.4%) i/s (44.36 ns/i) - 113.533M in 5.037001s + String#=~ 13.285M (± 2.6%) i/s (75.27 ns/i) - 67.438M in 5.079611s + Regexp#=== 12.472M (± 0.6%) i/s (80.18 ns/i) - 62.618M in 5.020860s + Regexp#match 11.865M (± 0.8%) i/s (84.28 ns/i) - 59.340M in 5.001611s + String#match 10.223M (± 0.7%) i/s (97.81 ns/i) - 51.194M in 5.007796s Comparison: - String#end_with?: 2836536.9 i/s - String#=~: 854830.3 i/s - 3.32x slower + String#match?: 22544340.1 i/s + Regexp#match?: 22395457.7 i/s - same-ish: difference falls within error + String#=~: 13285297.2 i/s - 1.70x slower + Regexp#===: 12471978.3 i/s - 1.81x slower + Regexp#match: 11864949.7 i/s - 1.90x slower + String#match: 10223419.6 i/s - 2.21x slower ``` -##### `String#gsub` vs `String#sub` [code](code/string/gsub-vs-sub.rb) +See [#59](https://github.com/fastruby/fast-ruby/pull/59) and [#62](https://github.com/fastruby/fast-ruby/pull/62) for discussions. + + +##### `String#gsub` vs `String#sub` vs `String#[]=` [code](code/string/gsub-vs-sub.rb) ``` $ ruby -v code/string/gsub-vs-sub.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] - +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + String#gsub 230.980k i/100ms + String#sub 465.575k i/100ms +String#dup["string"]= + 838.284k i/100ms Calculating ------------------------------------- - String#gsub 35.724k i/100ms - String#sub 42.426k i/100ms -------------------------------------------------- - String#gsub 486.614k (± 5.4%) i/s - 2.429M - String#sub 611.259k (± 4.6%) i/s - 3.055M + String#gsub 2.338M (± 0.9%) i/s (427.64 ns/i) - 11.780M in 5.038072s + String#sub 4.691M (± 1.2%) i/s (213.16 ns/i) - 23.744M in 5.062169s +String#dup["string"]= + 8.433M (± 1.0%) i/s (118.58 ns/i) - 42.752M in 5.070311s Comparison: - String#sub: 611259.4 i/s - String#gsub: 486613.5 i/s - 1.26x slower +String#dup["string"]=: 8432793.1 i/s + String#sub: 4691246.0 i/s - 1.80x slower + String#gsub: 2338396.7 i/s - 3.61x slower ``` ##### `String#gsub` vs `String#tr` [code](code/string/gsub-vs-tr.rb) @@ -718,110 +1290,241 @@ Comparison: ``` $ ruby -v code/string/gsub-vs-tr.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + String#gsub 297.314k i/100ms + String#tr 914.116k i/100ms +Calculating ------------------------------------- + String#gsub 3.032M (± 0.8%) i/s (329.83 ns/i) - 15.163M in 5.001554s + String#tr 9.136M (± 0.8%) i/s (109.46 ns/i) - 45.706M in 5.003201s + +Comparison: + String#tr: 9135960.5 i/s + String#gsub: 3031853.2 i/s - 3.01x slower +``` +##### `String#gsub` vs `String#tr` vs `String#delete` [code](code/string/gsub-vs-tr-vs-delete.rb) + +``` +$ ruby -v code/string/gsub-vs-tr-vs-delete.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + String#gsub 314.117k i/100ms + String#tr 989.154k i/100ms + String#delete 1.159M i/100ms + String#delete const 1.320M i/100ms +Calculating ------------------------------------- + String#gsub 3.099M (± 1.1%) i/s (322.65 ns/i) - 15.706M in 5.068096s + String#tr 9.868M (± 1.3%) i/s (101.34 ns/i) - 49.458M in 5.012992s + String#delete 11.623M (± 0.6%) i/s (86.03 ns/i) - 59.109M in 5.085570s + String#delete const 13.192M (± 0.8%) i/s (75.80 ns/i) - 65.989M in 5.002525s + +Comparison: + String#delete const: 13191917.5 i/s + String#delete: 11623217.0 i/s - 1.13x slower + String#tr: 9867528.0 i/s - 1.34x slower + String#gsub: 3099363.7 i/s - 4.26x slower +``` + +##### `Mutable` vs `Immutable` [code](code/string/mutable_vs_immutable_strings.rb) + +``` +$ ruby -v code/string/mutable_vs_immutable_strings.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Without Freeze 2.671M i/100ms + With Freeze 4.464M i/100ms 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 + Without Freeze 27.091M (± 1.6%) i/s (36.91 ns/i) - 136.216M in 5.029377s + With Freeze 44.763M (± 0.9%) i/s (22.34 ns/i) - 227.646M in 5.085943s Comparison: - String#tr: 1861860.4 i/s - String#gsub: 516604.2 i/s - 3.60x slower + With Freeze: 44763124.8 i/s + Without Freeze: 27091288.1 i/s - 1.65x slower ``` + ##### `String#sub!` vs `String#gsub!` vs `String#[]=` [code](code/string/sub!-vs-gsub!-vs-[]=.rb) 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] +$ ruby -v code/string/sub!-vs-gsub!-vs-[]=.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + String#['string']= 867.608k i/100ms + String#sub!'string' 452.774k i/100ms +String#gsub!'string' 219.002k i/100ms + String#[/regexp/]= 484.258k i/100ms + String#sub!/regexp/ 439.965k i/100ms +String#gsub!/regexp/ 222.202k i/100ms +Calculating ------------------------------------- + String#['string']= 8.875M (± 0.9%) i/s (112.67 ns/i) - 45.116M in 5.083636s + String#sub!'string' 4.648M (± 0.6%) i/s (215.15 ns/i) - 23.544M in 5.065834s +String#gsub!'string' 2.234M (± 0.6%) i/s (447.72 ns/i) - 11.169M in 5.000776s + String#[/regexp/]= 4.913M (± 0.6%) i/s (203.54 ns/i) - 24.697M in 5.026953s + String#sub!/regexp/ 4.474M (± 0.4%) i/s (223.50 ns/i) - 22.438M in 5.014974s +String#gsub!/regexp/ 2.221M (± 0.7%) i/s (450.25 ns/i) - 11.110M in 5.002620s + +Comparison: + String#['string']=: 8875438.0 i/s + String#[/regexp/]=: 4913121.6 i/s - 1.81x slower + String#sub!'string': 4647843.8 i/s - 1.91x slower + String#sub!/regexp/: 4474302.7 i/s - 1.98x slower +String#gsub!'string': 2233558.9 i/s - 3.97x slower +String#gsub!/regexp/: 2220978.6 i/s - 4.00x slower +``` + +##### `String#sub` vs `String#delete_prefix` [code](code/string/sub-vs-delete_prefix.rb) + +[Ruby 2.5 introduced](https://bugs.ruby-lang.org/issues/12694) `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 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- +String#delete_prefix 1.446M i/100ms + String#sub 473.108k i/100ms 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 +String#delete_prefix 14.867M (± 1.3%) i/s (67.26 ns/i) - 75.200M in 5.059147s + String#sub 4.726M (± 0.7%) i/s (211.58 ns/i) - 23.655M in 5.005369s 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#delete_prefix: 14866723.8 i/s + String#sub: 4726255.3 i/s - 3.15x slower ``` -##### `attr_accessor` vs `getter and setter` [code](code/general/attr-accessor-vs-getter-and-setter.rb) +##### `String#sub` vs `String#chomp` vs `String#delete_suffix` [code](code/string/sub-vs-chomp-vs-delete_suffix.rb) -> https://www.omniref.com/ruby/2.2.0/files/method.h?#annotation=4081781&line=47 +[Ruby 2.5 introduced](https://bugs.ruby-lang.org/issues/13665) `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/general/attr-accessor-vs-getter-and-setter.rb -ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] +$ ruby -v code/string/sub-vs-chomp-vs-delete_suffix.rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + String#sub 472.278k i/100ms + String#chomp 1.354M i/100ms +String#delete_suffix 1.477M i/100ms +Calculating ------------------------------------- + String#sub 4.809M (± 1.0%) i/s (207.93 ns/i) - 24.086M in 5.008842s + String#chomp 13.829M (± 1.2%) i/s (72.31 ns/i) - 70.425M in 5.093208s +String#delete_suffix 14.936M (± 0.9%) i/s (66.95 ns/i) - 75.317M in 5.042961s + +Comparison: +String#delete_suffix: 14936380.8 i/s + String#chomp: 13829135.7 i/s - 1.08x slower + String#sub: 4809249.5 i/s - 3.11x slower +``` + +##### `String#unpack1` vs `String#unpack[0]` [code](code/string/unpack1-vs-unpack[0].rb) + +[Ruby 2.4.0 introduced `unpack1`](https://bugs.ruby-lang.org/issues/12752) to skip creating the intermediate array object. + +``` +$ ruby -v code/string/unpack1-vs-unpack[0].rb +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + String#unpack1 1.372M i/100ms + String#unpack[0] 1.095M i/100ms 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 + String#unpack1 14.219M (± 0.7%) i/s (70.33 ns/i) - 71.349M in 5.018051s + String#unpack[0] 11.071M (± 1.2%) i/s (90.33 ns/i) - 55.839M in 5.044380s Comparison: - attr_accessor: 1865408.4 i/s - getter_and_setter: 1660021.9 i/s - 1.12x slower + String#unpack1: 14219190.8 i/s + String#unpack[0]: 11071030.6 i/s - 1.28x slower ``` +##### Remove extra spaces (or other contiguous characters) [code](code/string/remove-extra-spaces-or-other-chars.rb) + +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 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + String#gsub/regex+/ 15.087k i/100ms + String#squeeze 429.561k i/100ms +Calculating ------------------------------------- + String#gsub/regex+/ 152.509k (± 0.6%) i/s (6.56 μs/i) - 769.437k in 5.045412s + String#squeeze 4.264M (± 1.7%) i/s (234.51 ns/i) - 21.478M in 5.038218s + +Comparison: + String#squeeze: 4264261.4 i/s + String#gsub/regex+/: 152508.7 i/s - 27.96x slower +``` + +### Time + +##### `Time.iso8601` vs `Time.parse` [code](code/time/iso8601-vs-parse.rb) + +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 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + Time.iso8601 105.846k i/100ms + Time.parse 19.826k i/100ms +Calculating ------------------------------------- + Time.iso8601 1.076M (± 0.5%) i/s (929.16 ns/i) - 5.398M in 5.015850s + Time.parse 200.370k (± 0.8%) i/s (4.99 μs/i) - 1.011M in 5.046598s + +Comparison: + Time.iso8601: 1076241.3 i/s + Time.parse: 200370.2 i/s - 5.37x slower +``` ### Range -#### `cover?` vs `include?` [code](code/range/cover-vs-include.rb) +##### `cover?` vs `include?` [code](code/range/cover-vs-include.rb) `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.2p95 (2015-04-13 revision 50295) [x86_64-darwin14] - +ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +Warming up -------------------------------------- + range#cover? 811.176k i/100ms + range#include? 34.101k i/100ms + range#member? 35.591k i/100ms + plain compare 1.131M i/100ms + value.between? 1.520M i/100ms Calculating ------------------------------------- - Range#cover? 95.445k i/100ms - Range#include? 9.326k i/100ms -------------------------------------------------- - Range#cover? 2.327M (± 4.7%) i/s - 11.644M - Range#include? 99.652k (± 5.4%) i/s - 503.604k + range#cover? 8.324M (± 0.3%) i/s (120.14 ns/i) - 42.181M in 5.067623s + range#include? 352.846k (± 1.7%) i/s (2.83 μs/i) - 1.773M in 5.027064s + range#member? 349.526k (± 1.9%) i/s (2.86 μs/i) - 1.780M in 5.093144s + plain compare 11.296M (± 1.3%) i/s (88.53 ns/i) - 56.554M in 5.007498s + value.between? 15.214M (± 0.9%) i/s (65.73 ns/i) - 77.527M in 5.096183s Comparison: - Range#cover?: 2327220.4 i/s - Range#include?: 99651.6 i/s - 23.35x slower + value.between?: 15213944.1 i/s + plain compare: 11295903.4 i/s - 1.35x slower + range#cover?: 8323723.8 i/s - 1.83x slower + range#include?: 352846.2 i/s - 43.12x slower + range#member?: 349526.0 i/s - 43.53x slower ``` ## Less idiomatic but with significant performance ruby -Checkout: https://github.com/JuanitoFatas/fast-ruby/wiki/Less-idiomatic-but-with-significant-performance-diffrence +Checkout: https://github.com/fastruby/fast-ruby/wiki/Less-idiomatic-but-with-significant-performance-difference ## Submit New Entry -Please! [Edit this README.md](https://github.com/JuanitoFatas/fast-ruby/edit/master/README.md) then [Submit a Awesome Pull Request](https://github.com/JuanitoFatas/fast-ruby/pulls)! +Please! [Edit this README.md](https://github.com/fastruby/fast-ruby/edit/main/README.md) then [Submit a Awesome Pull Request](https://github.com/fastruby/fast-ruby/pulls)! ## Something went wrong Code example is wrong? :cry: Got better example? :heart_eyes: Excellent! -[Please open an issue](https://github.com/JuanitoFatas/fast-ruby/issues/new) or [Open a Pull Request](https://github.com/JuanitoFatas/fast-ruby/pulls) to fix it. +[Please open an issue](https://github.com/fastruby/fast-ruby/issues/new) or [Open a Pull Request](https://github.com/fastruby/fast-ruby/pulls) to fix it. Thank you in advance! :wink: :beer: @@ -837,6 +1540,10 @@ Feel free to talk with me on Twitter! <3 ## Also Checkout +- [Derailed Benchmarks](https://github.com/schneems/derailed_benchmarks) + + Go faster, off the Rails - Benchmarks for your whole Rails app + - [Benchmarking Ruby](https://speakerdeck.com/davystevenson/benchmarking-ruby) Talk by Davy Stevenson @ RubyConf 2014. @@ -845,7 +1552,7 @@ Feel free to talk with me on Twitter! <3 Provides Big O notation benchmarking for Ruby. -- [The Ruby Challenge](https://therubychallenge.com/) +- [The Ruby Challenge](https://www.youtube.com/watch?v=aDeP7FGQBig) Talk by Prem Sichanugrist @ Ruby Kaigi 2014. diff --git a/code/array/array-concat-vs-+.rb b/code/array/array-concat-vs-+.rb new file mode 100644 index 00000000..acfc6539 --- /dev/null +++ b/code/array/array-concat-vs-+.rb @@ -0,0 +1,19 @@ +require 'benchmark/ips' + +RANGE = (0..10_000).freeze + +def fast + array = [] + RANGE.each { |number| array.concat(Array.new(10, number)) } +end + +def slow + array = [] + RANGE.each { |number| array += Array.new(10, number) } +end + +Benchmark.ips do |x| + x.report('Array#concat') { fast } + x.report('Array#+') { slow } + x.compare! +end diff --git a/code/array/array-new-vs-fixnum-times-map.rb b/code/array/array-new-vs-fixnum-times-map.rb new file mode 100644 index 00000000..fcd72605 --- /dev/null +++ b/code/array/array-new-vs-fixnum-times-map.rb @@ -0,0 +1,17 @@ +require "benchmark/ips" + +ELEMENTS = 9 + +def fast + Array.new(ELEMENTS) { |i| i } +end + +def slow + ELEMENTS.times.map { |i| i } +end + +Benchmark.ips do |x| + x.report("Array#new") { fast } + x.report("Fixnum#times + map") { slow } + x.compare! +end diff --git a/code/array/count-vs-size.rb b/code/array/count-vs-size.rb deleted file mode 100644 index a14fa399..00000000 --- a/code/array/count-vs-size.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'benchmark/ips' - -ARRAY = [*1..100] - -def slow - ARRAY.count -end - -def fast - ARRAY.size -end - -Benchmark.ips do |x| - x.report('#count') { slow } - x.report('#size') { fast } - x.compare! -end diff --git a/code/array/insert-vs-unshift.rb b/code/array/insert-vs-unshift.rb new file mode 100644 index 00000000..992f24f6 --- /dev/null +++ b/code/array/insert-vs-unshift.rb @@ -0,0 +1,15 @@ +require 'benchmark/ips' + +Benchmark.ips do |x| + x.report('Array#unshift') do + array = [] + 100_000.times { |i| array.unshift(i) } + end + + x.report('Array#insert') do + array = [] + 100_000.times { |i| array.insert(0, i) } + end + + x.compare! +end diff --git a/code/array/length-vs-size-vs-count.rb b/code/array/length-vs-size-vs-count.rb new file mode 100644 index 00000000..6b7ff243 --- /dev/null +++ b/code/array/length-vs-size-vs-count.rb @@ -0,0 +1,10 @@ +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 diff --git a/code/array/sort-reverse-vs-sort_by-with-block.rb b/code/array/sort-reverse-vs-sort_by-with-block.rb new file mode 100644 index 00000000..7cafc50f --- /dev/null +++ b/code/array/sort-reverse-vs-sort_by-with-block.rb @@ -0,0 +1,18 @@ +require "benchmark/ips" + +ARRAY = 100.times.map { rand(1_000_000_000) } + +def fast + ARRAY.sort.reverse +end + +def slow + ARRAY.sort_by(&:-@) +end + +Benchmark.ips do |x| + x.report('Array#sort.reverse') { fast } + x.report('Array#sort_by &:-@') { slow } + + x.compare! +end diff --git a/code/date/iso8601-vs-parse.rb b/code/date/iso8601-vs-parse.rb new file mode 100644 index 00000000..6d0bce44 --- /dev/null +++ b/code/date/iso8601-vs-parse.rb @@ -0,0 +1,18 @@ +require 'benchmark/ips' +require 'date' + +STRING = '2018-03-21'.freeze + +def fast + Date.iso8601(STRING) +end + +def slow + Date.parse(STRING) +end + +Benchmark.ips do |x| + x.report('Date.iso8601') { fast } + x.report('Date.parse') { slow } + x.compare! +end diff --git a/code/enumerable/each_with_index-vs-while-loop.rb b/code/enumerable/each_with_index-vs-while-loop.rb index aff5a93d..4cf3f3e2 100644 --- a/code/enumerable/each_with_index-vs-while-loop.rb +++ b/code/enumerable/each_with_index-vs-while-loop.rb @@ -1,23 +1,24 @@ -require 'benchmark/ips' +require "benchmark/ips" ARRAY = [*1..100] -def slow - ARRAY.each_with_index do |number, index| - number + index - end -end - def fast index = 0 while index < ARRAY.size ARRAY[index] + index index += 1 end + ARRAY +end + +def slow + ARRAY.each_with_index do |number, index| + number + index + end end Benchmark.ips do |x| - x.report('each_with_index') { slow } - x.report('While Loop') { fast } + x.report("While Loop") { fast } + x.report("each_with_index") { slow } x.compare! end diff --git a/code/enumerable/inject-symbol-vs-block.rb b/code/enumerable/inject-symbol-vs-block.rb new file mode 100644 index 00000000..c261f718 --- /dev/null +++ b/code/enumerable/inject-symbol-vs-block.rb @@ -0,0 +1,24 @@ +require "rubygems" +require "benchmark/ips" + +ARRAY = (1..1000).to_a + +def fastest + ARRAY.inject(:+) +end + +def fast + ARRAY.inject(&:+) +end + +def slow + ARRAY.inject { |a, i| a + i } +end + +Benchmark.ips do |x| + x.report('inject symbol') { fastest } + x.report('inject to_proc') { fast } + x.report('inject block') { slow } + + x.compare! +end diff --git a/code/enumerable/select-last-vs-reverse-detect.rb b/code/enumerable/select-last-vs-reverse-detect.rb index f2d64b5a..f06ed603 100644 --- a/code/enumerable/select-last-vs-reverse-detect.rb +++ b/code/enumerable/select-last-vs-reverse-detect.rb @@ -1,13 +1,13 @@ require 'benchmark/ips' -ARRAY = [*1..100] +ARRAY = [*1..7777] def fast - ARRAY.reverse.detect { |x| (x % 10).zero? } + ARRAY.reverse.detect { |x| (x % 5).zero? } end def slow - ARRAY.select { |x| (x % 10).zero? }.last + ARRAY.select { |x| (x % 5).zero? }.last end Benchmark.ips do |x| diff --git a/code/enumerable/sort-vs-sort_by.rb b/code/enumerable/sort-vs-sort_by.rb index 979324f4..61032448 100644 --- a/code/enumerable/sort-vs-sort_by.rb +++ b/code/enumerable/sort-vs-sort_by.rb @@ -1,20 +1,26 @@ -require 'benchmark/ips' +require "benchmark/ips" User = Struct.new(:name) ARRAY = Array.new(100) do User.new(sprintf "%010d", rand(1_000_000_000)) end -def slow - ARRAY.sort { |a, b| a.name <=> b.name } +def fastest + ARRAY.sort_by(&:name) end -def fast - ARRAY.sort_by(&:name) +def faster + ARRAY.sort_by { |element| element.name } +end + +def slow + ARRAY.sort { |a, b| a.name <=> b.name } end Benchmark.ips do |x| + x.report('Enumerable#sort_by (Symbol#to_proc)') { fastest } + x.report('Enumerable#sort_by') { faster } x.report('Enumerable#sort') { slow } - x.report('Enumerable#sort_by') { fast } + x.compare! end diff --git a/code/enumerable/sort_by-first-vs-min_by.rb b/code/enumerable/sort_by-first-vs-min_by.rb new file mode 100644 index 00000000..75cd2ecc --- /dev/null +++ b/code/enumerable/sort_by-first-vs-min_by.rb @@ -0,0 +1,17 @@ +require 'benchmark/ips' + +ARRAY = [*1..100] + +def fast + ARRAY.min_by { |x| x.succ } +end + +def slow + ARRAY.sort_by { |x| x.succ }.first +end + +Benchmark.ips do |x| + x.report('Enumerable#min_by') { fast } + x.report('Enumerable#sort_by...first') { slow } + x.compare! +end diff --git a/code/general/array-argument-vs-splat-arguments.rb b/code/general/array-argument-vs-splat-arguments.rb new file mode 100644 index 00000000..28de4604 --- /dev/null +++ b/code/general/array-argument-vs-splat-arguments.rb @@ -0,0 +1,22 @@ +require "benchmark/ips" + +module M + ITEMS = (1..10000).to_a.freeze + + def self.func(*args) + end +end + +def fast + M.func(M::ITEMS) +end + +def slow + M.func(*M::ITEMS) +end + +Benchmark.ips do |x| + x.report("Function with single Array argument") { fast } + x.report("Function with splat arguments") { slow } + x.compare! +end diff --git a/code/general/assignment.rb b/code/general/assignment.rb deleted file mode 100644 index 320af1ba..00000000 --- a/code/general/assignment.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'benchmark/ips' - -def slow - a, b, c, d, e, f, g, h = 1, 2, 3, 4, 5, 6, 7, 8 -end - -def fast - a = 1 - b = 2 - c = 3 - d = 4 - e = 5 - f = 6 - g = 7 - h = 8 -end - -Benchmark.ips do |x| - x.report('Parallel Assignment') { slow } - x.report('Sequential Assignment') { fast } - x.compare! -end diff --git a/code/general/constantize-vs-comparison.rb b/code/general/constantize-vs-comparison.rb new file mode 100644 index 00000000..ab797b4b --- /dev/null +++ b/code/general/constantize-vs-comparison.rb @@ -0,0 +1,20 @@ +require "active_support/core_ext/string/inflections.rb" +require "benchmark/ips" + +class Foo; end + +def fast(s) + klass = Foo if s == "Foo" + nil +end + +def slow(s) + klass = s.constantize + nil +end + +Benchmark.ips do |x| + x.report("using an if statement") { fast("Foo") } + x.report("String#constantize") { slow("Foo") } + x.compare! +end diff --git a/code/general/format-vs-round-and-to-s.rb b/code/general/format-vs-round-and-to-s.rb new file mode 100644 index 00000000..2556d96c --- /dev/null +++ b/code/general/format-vs-round-and-to-s.rb @@ -0,0 +1,22 @@ +require 'benchmark/ips' + +NUM = 1.12678.freeze + +def fast + NUM.round(2).to_s +end + +def avg + format('%.2f', NUM) +end + +def slow + '%.2f' % NUM +end + +Benchmark.ips do |x| + x.report('Float#round') { fast } + x.report('Kernel#format') { avg } + x.report('String#%') { slow } + x.compare! +end diff --git a/code/general/hash-vs-openstruct-on-access.rb b/code/general/hash-vs-openstruct-on-access.rb new file mode 100644 index 00000000..bf5a65eb --- /dev/null +++ b/code/general/hash-vs-openstruct-on-access.rb @@ -0,0 +1,19 @@ +require "benchmark/ips" +require "ostruct" + + HASH = { field_1: 1, field_2: 2} + OPENSTRUCT = OpenStruct.new(field_1: 1, field_2: 2) + +def fast + [HASH[:field_1], HASH[:field_2]] +end + +def slow + [OPENSTRUCT.field_1, OPENSTRUCT.field_2] +end + +Benchmark.ips do |x| + x.report("Hash") { fast } + x.report("OpenStruct") { slow } + x.compare! +end diff --git a/code/general/hash-vs-openstruct.rb b/code/general/hash-vs-openstruct.rb new file mode 100644 index 00000000..bb342d89 --- /dev/null +++ b/code/general/hash-vs-openstruct.rb @@ -0,0 +1,16 @@ +require "benchmark/ips" +require "ostruct" + +def fast + { field_1: 1, field_2: 2 } +end + +def slow + OpenStruct.new(field_1: 1, field_2: 2) +end + +Benchmark.ips do |x| + x.report("Hash") { fast } + x.report("OpenStruct") { slow } + x.compare! +end diff --git a/code/general/inheritance-check.rb b/code/general/inheritance-check.rb new file mode 100644 index 00000000..2c272212 --- /dev/null +++ b/code/general/inheritance-check.rb @@ -0,0 +1,38 @@ +require 'benchmark/ips' + +# You may ask: 'Is there a project that still using `ancestors.include?`?' +# By quick searching, I found the following popular repositories are still using it: +# - rake +# - https://github.com/ruby/rake/blob/7d0c08fe4e97083a92d2c8fc740cb421fd062117/lib/rake/task_manager.rb#L28 +# - warden +# - https://github.com/hassox/warden/blob/090ed153dbd2f5bf4a1ca672b3018877e21223a4/lib/warden/strategies.rb#L16 +# - metasploit-framework +# - https://github.com/rapid7/metasploit-framework/blob/cac890a797d0d770260074dfe703eb5cfb63bd46/lib/msf/core/payload_set.rb#L239 +# - https://github.com/rapid7/metasploit-framework/blob/cb82015c8782280d964e222615b54c881bd36bbe/lib/msf/core/exploit.rb#L1440 +# - hanami +# - https://github.com/hanami/hanami/blob/506a35e5262939eb4dce9195ade3268e19928d00/lib/hanami/components/routes_inspector.rb#L54 +# - https://github.com/hanami/hanami/blob/aec069b602c772e279aa0a7f48d1a04d01756ee3/lib/hanami/configuration.rb#L114 +raise unless Object.ancestors.include?(Kernel) +raise unless (Object <= Kernel) + +def fast + (Class <= Class) + (Class <= Module) + (Class <= Object) + (Class <= Kernel) + (Class <= BasicObject) +end + +def slow + Class.ancestors.include?(Class) + Class.ancestors.include?(Module) + Class.ancestors.include?(Object) + Class.ancestors.include?(Kernel) + Class.ancestors.include?(BasicObject) +end + +Benchmark.ips do |x| + x.report('less than or equal') { fast } + x.report('ancestors.include?') { slow } + x.compare! +end diff --git a/code/general/loop-vs-while-true.rb b/code/general/loop-vs-while-true.rb new file mode 100644 index 00000000..33ae1391 --- /dev/null +++ b/code/general/loop-vs-while-true.rb @@ -0,0 +1,25 @@ +require "benchmark/ips" + +NUMBER = 100_000_000 + +def fast + index = 0 + while true + break if index > NUMBER + index += 1 + end +end + +def slow + index = 0 + loop do + break if index > NUMBER + index += 1 + end +end + +Benchmark.ips do |x| + x.report("While Loop") { fast } + x.report("Kernel loop") { slow } + x.compare! +end diff --git a/code/general/raise-vs-e2mmap.rb b/code/general/raise-vs-e2mmap.rb new file mode 100644 index 00000000..2d8ee4ee --- /dev/null +++ b/code/general/raise-vs-e2mmap.rb @@ -0,0 +1,73 @@ +require 'benchmark/ips' +require 'e2mmap' + +class WithE2MM + extend Exception2MessageMapper + + def_e2message TypeError, 'argument must be a %s' + def_exception :FooError, 'foo: %s' + + def self.raise_ruby_defined + Raise TypeError, 'Hash' + end + + def self.raise_user_defined + Raise FooError, 'bar!' + end +end + +class WithoutE2MM + FooError = Class.new(StandardError) + + def self.raise_ruby_defined + raise TypeError, 'argument must be a Hash' + end + + def self.raise_user_defined + raise FooError, 'foo: bar!' + end +end + +def slow_ruby_defined + begin + WithE2MM.raise_ruby_defined + rescue + 'fast ruby' + end +end + +def fast_ruby_defined + begin + WithoutE2MM.raise_ruby_defined + rescue + 'fast ruby' + end +end + +def slow_user_defined + begin + WithE2MM.raise_user_defined + rescue + 'fast ruby' + end +end + +def fast_user_defined + begin + WithoutE2MM.raise_user_defined + rescue + 'fast ruby' + end +end + +Benchmark.ips do |x| + x.report('Ruby exception: E2MM#Raise') { slow_ruby_defined } + x.report('Ruby exception: Kernel#raise') { fast_ruby_defined } + x.compare! +end + +Benchmark.ips do |x| + x.report('Custom exception: E2MM#Raise') { slow_user_defined } + x.report('Custom exception: Kernel#raise') { fast_user_defined } + x.compare! +end diff --git a/code/hash/dig-vs-[]-vs-fetch.rb b/code/hash/dig-vs-[]-vs-fetch.rb new file mode 100644 index 00000000..d2964416 --- /dev/null +++ b/code/hash/dig-vs-[]-vs-fetch.rb @@ -0,0 +1,31 @@ +require "benchmark/ips" + +h = { a: { b: { c: { d: { e: "foo" } } } } } + +Benchmark.ips do |x| + x.report "Hash#dig" do + h.dig(:a, :b, :c, :d, :e) + end + + x.report "Hash#[]" do + h[:a][:b][:c][:d][:e] + end + + x.report "Hash#[] ||" do + ((((h[:a] || {})[:b] || {})[:c] || {})[:d] || {})[:e] + end + + x.report "Hash#[] &&" do + h[:a] && h[:a][:b] && h[:a][:b][:c] && h[:a][:b][:c][:d] && h[:a][:b][:c][:d][:e] + end + + x.report "Hash#fetch" do + h.fetch(:a).fetch(:b).fetch(:c).fetch(:d).fetch(:e) + end + + x.report "Hash#fetch fallback" do + h.fetch(:a, {}).fetch(:b, {}).fetch(:c, {}).fetch(:d, {}).fetch(:e, nil) + end + + x.compare! +end diff --git a/code/hash/fetch-vs-fetch-with-block.rb b/code/hash/fetch-vs-fetch-with-block.rb index 94e2a49c..68badccf 100644 --- a/code/hash/fetch-vs-fetch-with-block.rb +++ b/code/hash/fetch-vs-fetch-with-block.rb @@ -1,17 +1,11 @@ require "benchmark/ips" HASH = { writing: :fast_ruby } - -def fast - HASH.fetch(:writing) { "fast ruby" } -end - -def slow - HASH.fetch(:writing, "fast ruby") -end +DEFAULT = "fast ruby" Benchmark.ips do |x| - x.report("Hash#fetch + block") { fast } - x.report("Hash#fetch + arg") { slow } + 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 diff --git a/code/hash/keys-include-vs-key.rb b/code/hash/keys-include-vs-key.rb new file mode 100644 index 00000000..1dbd4cbe --- /dev/null +++ b/code/hash/keys-include-vs-key.rb @@ -0,0 +1,18 @@ +require "benchmark/ips" + +HASH = Hash[*("a".."zzz").to_a.shuffle] +KEY = "zz" + +def key_fast + HASH.key? KEY +end + +def key_slow + HASH.keys.include? KEY +end + +Benchmark.ips do |x| + x.report("Hash#keys.include?") { key_slow } + x.report("Hash#key?") { key_fast } + x.compare! +end diff --git a/code/hash/merge-bang-vs-merge-vs-dup-merge-bang.rb b/code/hash/merge-bang-vs-merge-vs-dup-merge-bang.rb new file mode 100644 index 00000000..b4fd86b5 --- /dev/null +++ b/code/hash/merge-bang-vs-merge-vs-dup-merge-bang.rb @@ -0,0 +1,29 @@ +require "benchmark/ips" + +ENUM = (1..100) +ORIGINAL_HASH = { foo: "foo" } + +def fast + ENUM.inject([]) do |accumulator, element| + accumulator << ({ bar: element }.merge!(ORIGINAL_HASH){ |_key, left, _right| left }) + end +end + +def slow + ENUM.inject([]) do |accumulator, element| + accumulator << ORIGINAL_HASH.merge(bar: element) + end +end + +def slow_dup + ENUM.inject([]) do |accumulator, element| + accumulator << ORIGINAL_HASH.dup.merge!(bar: element) + end +end + +Benchmark.ips do |x| + x.report("{}#merge!(Hash) do end") { fast } + x.report("Hash#merge({})") { slow } + x.report("Hash#dup#merge!({})") { slow_dup } + x.compare! +end diff --git a/code/hash/merge-vs-double-splat-operator.rb b/code/hash/merge-vs-double-splat-operator.rb new file mode 100644 index 00000000..b0b63e0d --- /dev/null +++ b/code/hash/merge-vs-double-splat-operator.rb @@ -0,0 +1,17 @@ +require 'benchmark/ips' + +def fast + h2 = { a: 'a' } + { one: 1, **h2 } +end + +def slow + h2 = { a: 'a' } + { one: 1 }.merge(h2) +end + +Benchmark.ips do |x| + x.report('Hash#**other') { fast } + x.report('Hash#merge') { slow } + x.compare! +end diff --git a/code/hash/slice-native-vs-before-native.rb b/code/hash/slice-native-vs-before-native.rb new file mode 100644 index 00000000..85cc70f3 --- /dev/null +++ b/code/hash/slice-native-vs-before-native.rb @@ -0,0 +1,49 @@ +require 'benchmark/ips' + +HASH = { + title: "awesome", + description: "a description", + author: "styd", + published_at: Time.now +} + +HASH.dup.each{ |k, v| HASH[:"other_#{k}"], HASH[:"more_#{k}"] = v, v } + +# Native implementation since v2.5 +# +def fastest + HASH.slice(*%i[title author other]) +end + +# @ixti's faster implementation than that of ActiveSupport below +# +# Source: +# https://github.com/JuanitoFatas/fast-ruby/pull/173#issuecomment-470554483 +# +def faster + memo = {} + %i[title author other].each { |k| memo[k] = HASH[k] if HASH.key?(k) } + memo +end + +# ActiveSupport implementation of `slice` when it's not defined. +# +# Source: +# https://github.com/rails/rails/blob/01ae39660243bc5f0a986e20f9c9bff312b1b5f8/activesupport/lib/active_support/core_ext/hash/slice.rb#L24 +# +def fast + %i[title author other].each_with_object({}){ |k, h| h[k] = HASH[k] if HASH.key?(k) } +end + +def slow + keys = %i[title author other] + HASH.select { |k, _| keys.include? k } +end + +Benchmark.ips do |x| + x.report('Hash#native-slice ') { fastest } + x.report('Array#each ') { faster } + x.report('Array#each_w/_object') { fast } + x.report('Hash#select-include ') { slow } + x.compare! +end diff --git a/code/hash/update-vs-[]=.rb b/code/hash/update-vs-[]=.rb new file mode 100644 index 00000000..a004edf5 --- /dev/null +++ b/code/hash/update-vs-[]=.rb @@ -0,0 +1,21 @@ +require 'benchmark/ips' + +ENUM = (1..100) + +def fast + ENUM.each_with_object({}) do |e, h| + h[e] = e + end +end + +def slow + ENUM.each_with_object({}) do |e, h| + h.update(e => e) + end +end + +Benchmark.ips do |x| + x.report('Hash#[]=') { fast } + x.report('Hash#update') { slow } + x.compare! +end diff --git a/code/hash/values-include-vs-value.rb b/code/hash/values-include-vs-value.rb new file mode 100644 index 00000000..ae4342dc --- /dev/null +++ b/code/hash/values-include-vs-value.rb @@ -0,0 +1,18 @@ +require "benchmark/ips" + +HASH = Hash[*("a".."zzz").to_a.shuffle] +VALUE = "zz" + +def value_fast + HASH.value? VALUE +end + +def value_slow + HASH.values.include? VALUE +end + +Benchmark.ips do |x| + x.report("Hash#values.include?") { value_slow } + x.report("Hash#value?") { value_fast } + x.compare! +end diff --git a/code/proc-and-block/proc-call-vs-yield.rb b/code/proc-and-block/proc-call-vs-yield.rb index f2e4545d..3e95968c 100644 --- a/code/proc-and-block/proc-call-vs-yield.rb +++ b/code/proc-and-block/proc-call-vs-yield.rb @@ -1,15 +1,25 @@ require 'benchmark/ips' -def slow &block +def slow(&block) block.call end +def slow2(&block) + yield +end + +def slow3(&block) + +end + def fast yield end Benchmark.ips do |x| x.report('block.call') { slow { 1 + 1 } } + x.report('block + yield') { slow2 { 1 + 1 } } + x.report('unused block') { slow3 { 1 + 1 } } x.report('yield') { fast { 1 + 1 } } x.compare! end diff --git a/code/range/cover-vs-include.rb b/code/range/cover-vs-include.rb index 34085ae2..8d23c1b4 100644 --- a/code/range/cover-vs-include.rb +++ b/code/range/cover-vs-include.rb @@ -5,16 +5,12 @@ END_OF_JULY = Date.new(2015, 7, 31) DAY_IN_JULY = Date.new(2015, 7, 15) -def fast - (BEGIN_OF_JULY..END_OF_JULY).cover? DAY_IN_JULY -end - -def slow - (BEGIN_OF_JULY..END_OF_JULY).include? DAY_IN_JULY -end - Benchmark.ips do |x| - x.report("Range#cover?") { fast } - x.report("Range#include?") { slow } + 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 diff --git a/code/string/===-vs-=~-vs-match.rb b/code/string/===-vs-=~-vs-match.rb new file mode 100644 index 00000000..6ac401bd --- /dev/null +++ b/code/string/===-vs-=~-vs-match.rb @@ -0,0 +1,35 @@ +require "benchmark/ips" + +def fastest + /boo/.match?('foo'.freeze) +end + +def fast + "foo".freeze.match?(/boo/) +end + +def slow + "foo".freeze =~ /boo/ +end + +def slower + /boo/ === "foo".freeze +end + +def even_slower + /boo/.match('foo'.freeze) +end + +def slowest + "foo".freeze.match(/boo/) +end + +Benchmark.ips do |x| + x.report("Regexp#match?") { fastest } if RUBY_VERSION >= "2.4.0".freeze + x.report("String#match?") { fast } if RUBY_VERSION >= "2.4.0".freeze + x.report("String#=~") { slow } + x.report("Regexp#===") { slower } + x.report("Regexp#match") { even_slower } + x.report("String#match") { slowest } + x.compare! +end diff --git a/code/string/casecmp-vs-downcase-==.rb b/code/string/casecmp-vs-downcase-==.rb index 027f7483..be46803f 100644 --- a/code/string/casecmp-vs-downcase-==.rb +++ b/code/string/casecmp-vs-downcase-==.rb @@ -2,6 +2,10 @@ SLUG = 'ABCD' +def slowest + SLUG.casecmp?('abcd') +end + def slow SLUG.downcase == 'abcd' end @@ -11,6 +15,7 @@ def fast end Benchmark.ips do |x| + x.report("String#casecmp?") { slowest } if RUBY_VERSION >= "2.4.0".freeze x.report('String#downcase + ==') { slow } x.report('String#casecmp') { fast } x.compare! diff --git a/code/string/concatenation.rb b/code/string/concatenation.rb index 1d697c32..e249a307 100644 --- a/code/string/concatenation.rb +++ b/code/string/concatenation.rb @@ -20,10 +20,15 @@ def fast 'foo' 'bar' end +def fast_interpolation + "#{'foo'}#{'bar'}" +end + Benchmark.ips do |x| - x.report('String#+') { slow_plus } - x.report('String#concat') { slow_concat } - x.report('String#append') { slow_append } - x.report('"foo" "bar"') { fast } + x.report('String#+') { slow_plus } + x.report('String#concat') { slow_concat } + x.report('String#append') { slow_append } + x.report('"foo" "bar"') { fast } + x.report('"#{\'foo\'}#{\'bar\'}"') { fast_interpolation } x.compare! end diff --git a/code/string/concatenation_randomized.rb b/code/string/concatenation_randomized.rb new file mode 100644 index 00000000..8a5e7a9f --- /dev/null +++ b/code/string/concatenation_randomized.rb @@ -0,0 +1,154 @@ +require 'benchmark/ips' + +module RandStr + RND_STRINGS_AMOUNT = 1000 + @rand_strs = { + lt100: [], + lt10: [], + lt1000: [], + eq10: [], + eq100: [], + } + + def self.generate_rand_strs + chars = ('A'..'z').to_a * 20 + @rand_strs[:lt10] = Array.new(RND_STRINGS_AMOUNT) { chars.sample(rand(10)).join } + @rand_strs[:lt100] = Array.new(RND_STRINGS_AMOUNT) { chars.sample(rand(100)).join } + @rand_strs[:lt1000] = Array.new(RND_STRINGS_AMOUNT) { chars.sample(rand(1000)).join } + @rand_strs[:eq10] = Array.new(RND_STRINGS_AMOUNT) { chars.sample(10).join } + @rand_strs[:eq100] = Array.new(RND_STRINGS_AMOUNT) { chars.sample(100).join } + end + + self.generate_rand_strs + + def self.rand_str(named_range) + @rand_strs[named_range][rand(RND_STRINGS_AMOUNT)] + end + + def self.method_missing(symbol) + return super unless @rand_strs.keys.include?(symbol) + + define_singleton_method(symbol) { rand_str(symbol) } + return rand_str(symbol) + end + +end + + +# 2 + 1 = 3 object +def fastest_plus(foo, bar) + foo + bar +end + +# 2 + 1 = 3 object +def slow_concat(foo, bar) + foo.concat bar +end + +# 2 + 1 = 3 object +def slow_append(foo, bar) + foo << bar +end + + +def fast_interpolation(foo, bar) + "#{foo}#{bar}" +end + +# bench_100_to_100 +# Rehearsal ----------------------------------------------------------- +# String#+ 1.263725 0.027868 1.291593 ( 1.292498) +# "#{foo}#{bar}" 1.139442 0.022956 1.162398 ( 1.163574) +# String#concat 2.017746 0.014836 2.032582 ( 2.034682) +# String#append 1.320778 0.000000 1.320778 ( 1.321896) +# Collateral actions only 0.713309 0.000000 0.713309 ( 0.714402) +# -------------------------------------------------- total: 6.520660sec +# +# user system total real nomalized ratio +# Collateral actions only 0.703668 0.000000 0.703668 ( 0.705658) +# String#+ 1.014123 0.000000 1.014123 ( 1.015003) 0.30934 +# "#{foo}#{bar}" 1.101751 0.000585 1.102336 ( 1.103558) 0.3979 x 1.3 slower +# String#concat 1.382647 0.000000 1.382647 ( 1.385333) 0.679675 x 2.2 slower +# String#append 1.319974 0.000000 1.319974 ( 1.324772) 0.619114 x 2 slower + +def bench_100_to_100 + Benchmark.ips do |x| + # 1M for rehearsal + 1M for bm + sarr1 = Array.new(2_000_000) { RandStr.eq100.dup } + sarr2 = Array.new(2_000_000) { RandStr.eq100.dup } + + i, j = 0, 0 + # if we want compare apples with apples, we need to measure and exclude "collateral" operations: + # integer += 1, access to an array of randomized strings 100 symbols length, + # then two methods invocation from RandStr module eq100 / lt100. + # + # and only then we can compare string concat methods properly + x.report("Collateral actions only") { k=0; 1_000_000.times { k+=1; RandStr.eq100; sarr2[k]; RandStr.lt100; } } + + x.report("String#+") { k=0; 1_000_000.times { k+=1; sarr1[k]; fastest_plus(RandStr.eq100, RandStr.lt100) } } + x.report('"#{foo}#{bar}"') { k=0; 1_000_000.times { k+=1; sarr2[k]; fast_interpolation(RandStr.eq100, RandStr.lt100) } } + x.report("String#concat") { 1_000_000.times { RandStr.eq100; slow_concat(sarr1[i], RandStr.lt100); i+=1; } } + x.report("String#append") { 1_000_000.times { RandStr.eq100; slow_append(sarr2[j], RandStr.lt100); j+=1; } } + end +end + +# bench_100_to_1000 +# Rehearsal ----------------------------------------------------------- +# Collateral actions only 0.674168 0.000016 0.674184 ( 0.675031) +# String#+ 2.148756 0.032954 2.181710 ( 2.187042) +# "#{foo}#{bar}" 1.570816 0.004948 1.575764 ( 1.579080) +# String#concat 2.223220 0.160917 2.384137 ( 2.387601) +# String#append 2.005056 0.202962 2.208018 ( 2.211476) +# -------------------------------------------------- total: 9.023813sec +# +# user system total real nomalized ratio +# Collateral actions only 0.666190 0.000000 0.666190 ( 0.666398) +# String#+ 1.077629 0.036944 1.114573 ( 1.115465) 0.449067 +# "#{foo}#{bar}" 1.230489 0.001029 1.231518 ( 1.232423) 0.566025 x 1.25 slower +# String#concat 1.881313 0.149949 2.031262 ( 2.033965) 1.367567 x 3.05 slower +# String#append 1.913785 0.177921 2.091706 ( 2.094298) 1.4279 x 3.18 slower + +def bench_100_to_1000 + Benchmark.ips do |x| + sarr1 = Array.new(2_000_000) { RandStr.eq100.dup } + sarr2 = Array.new(2_000_000) { RandStr.eq100.dup } + + i, j = 0, 0 + x.report("Collateral actions only") { k=0; 1_000_000.times { k+=1; RandStr.eq100; sarr2[k]; RandStr.lt1000; } } + + x.report("String#+") { k=0; 1_000_000.times { k+=1; sarr1[k]; fastest_plus(RandStr.eq100, RandStr.lt1000) } } + x.report('"#{foo}#{bar}"') { k=0; 1_000_000.times { k+=1; sarr2[k]; fast_interpolation(RandStr.eq100, RandStr.lt1000) } } + x.report("String#concat") { 1_000_000.times { RandStr.eq100; slow_concat(sarr1[i], RandStr.lt1000); i+=1; } } + x.report("String#append") { 1_000_000.times { RandStr.eq100; slow_append(sarr2[j], RandStr.lt1000); j+=1; } } + end +end + +# bench_10_to_100 +# Rehearsal ----------------------------------------------------------- +# Collateral actions only 0.681273 0.000000 0.681273 ( 0.681611) +# String#+ 1.188326 0.000701 1.189027 ( 1.196455) +# "#{foo}#{bar}" 1.182554 0.003851 1.186405 ( 1.191678) +# String#concat 1.707191 0.006764 1.713955 ( 1.720055) +# String#append 1.177368 0.000831 1.178199 ( 1.184116) +# -------------------------------------------------- total: 5.948859sec +# +# user system total real nomalized ratio +# Collateral actions only 0.682486 0.000000 0.682486 ( 0.682818) +# String#+ 0.914002 0.000000 0.914002 ( 0.917294) 0.234476 +# "#{foo}#{bar}" 1.096633 0.000966 1.097599 ( 1.100782) 0.417964 x 1.78 slower +# String#concat 1.373582 0.000910 1.374492 ( 1.375239) 0.692421 x 2.95 slower +# String#append 1.300632 0.000000 1.300632 ( 1.300807) 0.617989 x 2.63 slower + +def bench_10_to_100 + Benchmark.ips do |x| + sarr1 = Array.new(2_000_000) { RandStr.eq100.dup } + sarr2 = Array.new(2_000_000) { RandStr.eq100.dup } + + i, j = 0, 0 + x.report("Collateral actions only") { k=0; 1_000_000.times { k+=1; RandStr.eq10; sarr2[k]; RandStr.lt100; } } + x.report("String#+") { k=0; 1_000_000.times { k+=1; sarr1[k]; fastest_plus(RandStr.eq10, RandStr.lt100) } } + x.report('"#{foo}#{bar}"') { k=0; 1_000_000.times { k+=1; sarr2[k]; fast_interpolation(RandStr.eq10, RandStr.lt100) } } + x.report("String#concat") { 1_000_000.times { RandStr.eq10; slow_concat(sarr1[i], RandStr.lt100); i+=1; } } + x.report("String#append") { 1_000_000.times { RandStr.eq10; slow_append(sarr2[j], RandStr.lt100); j+=1; } } + end +end diff --git a/code/string/dup-vs-unary-plus.rb b/code/string/dup-vs-unary-plus.rb new file mode 100644 index 00000000..e31381b3 --- /dev/null +++ b/code/string/dup-vs-unary-plus.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'benchmark/ips' + +if RUBY_VERSION >= '2.3.0' + def fast + +'' + end + + def slow + ''.dup + end + + Benchmark.ips do |x| + x.report('String#+@') { fast } + x.report('String#dup') { slow } + x.compare! + end +end diff --git a/code/string/end-string-checking-match-vs-end_with.rb b/code/string/end-string-checking-match-vs-end_with.rb new file mode 100644 index 00000000..e95d87ab --- /dev/null +++ b/code/string/end-string-checking-match-vs-end_with.rb @@ -0,0 +1,22 @@ +require 'benchmark/ips' + +SLUG = "some_kind_of_root_url" + +def slower + SLUG =~ /_(path|url)$/ +end + +def slow + SLUG.match?(/_(path|url)$/) +end + +def fast + SLUG.end_with?('_path', '_url') +end + +Benchmark.ips do |x| + x.report('String#=~') { slower } + x.report('String#match?') { slow } if RUBY_VERSION >= "2.4.0".freeze + x.report('String#end_with?') { fast } + x.compare! +end diff --git a/code/string/end-string-checking-match-vs-start_with.rb b/code/string/end-string-checking-match-vs-start_with.rb deleted file mode 100644 index c73602dd..00000000 --- a/code/string/end-string-checking-match-vs-start_with.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'benchmark/ips' - -SLUG = 'root_url' - -def slow - SLUG =~ /_(path|url)$/ -end - -def fast - SLUG.end_with?('_path', '_url') -end - -Benchmark.ips do |x| - x.report('String#=~') { slow } - x.report('String#end_with?') { fast } - x.compare! -end diff --git a/code/string/gsub-vs-sub.rb b/code/string/gsub-vs-sub.rb index c4bb0325..1d0804dd 100644 --- a/code/string/gsub-vs-sub.rb +++ b/code/string/gsub-vs-sub.rb @@ -10,8 +10,15 @@ def fast URL.sub('http://', 'https://') end +def fastest + str = URL.dup + str['http://'] = 'https://' + str +end + Benchmark.ips do |x| x.report('String#gsub') { slow } x.report('String#sub') { fast } + x.report('String#dup["string"]=') { fastest } x.compare! end diff --git a/code/string/gsub-vs-tr-vs-delete.rb b/code/string/gsub-vs-tr-vs-delete.rb new file mode 100644 index 00000000..6afd0393 --- /dev/null +++ b/code/string/gsub-vs-tr-vs-delete.rb @@ -0,0 +1,28 @@ +require 'benchmark/ips' + +WORDS = 'writing fast ruby' +SPACE = ' ' + +def use_gsub + WORDS.gsub(' ', '') +end + +def use_tr + WORDS.tr(' ', '') +end + +def use_delete + WORDS.delete(' ') +end + +def use_delete_const + WORDS.delete(SPACE) +end + +Benchmark.ips do |x| + x.report('String#gsub') { use_gsub } + x.report('String#tr') { use_tr } + x.report('String#delete') { use_delete } + x.report('String#delete const') { use_delete_const } + x.compare! +end diff --git a/code/string/mutable_vs_immutable_strings.rb b/code/string/mutable_vs_immutable_strings.rb new file mode 100644 index 00000000..d02a739a --- /dev/null +++ b/code/string/mutable_vs_immutable_strings.rb @@ -0,0 +1,17 @@ +require "benchmark/ips" + +# Allocates new string over and over again +def without_freeze + "To freeze or not to freeze" +end + +# Keeps and reuses shared string +def with_feeze + "To freeze or not to freeze".freeze +end + +Benchmark.ips do |x| + x.report("Without Freeze") { without_freeze } + x.report("With Freeze") { with_feeze } + x.compare! +end diff --git a/code/string/remove-extra-spaces-or-other-chars.rb b/code/string/remove-extra-spaces-or-other-chars.rb new file mode 100644 index 00000000..fb1349f7 --- /dev/null +++ b/code/string/remove-extra-spaces-or-other-chars.rb @@ -0,0 +1,24 @@ +require 'benchmark/ips' + +PASSAGE = <<~LIPSUM + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +LIPSUM + +raise unless PASSAGE.gsub(/ +/, " ") == PASSAGE.squeeze(" ") + +def slow + PASSAGE.gsub(/ +/, " ") +end + +def fast + PASSAGE.squeeze(" ") +end + +Benchmark.ips do |x| + x.report('String#gsub/regex+/') { slow } + x.report('String#squeeze') { fast } + x.compare! +end diff --git a/code/string/start-string-checking-match-vs-start_with.rb b/code/string/start-string-checking-match-vs-start_with.rb index eb529f1b..a58eb35d 100644 --- a/code/string/start-string-checking-match-vs-start_with.rb +++ b/code/string/start-string-checking-match-vs-start_with.rb @@ -1,17 +1,22 @@ require 'benchmark/ips' -SLUG = 'test_reverse_merge.rb' +SLUG = 'test_some_kind_of_long_file_name.rb' -def slow +def slower SLUG =~ /^test_/ end +def slow + SLUG.match?(/^test_/) +end + def fast SLUG.start_with?('test_') end Benchmark.ips do |x| - x.report('String#=~') { slow } + x.report('String#=~') { slower } + x.report('String#match?') { slow } if RUBY_VERSION >= "2.4.0".freeze x.report('String#start_with?') { fast } x.compare! end diff --git a/code/string/start_with-vs-substring-==.rb b/code/string/start_with-vs-substring-==.rb new file mode 100755 index 00000000..32d8c180 --- /dev/null +++ b/code/string/start_with-vs-substring-==.rb @@ -0,0 +1,33 @@ +require "benchmark/ips" + +PREFIX = "_" +STRINGS = (0..9).map{|n| "#{PREFIX if n.odd?}#{n}" } + +START_WITH = STRINGS.each_index.map do |i| + "STRINGS[#{i}].start_with?(PREFIX)" +end.join(";") + +EQL_USING_LENGTH = STRINGS.each_index.map do |i| + # use `eql?` instead of `==` to prevent warnings + "STRINGS[#{i}][0, PREFIX.length].eql?(PREFIX)" +end.join(";") + +RANGE = 0...PREFIX.length + +EQL_USING_RANGE_PREALLOC = STRINGS.each_index.map do |i| + # use `eql?` instead of `==` to prevent warnings + "STRINGS[#{i}][RANGE].eql?(PREFIX)" +end.join(";") + +EQL_USING_RANGE = STRINGS.each_index.map do |i| + # use `eql?` instead of `==` to prevent warnings + "STRINGS[#{i}][0...PREFIX.length].eql?(PREFIX)" +end.join(";") + +Benchmark.ips do |x| + x.report("String#start_with?", START_WITH) + x.report("String#[0, n] ==", EQL_USING_LENGTH) + x.report("String#[RANGE] ==", EQL_USING_RANGE_PREALLOC) + x.report("String#[0...n] ==", EQL_USING_RANGE) + x.compare! +end diff --git a/code/string/sub-vs-chomp-vs-delete_suffix.rb b/code/string/sub-vs-chomp-vs-delete_suffix.rb new file mode 100644 index 00000000..6ecfcf9f --- /dev/null +++ b/code/string/sub-vs-chomp-vs-delete_suffix.rb @@ -0,0 +1,22 @@ +require 'benchmark/ips' + +SLUG = 'YourSubclassType' + +def slow + SLUG.sub(/Type\z/, '') +end + +def fast + SLUG.chomp('Type') +end + +def faster + SLUG.delete_suffix('Type') +end + +Benchmark.ips do |x| + x.report('String#sub') { slow } + x.report("String#chomp") { fast } + x.report("String#delete_suffix") { faster } if RUBY_VERSION >= '2.5.0' + x.compare! +end diff --git a/code/string/sub-vs-delete_prefix.rb b/code/string/sub-vs-delete_prefix.rb new file mode 100644 index 00000000..19ac1382 --- /dev/null +++ b/code/string/sub-vs-delete_prefix.rb @@ -0,0 +1,19 @@ +require "benchmark/ips" + +if RUBY_VERSION >= '2.5.0' + STRING = 'Foo::Foo::Bar'.freeze + + def fast + STRING.delete_prefix('Foo::') + end + + def slow + STRING.sub(/\AFoo::/, '') + end + + Benchmark.ips do |x| + x.report("String#delete_prefix") { fast } + x.report('String#sub') { slow } + x.compare! + end +end diff --git a/code/string/unpack1-vs-unpack[0].rb b/code/string/unpack1-vs-unpack[0].rb new file mode 100644 index 00000000..746909a8 --- /dev/null +++ b/code/string/unpack1-vs-unpack[0].rb @@ -0,0 +1,19 @@ +require 'benchmark/ips' + +if RUBY_VERSION >= '2.4.0' + STRING = "foobarbaz".freeze + + def fast + STRING.unpack1('h*') + end + + def slow + STRING.unpack('h*')[0] + end + + Benchmark.ips do |x| + x.report('String#unpack1') { fast } + x.report('String#unpack[0]') { slow } + x.compare! + end +end diff --git a/code/time/iso8601-vs-parse.rb b/code/time/iso8601-vs-parse.rb new file mode 100644 index 00000000..6cf7f9f8 --- /dev/null +++ b/code/time/iso8601-vs-parse.rb @@ -0,0 +1,18 @@ +require 'benchmark/ips' +require 'time' + +STRING = '2018-03-21T11:26:50Z'.freeze + +def fast + Time.iso8601(STRING) +end + +def slow + Time.parse(STRING) +end + +Benchmark.ips do |x| + x.report('Time.iso8601') { fast } + x.report('Time.parse') { slow } + x.compare! +end