Base.precompile appears not to work

I found out about Base.precompile for compiling functions without running, so I tried it out. It doesn’t seem to work though when I put this in the REPL:

> using Plots
> precompile(plot, (typeof(rand(10)),) )  # returns true, I assumed it meant it worked
> @time plot(rand(10)) # ~2s, 3.1M alloc
> @time plot(rand(10)) # ~1.1ms, 2.77k alloc

I think I must be misunderstanding Base.precompile and @ time because the longer timing of the first plot suggests to me that the compilation did not work at the precompile call. Can anyone explain what happened?

As an aside, I wonder why Base.precompile isn’t mentioned more often. It seems like a good way to compile stuff in advance like other static languages.

Precomplation does not fully compile the code only partially (there is a good reason for this, which I don’t know). Thus there is still time needed to fully compile the code. This is what you see. If you want to completely compile everything then you need to create a custom system image, see https://github.com/JuliaLang/PackageCompiler.jl.

You can see the effect of pre-compilation by turning it off:

 >> julia --compiled-modules=no
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.5.2 (2020-09-23)
 _/ |\__'_|_|_|\__'_|  |  
|__/                   |

julia> @time using Plots; @time plot(rand(10)); @time plot(rand(10))
WARNING: using Plots.GR in module Main conflicts with an existing identifier.
 55.193715 seconds (86.28 M allocations: 4.281 GiB, 3.10% gc time)
  3.356667 seconds (4.55 M allocations: 229.180 MiB, 2.77% gc time)
  0.000789 seconds (2.62 k allocations: 160.070 KiB)

vs with precompile

>> julia                      
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.5.2 (2020-09-23)
 _/ |\__'_|_|_|\__'_|  |  
|__/                   |

julia> @time using Plots; @time plot(rand(10)); @time plot(rand(10))
  9.253607 seconds (14.31 M allocations: 856.438 MiB, 3.50% gc time)
  2.318332 seconds (3.22 M allocations: 166.281 MiB, 1.86% gc time)
  0.000696 seconds (2.76 k allocations: 164.273 KiB)

So, it impacts both the load-time during using and the time to execute plot.

Last note that most likely the precompile statement for plot(rand(10)) is already contained within Plots.jl; precompile statements is not something the user needs to do (unless one is missing).

2 Likes

In terms of the bigger picture, precompilation is still an evolving story in Julia. You might be surprised to hear it’s not “done,” but there are good reasons. Historically, Julia’s dynamic development model had one bad side effect: triggering invalidations which reduce the benefit of precompilation. In Julia versions up to and including 1.5, invalidations were so common that it wasn’t obvious that a big push on precompilation was worthwhile. Fortunately, starting in Julia 1.6 (which should enter release cycle soon), there will be relatively few invalidations, and so there’s some hope that in future versions of Julia we’ll make significant advances in precompilation.

That said, there will still be limitations to what precompile can do—most fundamentally, it is limited by type inference, and when inference fails it can’t necessarily do what you’re expecting it to do. See Understanding precompilation and its limitations (reducing latency) for a more detailed discussion.

The good news is that, even as of today, you can start preparing for a brave new world with really good precompilation by starting to analyze packages (ones you develop and/or use) for inference failures, and start fixing what problems you can. If that work is done ahead of time, the effect of advances in precompilation should be immediate and dramatic.

5 Likes