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.toml records your direct dependencies and their compatibility bounds; Manifest.toml records 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 using after 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
RuleInstead ofDo
No non-const globalsx = 0.0 then x += ... in a loopWrap it in a function, or mark it const
Concrete typesVector{Any}, abstract struct fieldsVector{Float64}, concrete fields
Views, not copiesA[1:100] inside a loop@view A[1:100]
Column-major loopsfor i then for j over a MatrixInner loop over the first index
MeasureEyeballing the code@btime before and after each change
Type stabilityFunctions returning different types@code_warntype until nothing is red
Preallocatepush! a million timesVector{T}(undef, n) and index it
Hoist workParsing a string per iterationParse 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.

Multiple dispatch NumPy arrays

Last refreshed 2026-09-18.