Nothingness in Julia

April 3, 2024

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.