Packages, environments and performance
Reproducible project environments with Pkg, and the handful of rules that separate fast Julia from slow Julia.
Project environments
using Pkg
Pkg.activate(".") # use the environment defined by Project.toml
Pkg.add("DataFrames")
Pkg.add(name = "CSV", version = "0.10")
Pkg.status()
Pkg.instantiate() # install exactly the versions in Manifest.toml
Pkg.resolve()Project.tomlrecords your direct dependencies and their compatibility bounds;Manifest.tomlrecords the exact resolved versions of everything, direct and transitive.- Commit both files. The manifest is what makes a build reproducible on another machine or in CI.
Pkg.activate(".")pins the current session to the project; without it, packages install into a shared global environment and two projects can silently interfere.- Add compatibility bounds —
Pkg.compat("DataFrames", "1.6")— so a breaking release cannot be pulled in by an unrelated install. - The first call to
usingafter an install compiles the package, which is why the very first import is slow and every later one is not.
Pkg.compat("DataFrames", "1.6")
Pkg.status(; outdated = true)What actually makes it fast
using BenchmarkTools
# slow: a non-const global in a loop, so the compiler cannot know the type
total = 0.0
for i in 1:1_000_000
global total += sqrt(i)
end
# fast: the same work inside a function, with concrete types throughout
function total_sqrt(n)
acc = 0.0
for i in 1:n
acc += sqrt(i)
end
return acc
end
@time total_sqrt(1_000_000) # seconds and allocation count
@allocated total_sqrt(1_000_000) # 0 for this loop; anything else is a clue
@btime total_sqrt(1_000_000) # runs it many times, reports a stable estimate
@code_warntype total_sqrt(10) # red means type-unstable: fix that line first| Rule | Instead of | Do |
|---|---|---|
| No non-const globals | x = 0.0 then x += ... in a loop | Wrap it in a function, or mark it const |
| Concrete types | Vector{Any}, abstract struct fields | Vector{Float64}, concrete fields |
| Views, not copies | A[1:100] inside a loop | @view A[1:100] |
| Column-major loops | for i then for j over a Matrix | Inner loop over the first index |
| Measure | Eyeballing the code | @btime before and after each change |
| Type stability | Functions returning different types | @code_warntype until nothing is red |
| Preallocate | push! a million times | Vector{T}(undef, n) and index it |
| Hoist work | Parsing a string per iteration | Parse once, outside the loop |
⚠️
Type instability is the number one cause of "Julia is as slow as Python". If
@code_warntype shows an ::Any in the middle of a hot function, every operation on that value is a dynamic dispatch. Fix the instability before micro-optimising anything else, and confirm with @btime — impressionistic timing at this scale is worthless.FAQ
Why is the first plot or import so slow?
Julia compiles specialised machine code the first time a method is called with a given set of types. That latency is real but one-off; later calls, and calls from other functions, reuse the compiled version.
Do I need to restart Julia after adding a package?
Not usually —
Pkg.add then using works in the same session. Restart if the load seems stale after a version change, and prefer one project per session over switching environments mid-work.Related
Multiple dispatch NumPy arrays
Last refreshed 2026-09-18.