Nothingness in Julia

April 3, 2024blog

If you ever work in Julia, something you'll notice is that lots of people and the language server will recommend that you use isnothing(x) or x === nothing instead of the value comparison x == nothing. This is also true of the ismissing functionality for missing values.

Good discussion here, and a great Stack Overflow answer here.

How meaningful is this, though? I decided to do some benchmarking to see how much of a difference this makes.

The TLDR is that x == nothing isn't specialized to checking nothing, whereas isnothing and x === nothing are. x === nothing is a core language feature (I believe) and isnothing is compile time dispatched and is thus relatively quick. x == nothing is a value comparison and I think requires some extra stuff to happen on top.

There's a speed component to this too -- isnothing is the fastest on my machine, x === nothing is the second fastest, and x == nothing is the slowest.

isnothing:

julia> @benchmark isnothing(x)
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
 Range (min … max):  1.539 ns … 24.880 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     1.579 ns              ┊ GC (median):    0.00%
 Time  (mean ± σ):   1.589 ns ±  0.269 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

   ▁▃▁          ██
  ▃███▄▃▂▂▂▂▂▂▂███▇▆▃▂▂▂▂▁▁▂▃▅▅▃▃▂▂▂▂▂▁▁▁▂▂▂▂▂▂▂▂▁▁▁▁▁▂▂▂▂▂▂ ▃
  1.54 ns        Histogram: frequency by time         1.7 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.

x === nothing:

julia> @benchmark x === nothing
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
 Range (min … max):  1.755 ns … 5.381 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     1.772 ns             ┊ GC (median):    0.00%
 Time  (mean ± σ):   1.791 ns ± 0.102 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

             █▃▄▃▁
  ▂▂▂▂▂▂▃▃▄▆▇██████▆▅▄▃▂▂▁▂▂▂▂▂▂▂▁▁▁▁▁▁▂▂▂▂▂▂▃▃▇▅▆▇▇▇▆▆▄▄▃▃ ▃
  1.76 ns        Histogram: frequency by time       1.82 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.

The bad one, x == nothing:

julia> @benchmark x == nothing
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
 Range (min … max):  2.419 ns … 10.722 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     2.681 ns              ┊ GC (median):    0.00%
 Time  (mean ± σ):   2.693 ns ±  0.235 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

    █       ▂          ▁▁      ▅▄    ▁
  ▂▄█▂▁▅▄▃▁▆██▂▁▅▇▅▁▂▄▅██▂▅▆▂▁▅██▂▁▂██▃▂▂▂▂▂▂▂▂▁▁▂▄▃▁▁▁▂▃▂▁▁ ▃
  2.42 ns        Histogram: frequency by time        3.11 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.

This is usually why the language server will recommend that you use isnothing or x === nothing instead.

Missing values

The same is generally true of missing values. Missing values differ from nothing in that they are used to represent missing data -- nothing is returned by default when a return value is not otherwise specified. missing is more for cases where you don't know a value, e.g. if you don't have data for an observation in a statistical model.

Interestingly, on Julia 1.10.2, the fastest is not one of the strict comparisons x === missing or ismissing(x), but a raw comparison using ==. Not really sure what's up with that, but whatever.

julia> @benchmark x == missing
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
 Range (min … max):  0.883 ns … 7.668 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     0.890 ns             ┊ GC (median):    0.00%
 Time  (mean ± σ):   0.896 ns ± 0.107 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

         ▁ ▇ ██ ▅ ▂
  ▂▂▁▄▁▆▁█▁█▁██▁█▁█▁▇▁▆▁▅▄▁▄▁▃▁▃▁▃▁▃▃▁▃▁▃▁▂▁▂▁▂▂▁▂▁▂▁▂▁▂▁▂▂ ▃
  0.883 ns       Histogram: frequency by time      0.914 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.
julia> @benchmark ismissing(x)
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
 Range (min … max):  1.757 ns … 5.822 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     1.776 ns             ┊ GC (median):    0.00%
 Time  (mean ± σ):   1.783 ns ± 0.088 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

           ▁▁▂█    ▂█▁▁▁▁
  ▂▂▂▂▂▃▆▆▇████▇▆▅███████▅▂▂▂▂▂▂▂▂▂▂▁▂▂▂▂▂▂▂▂▃▃▃▃▃▃▂▃▃▃▃▃▃▃ ▃
  1.76 ns        Histogram: frequency by time       1.82 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.
julia> @benchmark x === missing
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
 Range (min … max):  1.540 ns … 4.822 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     1.554 ns             ┊ GC (median):    0.00%
 Time  (mean ± σ):   1.561 ns ± 0.081 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

           ▂▃▄▅▇▇█▇▅▄ ▃
  ▂▂▂▃▄▅▅▇███████████▁█▇▅▃▃▂▂▂▂▂▁▁▁▁▁▁▁▁▁▂▂▂▂▂▃▂▃▃▃▃▃▃▃▃▃▃▃ ▄
  1.54 ns        Histogram: frequency by time       1.59 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.
Appearance