[ruby-core:94085] [Ruby master Feature#15902] Add a specialized instruction for `.nil?`
From:
tenderlove@...
Date:
2019-07-31 23:26:02 UTC
List:
ruby-core #94085
Issue #15902 has been updated by tenderlovemaking (Aaron Patterson).
Status changed from Open to Closed
I pushed this as 9faef3113fb4331524b81ba73005ba13fa0ef6c6
We found it decreased our app boot time by ~1.8 seconds (which is not large percentage wise, but definitely measurable). I tested by booting our app in production mode 50 times with and without the patch.
Here is a graph of the results:

Again with Y at 0:

----------------------------------------
Feature #15902: Add a specialized instruction for `.nil?`
https://bugs.ruby-lang.org/issues/15902#change-80316
* Author: tenderlovemaking (Aaron Patterson)
* Status: Closed
* Priority: Normal
* Assignee:
* Target version:
----------------------------------------
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
``` ruby
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:
``` yaml
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--------------------------------
0001-Add-a-specialized-instruction-for-.nil-calls.patch (7.13 KB)
--
https://bugs.ruby-lang.org/
Unsubscribe: <mailto:ruby-core-request@ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-core>