Languages No. 110

Julia Scientific Computing skill for Claude Code

Type-stable Julia, measured before and after. Claude writes Julia kernels in functions with concrete types, then proves they are fast with BenchmarkTools, @code_warntype and JET instead of trusting the language's reputation. Projects get their own Project.toml and Manifest.toml, DataFrames work uses transform and combine rather than row loops, and first-call latency is handled with PrecompileTools or a sysimage when users feel it.

  • namejulia-scientific
  • size236 lines · ≈2,700 tokens
  • files2
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Writes fast, reproducible Julia for scientific computing - type-stable functions checked with @code_warntype and JET, no untyped globals, multiple-dispatch designs without type piracy, Project.toml and Manifest.toml environments with Pkg and juliaup, BenchmarkTools measurements, DataFrames.jl and CSV.jl workflows, Makie or Plots, time-to-first-plot fixes with PrecompileTools and sysimages, and interop with Python via PythonCall and C via ccall. Use when writing or optimizing .jl code, when Julia is slower than expected or allocates heavily, when setting up a Julia project or package, or when deciding between Julia and Python with NumPy.

Try asking

  • This Julia simulation is slower than my NumPy version. Find out why
  • Set up a reproducible Julia project for my thesis analysis with tests
  • Plotting takes 40 seconds on first run in Julia. Can we fix the startup time?

Use it when

  • Writing or optimizing .jl numerical code
  • Julia code allocates heavily or shows Any in @code_warntype
  • Setting up Julia environments, packages or CI
  • Deciding whether a workload should move from Python to Julia

Skip it when

How to check the result

pkg> test passes after instantiate on a fresh clone, hot functions show no Any in @code_warntype, and @btime before and after shows the speedup with zero allocations in the kernel.

The common mistake it prevents

Benchmarking the first call or a top-level loop over globals, then concluding Julia is slow when the time is compilation and dynamic dispatch.

~/.claude/skills/julia-scientific/SKILL.md

--- name: julia-scientific description: Writes fast, reproducible Julia for scientific computing - type-stable functions checked with @code_warntype and JET, no untyped globals,… license: MIT ---

Done well means: hot functions are type-stable and allocation-free in their inner loops, proved with @code_warntype and @btime rather than assumed; every project runs from its own Project.toml with a committed Manifest.toml for applications; code is organized around functions and dispatch, not scripts mutating globals; and the first-run latency is known and, where it matters, paid once by precompilation.

Versions

  • Install and switch Julia with juliaup: juliaup add release, juliaup default release, juliaup status. Stable in late 2026 is 1.13.x; the LTS channel (juliaup add lts) has been 1.10. Pin the minimum supported version in [compat] for packages.
  • Start every session in the project: julia --project=. -t auto (-t auto enables threads; Julia starts single-threaded by default).

Ground rules

  • Put performance-critical code in functions. Top-level script code and non-const globals are type-unstable and slow.
  • Never benchmark the first call; it includes compilation. Use BenchmarkTools with $ interpolation for arguments.
  • Struct fields must have concrete types (or type parameters). x::Real or an untyped field makes every access dynamic.
  • Do not add methods to functions you do not own for types you do not own (type piracy). It changes behavior for every package loaded in the session.
  • Arrays are column-major: the first index varies fastest. Loop for j in axes(A,2), i in axes(A,1).
  • Prove before using @inbounds or @fastmath; both trade safety or IEEE semantics for speed. Run tests with --check-bounds=yes regardless.

Workflow

- [ ] 1. Set up the environment (Project.toml, Manifest.toml, Revise)
- [ ] 2. Write it plainly in functions, with tests
- [ ] 3. Measure: @btime / @benchmark on realistic sizes
- [ ] 4. Check type stability: @code_warntype, JET @report_opt
- [ ] 5. Remove allocations in hot loops; profile the rest
- [ ] 6. Parallelize only after the serial version is efficient
- [ ] 7. Reduce latency (precompile workloads, sysimage) if users feel it

1. Environment

