Debugging, testing and benchmarking Julia code

Write test sets, benchmark honestly with BenchmarkTools, find allocations, and trace a running function with the debugger.

Testing with Test.jl

using Test

@testset "Geometry" begin
    @test area(Circle(1.0)) ≈ pi
    @test perimeter(Circle(2.0)) == 4 * pi
    @test_throws MethodError Circle("not a number")

    @testset "edge cases" begin
        @test isnan(area(Circle(NaN)))
        @test area(Circle(0.0)) == 0.0
    end
end

# approximate comparison needs the ≈ operator, not ==, for floating point
@test 0.1 + 0.2 ≈ 0.3
@test 0.1 + 0.2 == 0.3            # fails

# run the suite for a package
# julia --project=. -e 'using Pkg; Pkg.test()'
  • @testset groups results and reports which ones failed, and nested sets give you a readable hierarchy.
  • Use (typed as \approx then tab) for floating-point equality, optionally with atol and rtol.
  • @test_broken documents a known failure and turns into a failure if it starts passing, which is a useful reminder.

Benchmarking with BenchmarkTools

using BenchmarkTools

f(x) = sum(abs2, x)

x = rand(1000)

@btime f(x)                 # multiple samples, reports the minimum
@benchmark f($x)            # never use a global name without $
@benchmark f($x) samples  = 1000 evals = 1

# inspect allocations
@ballocated f($x)
b = @benchmark f($x)
b.memory                     # bytes allocated across the batch
b.allocs

# type stability, the usual cause of slowness
@code_warntype f(x)
@code_typed f(x)
SymptomLikely cause
Many allocations in a tight loopBoxing, type instability, or a temporary array
Performance grows with input sizeA copy inside the function, or a non-constant global
First call slow, later fastCompilation, not the algorithm
Type shown as a union or AnyAn abstractly typed field or an untyped container
⚠️
A benchmark that reads global variables measures dynamic dispatch rather than your algorithm, and one that omits the $ interpolation prints a warning for exactly that reason. Always interpolate inputs with $, and treat the first call as compilation rather than execution.

Debugging

using Debugger

@enter f(rand(10))          # step into the call, then use the REPL commands
#   n         step to the next line
#   s         step into the next call
#   bt        show the backtrace
#   fr 2      switch to frame 2
#   q         quit

@run f(rand(10))            # run until an error and stop there

using Infiltrator
function g(x)
    @infiltrate x < 0       # drops into a REPL only when the condition holds
    sqrt(abs(x))
end
  1. Reproduce the failure as a small script, not through the full pipeline.
  2. Print types, not just values: typeof(x) and @code_warntype answer most performance questions.
  3. Check method existence with methods(f) and @which when dispatch picks something unexpected.
  4. Reach for the debugger only after printing has failed to reveal the state.

FAQ

Why is @btime faster than the code in my program?
The benchmark warms up, runs the function many times and reports the fastest sample, so it measures the compiled steady state. Real workloads include dispatch, allocation and I/O that the benchmark excludes.
How do I know whether my code is type stable?
Run @code_warntype f(args) and look for red entries such as Any or Union in the return type. Fix abstract field types and non-constant globals first; those cause most of the instability seen in practice.

Packages, environments and performance Modules and organising a Julia project

Last refreshed 2026-09-18.