Project

General

Profile

Feature #15902

Add a specialized instruction for `.nil?`

Added by tenderlovemaking (Aaron Patterson) about 1 month ago. Updated about 1 month ago.

Status:
Open
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:92982]

Description

I'd like to add a specialized instruction for .nil?. We have specialized instructions for .length and .empty?, and surprisingly our application also calls .nil? a lot:

[aaron@TC ~/g/github (gc-boot-stats)]$ git grep '.empty?' | wc -l
    2553
[aaron@TC ~/g/github (gc-boot-stats)]$ git grep '.length' | wc -l
    3975
[aaron@TC ~/g/github (gc-boot-stats)]$ git grep '.nil?' | wc -l
    3117

I'm not sure how hot any of the .nil? callsites are, but I think this instruction will speed up most of them.

I tried two benchmark runners:

Benchmark/ips

require "benchmark/ips"

class Niller
  def nil?; true; end
end

not_nil = Object.new
xnil = nil
niller = Niller.new

Benchmark.ips do |x|
  x.report("nil?")    { xnil.nil? }
  x.report("not nil") { not_nil.nil? }
  x.report("niller")   { niller.nil? }
end

Results

On Ruby master:

[aaron@TC ~/g/ruby (master)]$ ./ruby compil.rb
Warming up --------------------------------------
                nil?   429.195k i/100ms
             not nil   437.889k i/100ms
              niller   437.935k i/100ms
Calculating -------------------------------------
                nil?     20.166M (± 8.1%) i/s -    100.002M in   5.002794s
             not nil     20.046M (± 7.6%) i/s -     99.839M in   5.020086s
              niller     22.467M (± 6.1%) i/s -    112.111M in   5.013817s
[aaron@TC ~/g/ruby (master)]$ ./ruby compil.rb
Warming up --------------------------------------
                nil?   449.660k i/100ms
             not nil   433.836k i/100ms
              niller   443.073k i/100ms
Calculating -------------------------------------
                nil?     19.997M (± 8.8%) i/s -     99.375M in   5.020458s
             not nil     20.529M (± 7.0%) i/s -    102.385M in   5.020689s
              niller     21.796M (± 8.0%) i/s -    108.110M in   5.002300s
[aaron@TC ~/g/ruby (master)]$ ./ruby compil.rb
Warming up --------------------------------------
                nil?   402.119k i/100ms
             not nil   438.968k i/100ms
              niller   398.226k i/100ms
Calculating -------------------------------------
                nil?     20.050M (±12.2%) i/s -     98.519M in   5.008817s
             not nil     20.614M (± 8.0%) i/s -    102.280M in   5.004531s
              niller     22.223M (± 8.8%) i/s -    110.309M in   5.013106s

On this patch:

[aaron@TC ~/g/ruby (specialized-nilp)]$ ./ruby compil.rb
Warming up --------------------------------------
                nil?   468.371k i/100ms
             not nil   456.517k i/100ms
              niller   454.981k i/100ms
Calculating -------------------------------------
                nil?     27.849M (± 7.8%) i/s -    138.169M in   5.001730s
             not nil     26.417M (± 8.7%) i/s -    131.020M in   5.011674s
              niller     21.561M (± 7.5%) i/s -    107.376M in   5.018113s
[aaron@TC ~/g/ruby (specialized-nilp)]$ ./ruby compil.rb
Warming up --------------------------------------
                nil?   477.259k i/100ms
             not nil   428.712k i/100ms
              niller   446.109k i/100ms
Calculating -------------------------------------
                nil?     28.071M (± 7.3%) i/s -    139.837M in   5.016590s
             not nil     25.789M (±12.9%) i/s -    126.470M in   5.011144s
              niller     20.002M (±12.2%) i/s -     98.144M in   5.001737s
[aaron@TC ~/g/ruby (specialized-nilp)]$ ./ruby compil.rb
Warming up --------------------------------------
                nil?   467.676k i/100ms
             not nil   445.791k i/100ms
              niller   415.024k i/100ms
Calculating -------------------------------------
                nil?     26.907M (± 8.0%) i/s -    133.755M in   5.013915s
             not nil     25.319M (± 7.9%) i/s -    125.713M in   5.007758s
              niller     19.569M (±11.8%) i/s -     96.286M in   5.008533s

According to benchmark/ips, it's about 27% faster when the object is nil or a regular object. When it's an object that implements .nil?, I think it might be slower but it's hard to tell.

Benchmark-driver

I added a benchmark driver file:

prelude: |
  class Niller; def nil?; true; end; end
  xnil, notnil = nil, Object.new
  niller = Niller.new
benchmark:
  - xnil.nil?
  - notnil.nil?
  - niller.nil?
loop_count: 10000000

Results (tested against master @ c9b74f9fd95113df903fc34cc1d6ec3fb3160c85 )

[aaron@TC ~/g/ruby (specialized-nilp)]$ make benchmark ARGS=benchmark/nil_p.yml
./revision.h unchanged
/Users/aaron/.rbenv/shims/ruby --disable=gems -rrubygems -I./benchmark/lib ./benchmark/benchmark-driver/exe/benchmark-driver \
                --executables="compare-ruby::/Users/aaron/.rbenv/shims/ruby --disable=gems -I.ext/common --disable-gem" \
                --executables="built-ruby::./miniruby -I./lib -I. -I.ext/common  ./tool/runruby.rb --extout=.ext  -- --disable-gems --disable-gem" \
                benchmark/nil_p.yml 
Calculating -------------------------------------
                     compare-ruby  built-ruby 
           xnil.nil?      68.825M    405.121M i/s -     10.000M times in 0.145296s 0.024684s
         notnil.nil?      66.357M    267.874M i/s -     10.000M times in 0.150700s 0.037331s
         niller.nil?     110.273M    123.089M i/s -     10.000M times in 0.090684s 0.081242s

Comparison:
                        xnil.nil?
          built-ruby: 405120725.0 i/s 
        compare-ruby:  68825019.2 i/s - 5.89x  slower

                      notnil.nil?
          built-ruby: 267873885.2 i/s 
        compare-ruby:  66357000.6 i/s - 4.04x  slower

                      niller.nil?
          built-ruby: 123089042.6 i/s 
        compare-ruby: 110273035.8 i/s - 1.12x  slower

[aaron@TC ~/g/ruby (specialized-nilp)]$ make benchmark ARGS=benchmark/nil_p.yml
./revision.h unchanged
/Users/aaron/.rbenv/shims/ruby --disable=gems -rrubygems -I./benchmark/lib ./benchmark/benchmark-driver/exe/benchmark-driver \
                --executables="compare-ruby::/Users/aaron/.rbenv/shims/ruby --disable=gems -I.ext/common --disable-gem" \
                --executables="built-ruby::./miniruby -I./lib -I. -I.ext/common  ./tool/runruby.rb --extout=.ext  -- --disable-gems --disable-gem" \
                benchmark/nil_p.yml 
Calculating -------------------------------------
                     compare-ruby  built-ruby 
           xnil.nil?      45.083M    360.998M i/s -     10.000M times in 0.221811s 0.027701s
         notnil.nil?      69.558M    271.054M i/s -     10.000M times in 0.143765s 0.036893s
         niller.nil?     115.423M     79.667M i/s -     10.000M times in 0.086638s 0.125523s

Comparison:
                        xnil.nil?
          built-ruby: 360997801.1 i/s 
        compare-ruby:  45083426.9 i/s - 8.01x  slower

                      notnil.nil?
          built-ruby: 271054130.3 i/s 
        compare-ruby:  69557959.1 i/s - 3.90x  slower

                      niller.nil?
        compare-ruby: 115422793.6 i/s 
          built-ruby:  79666674.5 i/s - 1.45x  slower

I think there is too much noise for the third case.

I'm not happy about making rb_false non-static, but I'm not sure how else to do this patch.

What do you think?


Files

History

Updated by Eregon (Benoit Daloze) about 1 month ago

FWIW, we already inline nil? in the AST for TruffleRuby:
https://github.com/oracle/truffleruby/commit/57628008#diff-09677670ed37ee212c217374c6468718

Makes sense since it's such a small operation and it's frequently used.

Also available in: Atom PDF