julia> ]                      # Pkg REPL
(@v1.13) pkg> activate .      # creates/uses ./Project.toml
(proj) pkg> add DataFrames CSV CairoMakie BenchmarkTools
(proj) pkg> instantiate       # on a fresh clone: install exactly what Manifest.toml records
(proj) pkg> status --outdated
Project typeCommit Manifest.toml?Compat
Analysis, paper, application, serviceYes (exact reproducibility)Optional
Registered package / libraryNoRequired [compat] for every dependency and julia
  • Keep the global environment (@v1.x) to dev tools only: Revise, BenchmarkTools, JET, maybe OhMyREPL. Everything else goes in the project.
  • using Revise before loading your code so edits reload without restarting.
  • Structure non-trivial work as a package (pkg> generate MyAnalysis) with src/ and test/runtests.jl; scripts then just using MyAnalysis.

3. Measure

using BenchmarkTools
x = rand(10^6)
@btime mysum($x)              # min time and allocations; $ avoids global-variable overhead
@benchmark mysum($x)          # distribution (median, GC time)
@allocated mysum(x)           # bytes for one call (after warm-up)

Targets: an inner kernel over plain arrays should report 0 allocations. A loop that allocates per iteration is the most common 10-100x slowdown.

4. Type stability

@code_warntype f(x)           # red Any / Union{...} in Body or variables = instability
using JET
@report_opt f(x)              # lists runtime dispatch sites through the call tree

Common causes and fixes are in references/performance-patterns.md; read it when @code_warntype shows Any or @btime shows unexpected allocations.

Quick rules:

  • A variable must keep one type: s = 0 then s += 0.5 makes s a Union. Use s = zero(eltype(x)).
  • Return the same type on every branch (nothing vs a number is a Union; small Unions are fine, Any is not).
  • Containers: Float64[], not [] (which is Vector{Any}).
  • Closures capturing a variable that is reassigned get boxed (Core.Box); assign once or use let.
  • Function barriers: when data arrives with an unknown type (parsed file, Dict{String,Any}), pass it to a separate kernel function; dispatch specializes the kernel once.

5. Allocations and profiling

  • Preallocate outputs and write mutating versions: mul!(C, A, B), f!(out, x).
  • Fuse broadcasts with dots: @. y = a * x + b makes one loop, no temporaries.
  • Slices copy: A[:, j] allocates; use @views or view(A, :, j).
  • Small fixed-size vectors (3D points, 4x4 matrices): StaticArrays SVector/SMatrix.
  • Profile: using Profile; @profile run(); Profile.print(format=:flat, sortedby=:count), or @profview in the VS Code Julia extension for a flame graph.

6. Parallelism

Work shapeTool
Independent loop iterations, shared memoryThreads.@threads or OhMyThreads.tmap with -t auto
Task-based or uneven workThreads.@spawn + fetch
Multiple machines or processesDistributed (addprocs, pmap)
Dense linear algebraAlready multithreaded via BLAS; check BLAS.get_num_threads() and avoid oversubscription with Julia threads
GPUCUDA.jl / AMDGPU.jl / Metal.jl with broadcast or KernelAbstractions

Never write to shared accumulators from threads without a reduction or per-task buffers; Threads.threadid()-indexed buffers are unsafe because tasks can migrate.

7. Latency (time to first X)

ProblemFix
Your package’s first call is slow for usersPrecompileTools.@compile_workload with a representative workload inside the package module
Your own scripts load many packages slowlyA local Startup package that usings them and runs a small @compile_workload
CLI tool or deployment must start in well under a secondPackageCompiler create_sysimage with a precompile execution file, or experimental juliac --trim (1.12+) for small static-dispatch programs
Iterating interactivelyKeep one session alive with Revise; do not restart
using PrecompileTools: @setup_workload, @compile_workload
@setup_workload begin
    data = rand(100, 3)
    @compile_workload begin
        fit_model(data)
    end
end

Invalidations (using SnoopCompileCore; invalidations = @snoop_invalidations using X) undo precompilation; type piracy and overly generic methods are the usual cause.

Dispatch design

  • Define behavior on abstract types and functions, data in concrete structs: abstract type Kernel end; struct Gaussian <: Kernel; σ::Float64; end; evaluate(k::Gaussian, r) = exp(-r^2 / (2k.σ^2)).
  • Parametric structs for generic numerics: struct Particle{T<:AbstractFloat}; x::T; v::T; end.
  • Accept abstract argument types (AbstractVector{<:Real}) for generality; type annotations on arguments do not make code faster, concrete field and container types do.
  • Extend Base generics (Base.length, Base.getindex, Base.show) for your own types to plug into the ecosystem.

DataFrames and plotting

using CSV, DataFrames, Statistics
df = CSV.read("runs.csv", DataFrame; types=Dict(:id => String), missingstring="NA")
clean = dropmissing(df, :energy)
summary = combine(groupby(clean, :material), :energy => mean => :mean_E, nrow => :n)
transform!(clean, [:mass, :v] => ByRow((m, v) -> 0.5m * v^2) => :kinetic)
  • Use select/transform/combine with source => function => target; row loops over a DataFrame are type-unstable because columns have heterogeneous types.
  • For a hot loop over columns, extract them first (x = df.x) and pass to a kernel.
Plotting needChoose
Publication figures, PDF/SVG/PNGCairoMakie
Interactive 3D or large dataGLMakie (needs OpenGL)
Quick plots in notebooks with many backendsPlots.jl

Save explicitly: save("fig.pdf", fig) (Makie) or savefig("fig.pdf") (Plots).

Interop

using PythonCall                      # Python from Julia; deps recorded in CondaPkg.toml
np = pyimport("numpy")
v = pyconvert(Vector{Float64}, np.linspace(0, 1, 5))
a = PyArray(np.zeros(10))             # no-copy view of a NumPy buffer
  • Add Python packages with pkg> conda add scipy (after using CondaPkg) and commit CondaPkg.toml. To use an existing interpreter instead, set JULIA_CONDAPKG_BACKEND=Null and JULIA_PYTHONCALL_EXE=/path/to/python.
  • From Python, juliacall exposes Julia (from juliacall import Main as jl).
  • C and Fortran: @ccall libm.cos(1.0::Cdouble)::Cdouble; pass arrays as Ptr{Cdouble} and keep them rooted with GC.@preserve while C holds the pointer.

Julia vs Python + NumPy

SituationBetter choice
Custom loops that do not vectorize (ODE right-hand sides, Monte Carlo with branching, agent models, custom kernels)Julia: loops compile to native code
Differential equations, optimization, AD-heavy modelsJulia (DifferentialEquations.jl, JuMP, Enzyme/ForwardDiff) is a strong default
Workload is already vectorized NumPy/BLAS callsRoughly equal; switching gains little
Deep learning training, cloud ML services, broad library needsPython (PyTorch/JAX ecosystem)
Short scripts run once, cold start matters, team only knows PythonPython; Julia’s compile latency dominates
Mixed teamPython front end calling Julia kernels via juliacall, or the reverse

Do not claim speedups without a benchmark of both versions on the user’s data size.

Gotchas

  • Integer overflow wraps silently: 2^64 == 0. Use big(2)^64 or Int128 when ranges can exceed Int64.
  • 1:n with n = 0 is empty, and arrays are 1-based; eachindex(A) and axes(A, d) are correct for offset arrays too.
  • a = b for arrays aliases; use copy(b).
  • == vs isequal/===: NaN == NaN is false; missing == 1 is missing.
  • Precompilation can fail silently in a broken environment; run pkg> precompile and read the errors.
  • Threads.@threads with a shared push! is a data race; it may “work” on small inputs.

Verification

  1. pkg> test passes from a fresh clone after instantiate.
  2. @code_warntype on each hot function shows no Any; @report_opt reports nothing in your code.
  3. @btime before and after, at the target data size, with allocations; report both.
  4. Numerical results match the previous implementation within a stated tolerance (isapprox(a, b; rtol=1e-10)), including on edge-case inputs.

If speed did not improve, profile before guessing; the bottleneck is often I/O, a DataFrame row loop, or a type-unstable function barrier one level up.

Bundled files

Loaded only when a step in the skill needs them. Review them here before installing.

references/performance-patterns.md 157 lines · 4.7 KB
# Julia performance patterns: diagnosis and fixes

Read this when `@code_warntype` shows `Any`/`Union` types, `@btime` reports allocations
in a kernel that should allocate nothing, or a function is far slower than the
equivalent C. Each entry: symptom, cause, fix.

## 1. Non-const global

```julia
# slow: `scale` is an untyped global; every use is a dynamic lookup
scale = 2.0
f(x) = scale * x

# fix A: make it const (value may change, type may not)
const SCALE = 2.0
# fix B: typed global (1.8+), still slower than passing it
scale::Float64 = 2.0
# fix C (best): pass it in
f(x, scale) = scale * x
```

Symptom: `@code_warntype f(1.0)` shows `Main.scale::Any`.

## 2. Accumulator changes type

```julia
function total(v)
    s = 0            # Int
    for x in v
        s += x       # becomes Float64 when v is Vector{Float64}: s::Union{Int,Float64}
    end
    s
end
# fix
function total(v)
    s = zero(eltype(v))
    for x in v
        s += x
    end
    s
end
```

## 3. Abstract or untyped struct fields

```julia
struct Bad;  data; scale::Real; end              # every access dynamic
struct Good{T<:AbstractFloat, V<:AbstractVector{T}}
    data::V
    scale::T
end
```

Check with `isconcretetype(fieldtype(Good{Float64,Vector{Float64}}, :data))`.
Containers: `Vector{Real}` and `Dict{String,Any}` are abstract-element containers; each
element is boxed.

## 4. Type-unstable return

```julia
findfirst_pos(v) = (i = findfirst(>(0), v); i === nothing ? -1 : i)   # stable: always Int
safe_div(a, b) = b == 0 ? nothing : a / b                            # Union{Nothing,Float64}: OK, small union
parse_any(s) = occursin('.', s) ? parse(Float64, s) : parse(Int, s)  # Union, spreads to callers
```

Small unions with `nothing`/`missing` are optimized by the compiler; unions that flow
into heavy arithmetic are not. Pick one output type.

## 5. Captured variable boxed in a closure

```julia
function g(xs)
    a = 1
    a = 2                       # reassignment makes `a` a Core.Box in the closure below
    map(x -> x + a, xs)
end
# fix
function g(xs)
    a = 2
    let a = a
        map(x -> x + a, xs)
    end
end
```

Symptom: `@code_warntype` shows `Core.Box`.

## 6. Function barrier for dynamic data

```julia
function process(path)
    cfg = JSON3.read(read(path, String))       # fields have unknown types at compile time
    n = Int(cfg.n); dt = Float64(cfg.dt)       # convert at the boundary
    simulate(n, dt)                            # kernel gets concrete types, compiles once
end
```

## 7. Allocation per iteration

| Pattern | Fix |
|---|---|
| `y = A * x` inside a loop | `mul!(y, A, x)` into a preallocated `y` |
| `v[i:j]` slices | `@views` on the block or `view(v, i:j)` |
| `y = a .* x .+ b` creating a new array each time | `y .= a .* x .+ b` or `@. y = a * x + b` |
| `push!` in a hot loop with known length | `Vector{T}(undef, n)` then index |
| Small vectors `[x, y, z]` | `SVector(x, y, z)` from StaticArrays |
| String building with `*` | `IOBuffer` + `print`, or `join` |

Measure each fix: `@btime` should show the allocation count drop.

## 8. Memory order

```julia
# slow: inner loop walks rows (stride n)
for i in 1:n, j in 1:n; A[i, j] = f(i, j); end      # j varies fastest here
# fast: inner loop walks columns (contiguous)
for j in axes(A, 2), i in axes(A, 1); A[i, j] = f(i, j); end
```

Julia's `for a in X, b in Y` nests `b` innermost.

## 9. Keyword arguments and splatting

Keyword arguments with abstract defaults and `f(args...)` with heterogeneous tuples can
defeat specialization in hot paths. Keep kernels positional with concrete types; use
keywords in the outer API.

## 10. Functions as arguments

Julia specializes on function arguments when they are called, but not when only passed
through. If a higher-order function only forwards `f`, add a type parameter:
`apply(f::F, x) where {F} = inner(f, x)`.

## Profiling flow

```julia
using Profile
f(x)                                    # warm up (compile)
Profile.clear()
@profile for _ in 1:100; f(x); end
Profile.print(format=:flat, sortedby=:count, mincount=50)
```

- In VS Code (or with ProfileView.jl), `@profview f(x)` shows a flame graph that
  color-highlights frames with runtime dispatch and garbage collection; start there.
- `@time` reports compilation percentage; if most time is compilation, the problem is
  latency (precompile), not throughput.
- `Cthulhu.@descend f(x)` walks the inferred call tree interactively to find where a
  type first becomes `Any`.

## Checklist before declaring a kernel optimized

- [ ] `@code_warntype` clean (no `Any`, no `Core.Box`)
- [ ] `@btime` shows 0 allocations for the inner kernel
- [ ] Loops follow column-major order
- [ ] Results unchanged (`isapprox` with a stated tolerance) on edge cases
- [ ] Benchmarks recorded before and after, same machine, same thread count

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc