Skip to content

FunctionWrappers (AutoSpecialize) cause false positive allocation reports in check_allocs #101

@singhharsh1708

Description

@singhharsh1708

Summary

When using check_allocs on functions that involve FunctionWrappers.jl (as used by SciML's AutoSpecialize default in ODEProblem), the static analysis reports spurious allocation sites that do not correspond to actual runtime allocations. Switching to FullSpecialize (which avoids FunctionWrappers) eliminates all reported allocations.

Minimum Working Example

using AllocCheck
using OrdinaryDiffEqTsit5, OrdinaryDiffEqCore
using SciMLBase: FullSpecialize

f!(du, u, p, t) = (du .= -u)

# --- AutoSpecialize (default): FunctionWrappers cause false positives ---
prob_auto = ODEProblem(f!, [1.0, 1.0], (0.0, 1.0))  # uses AutoSpecialize by default
integ_auto = init(prob_auto, Tsit5(), dt = 0.1, save_everystep = false,
                  abstol = 1e-6, reltol = 1e-6)
step!(integ_auto)

allocs_auto = check_allocs(OrdinaryDiffEqCore.perform_step!,
                           (typeof(integ_auto), typeof(integ_auto.cache)))
println("AutoSpecialize: $(length(allocs_auto)) allocation sites reported")
# Prints: AutoSpecialize: N allocation sites reported  (N > 0)

# --- FullSpecialize: no FunctionWrappers, correct result ---
prob_full = ODEProblem{true, FullSpecialize}(f!, [1.0, 1.0], (0.0, 1.0))
integ_full = init(prob_full, Tsit5(), dt = 0.1, save_everystep = false,
                  abstol = 1e-6, reltol = 1e-6)
step!(integ_full)

allocs_full = check_allocs(OrdinaryDiffEqCore.perform_step!,
                           (typeof(integ_full), typeof(integ_full.cache)))
println("FullSpecialize: $(length(allocs_full)) allocation sites reported")
# Prints: FullSpecialize: 0 allocation sites reported  ✓

Expected behavior

Both should report 0 allocations since perform_step! does not allocate at runtime in either case. You can verify with @allocated:

step!(integ_auto)
step!(integ_auto)
println(@allocated step!(integ_auto))  # 0 bytes

Root cause

AutoSpecialize wraps the user-provided ODE function in FunctionWrappers.jl to reduce compilation overhead. FunctionWrappers uses dynamic dispatch internally (via function pointers), which AllocCheck's static LLVM analysis cannot fully resolve. It flags the unresolvable call sites as potential allocations even though they don't allocate at runtime.

Context

This was discovered while fixing allocation tests in SciML/OrdinaryDiffEq.jl#3359. The workaround we adopted is to always use FullSpecialize in AllocCheck-based tests, but it would be helpful if this limitation were documented (or if check_allocs could detect/skip FunctionWrappers call sites).

Possible resolutions

  1. Documentation: Add a note in the check_allocs / @check_allocs docs warning that FunctionWrappers-wrapped callables may produce false positives.
  2. Detection: Optionally detect when a reported allocation site originates from inside FunctionWrappers.jl and warn the user rather than flagging it as an allocation.
  3. Issue Re-enable fast ABI (on top of TypedCallable / OpaqueClosures?) #85 relevance: This may be related to the "Re-enable fast ABI" issue — if FunctionWrappers used a faster non-allocating ABI, this wouldn't be a concern.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions