Suspicious allocations using Revise

I am using Revise.jl to work on a package for my research, and am noticing that “revising” a file to identical code with different variable names causes a function to stop allocating.

I am not sure how many details others will need to help me diagnose the issue, but my workflow is as follows:

  1. Run a script using Julia and the -i flag. startup.jl consists only of using Revise.
  2. The script loads some data files, as well as BenchmarkTools.jl and Plots.jl
  3. Benchmark some functions that need to be allocation-free (for performance reasons).

The offending function is:

function is_cell_contained_by(cell1, poly)
    cell_poly = cell_boundary_polygon(cell1) # this is an SVector
    return all(edge_starts(cell_poly)) do pt # edge_starts returns an SVector
        return PlanePolygons.point_inside_strict(poly, pt) # this does not allocate
    end
end

And benchmarking it:

julia> @benchmark Euler2D.is_cell_contained_by($test_cell, $test_poly)
BenchmarkTools.Trial: 10000 samples with 709 evaluations per sample.
 Range (min … max):  180.385 ns …  51.612 μs  ┊ GC (min … max): 0.00% … 99.40%
 Time  (median):     194.587 ns               ┊ GC (median):    0.00%
 Time  (mean ± σ):   237.031 ns ± 556.060 ns  ┊ GC (mean ± σ):  8.90% ±  8.09%

   ▃▇█▅▄▁                           ▂▂▂▃▃▂▂▁▁                   ▂
  ███████████▇▇▆▆▆▆▄▃▅▃▁▁▄▄▁▄▁▃▄▁▁▁▇██████████▇▆▆▃▄▃▁▁▁▃▄▁▃▃▄▃▄ █
  180 ns        Histogram: log(frequency) by time        437 ns <

 Memory estimate: 464 bytes, allocs estimate: 5.

Well, that’s odd. Edit the file defining is_cell_contained_by(...) and let Revise.jl do its magic. I change the function to:

function is_cell_contained_by(cell1, poly)
    cell_polys = cell_boundary_polygon(cell1)
    return all(edge_starts(cell_polys)) do pt
        return PlanePolygons.point_inside_strict(poly, pt)
    end
end

Notice that only the variable name has changed: cell_polycell_polys. Benchmark it again:

julia> @benchmark Euler2D.is_cell_contained_by($test_cell, $test_poly)
BenchmarkTools.Trial: 10000 samples with 985 evaluations per sample.
 Range (min … max):  53.646 ns … 108.297 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     57.632 ns               ┊ GC (median):    0.00%
 Time  (mean ± σ):   57.934 ns ±   2.129 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

                  ▂▁▁▁▄▆█▇▇▅▁▁  ▁        ▁▁▂▁ ▁▁ ▂▁            ▂
  ▄█▆▇▃▃▁▁▁▆▇▃▅▇▅▇█████████████████▆▆▅▄▆▇███████████▇▆▆▆▅▄▅▆▆▆ █
  53.6 ns       Histogram: log(frequency) by time      63.8 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.

This is the behavior I expect… but I do not know why I have to force Revise.jl to pick up on the new function every time I run the script.

Is there a cache I can clear somewhere? Should I kill every running Julia process on my machine and try again? I am absolutely baffled, and I hope that I’m just falling into a common Revise.jl trap, rather than doing something truly stupid.

I did try:

  1. removing the ~/.julia/compiled/ to force everything to be compiled again. The behavior persists.
  2. running the script and the benchmark without Revise loaded, which yields only the allocating version of the benchmark with no way to trigger optimization.

How exactly? Revise isn’t documented to track input scripts at command lines e.g. julia -i script.jl.

Disappearing allocations like that is bizarre though, I’d want to know if there’s some unstable compiler optimization going on. However, that’s impossible to tell from the outside without the source code (everything, startup.jl, the script, the packages, the benchmark) and a reproducible process.