# How to understand reasons for some unexpected heap allocations

**URL:** <https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645>\
**Category:** Performance\
**Tags:** memory-allocation\
**Created:** [June 15, 2026, 9:06pm UTC](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645 "2026-06-15T21:06:57Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![user664303](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/user664303/32/37843_2.png) [@user664303](https://discourse.julialang.org/u/user664303)\
**Post date:** [June 15, 2026, 9:06pm UTC](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645/1 "2026-06-15T21:06:57Z")

</div>

I’m really surprised by some heap allocations being made within a package I maintain. I don’t expect them because:

1. I’ve use [Cthulhu](https://github.com/JuliaDebug/Cthulhu.jl) to ensure that all variables in the code are concretely typed.
2. The variables in question are all fairly small (192 bytes max!), including one Int64.
3. Some of the variables are mutable structs, but none of these escape the function in which they’re created.
4. Several of the variables are immutable return values, and annotating the return type of the relevant functions makes no difference; they’re concretely typed anyway.

As well as using Cthulhu, I’ve used the [allocation profiler](https://docs.julialang.org/en/v1/manual/profile/#allocation-profiler) to see where and what the allocations are, but given what I said above, I’m unable to understand why the allocations are occurring (in Julia Version 1.12.6). It’s my belief that I should be able to avoid them, and that the code should (in principle) require no heap allocations at all.

Here is a screenshot of the allocations being made:

 ![Screenshot 2026-06-15 at 21.32.40](https://global.discourse-cdn.com/julialang/original/3X/f/6/f6ab2a91afd52d03560b031f20d99f50633ddb56.png)  
Does anyone have any further tips for understanding the cause of such allocations?

If anyone wants to recreate the problem, you can do so with the following code, which should (using latest NLLSsolver, v4.0.5) produce the same graph as above:

```julia-auto
using NLLSsolver, Random, Static, StaticArrays, LinearAlgebra, Profile, PProf

# Simple affine projection transform
NLLSsolver.generatemeasurement(pose::EuclideanVector{6, T}, X::EuclideanVector{3, U}) where {T, U} = SVector(dot(@inbounds(view(pose, NLLSsolver.SR(1, 3))), X), dot(@inbounds(view(pose, NLLSsolver.SR(4, 6))), X))
MyResType = SimpleError2{2, Float64, EuclideanVector{6, Float64}, EuclideanVector{3, Float64}}

function create_ba_problem(ncameras, nlandmarks, propvisible)
    problem = NLLSProblem(Union{EuclideanVector{6, Float64}, EuclideanVector{3, Float64}}, MyResType)

    # Generate the cameras on a unit sphere, pointing to the origin
    camoffset = SVector(1.0, 0.0, 0.0, 0.0, 1.0, 0.0)
    for i = 1:ncameras
        addvariable!(problem, randn(EuclideanVector{6, Float64}) .+ camoffset)
    end
    
    # Generate the landmarks in a unit cube centered on the origin
    lmoffset = SVector(-0.5, -0.5, 10.0)
    for i = 1:nlandmarks
        addvariable!(problem, rand(EuclideanVector{3, Float64}) .+ lmoffset)
    end

    # Generate the measurements
    visibility = abs.(repeat(vec(1:ncameras), outer=(1, nlandmarks)) .- LinRange(2, ncameras-1, nlandmarks)')
    visibility = visibility .<= sort(vec(visibility))[Int(ceil(length(visibility)*propvisible))]
    for camind = 1:ncameras
        camera = problem.variables[camind]::EuclideanVector{6, Float64}
        for (landmark, tf) in enumerate(view(visibility, camind, :)')
            if tf
                landmarkind = landmark + ncameras
                addcost!(problem, SimpleError2{EuclideanVector{6, Float64}, EuclideanVector{3, Float64}}(generatemeasurement(camera, problem.variables[landmarkind]::EuclideanVector{3, Float64}), camind, landmarkind))
            end
        end
    end

    # Return the NLLSProblem
    return problem
end

function perturb_ba_problem(problem, pointnoise, posenoise)
    for ind in 1:lastindex(problem.variables)
        if isa(problem.variables[ind], EuclideanVector{3, Float64})
            problem.variables[ind]::EuclideanVector{3, Float64} += randn(SVector{3, Float64}) * pointnoise
        else
            problem.variables[ind]::EuclideanVector{6, Float64} += randn(SVector{6, Float64}) * posenoise
        end
    end
    return problem
end

Random.seed!(1)
problem = create_ba_problem(3, 1, 1.0)
problem = perturb_ba_problem(problem, 0.003, 0.0)   
options = NLLSOptions(numthreads = static(1))
function myfun(problem, options)
    result = optimize!(problem, options, 4)
    problem = perturb_ba_problem(problem, 0.003, 0.0)
    Profile.clear_malloc_data()
    Profile.Allocs.clear()
    Profile.Allocs.@profile sample_rate=1.0 optimize!(problem, options, 4)
end
myfun(problem, options)
PProf.Allocs.pprof(from_c = false)

```

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [June 15, 2026, 11:56pm UTC](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645/2 "2026-06-15T23:56:32Z")

</div>

> [@user664303](#):
>
> 1. Some of the variables are mutable structs, but none of these escape the function in which they’re created.

I’d be interested in tooling that exposes escape analysis because proving bounded lifetimes is apparently not as simple as what we intend. AllocCheck.jl is nice but it doesn’t say much.

---

<div class="post-metadata">

**Author:** ![matthias314](https://avatars.discourse-cdn.com/v4/letter/m/a88e4f/32.png) [@matthias314](https://discourse.julialang.org/u/matthias314)\
**Post date:** [June 16, 2026, 12:28am UTC](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645/3 "2026-06-16T00:28:19Z")

</div>

> [@user664303](#):
>
> 1. Some of the variables are mutable structs, but none of these escape the function in which they’re created.

In my experience, one also needs that all functions that are called with the mutable struct as an argument are inlined. Do you have that?

EDIT: Also, are you sure that no mutable struct escapes? For example, one `MVector{3,Float64}` is created in line `@NLLSsolver/src/linearsystem.jl:71` as part of the struct `UniVariateLSstatic_x{3}` and then returned to line `@NLLSsolver/src/linearsystem.jl:25`. Ditto for the other one and the `MMatrix{3,3,Float64,9}`, both of which are created in line `@NLLSsolver/src/linearsystem.jl:63` and returned to `@NLLSsolver/src/linearsystem.jl:25`.

---

<div class="post-metadata">

**Author:** ![matthias314](https://avatars.discourse-cdn.com/v4/letter/m/a88e4f/32.png) [@matthias314](https://discourse.julialang.org/u/matthias314)\
**Post date:** [June 16, 2026, 2:16am UTC](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645/4 "2026-06-16T02:16:06Z")

</div>

JET.jl reports a possible type error exactly at the location of at least some of the allocations (`@NLLSsolver/src/optimize.jl:117`)

> ****
>
> ```julia-auto
> julia> using JET, NLLSsolver
> 
> julia> @report_call optimize!(problem, options, 4)
> [ Info: tracking NLLSsolver
> [ Info: tracking Base
> ═════ 1 possible error found ═════
> ┌ optimize!(problem::NLLSProblem{…}, options::NLLSOptions{…}, unfixed::Int64) @ NLLSsolver /mnt/data/git/NLLSsolver.jl/src/optimize.jl:2
> │┌ optimize!(problem::NLLSProblem{…}, options::NLLSOptions{…}, unfixed::Int64, callback::typeof(nullcallback)) @ NLLSsolver /mnt/data/git/NLLSsolver.jl/src/optimize.jl:2
> ││┌ setupsinglevarls(func::typeof(NLLSsolver.optimizeinternal!), problem::NLLSProblem{…}, options::NLLSOptions{…}, unfixed::Int64, startstats::NLLSsolver.Stats, trailingargs::typeof(nullcallback)) @ NLLSsolver /mnt/data/git/NLLSsolver.jl/src/optimize.jl:108
> │││┌ setupstaticvarls(func::typeof(NLLSsolver.optimizeinternal!), problem::NLLSProblem{…}, options::NLLSOptions{…}, unfixed::Int64, startstats::NLLSsolver.Stats, varlen::StaticInt{…}, trailingargs::Tuple{…}) @ NLLSsolver /mnt/data/git/NLLSsolver.jl/src/optimize.jl:117
> ││││┌ optimizeinternal!(problem::NLLSProblem{…}, options::NLLSOptions{…}, data::NLLSsolver.NLLSInternal{…}, iteratedata::NLLSsolver.LevMarData, callback::typeof(nullcallback)) @ NLLSsolver /mnt/data/git/NLLSsolver.jl/src/optimize.jl:228
> │││││┌ optimizeloop!(problem::NLLSProblem{…}, options::NLLSOptions{…}, data::NLLSsolver.NLLSInternal{…}, iteratedata::NLLSsolver.LevMarData, callback::typeof(nullcallback)) @ NLLSsolver /mnt/data/git/NLLSsolver.jl/src/optimize.jl:154
> ││││││┌ costgradhess!(linsystem::NLLSsolver.LinearSystem{…}, vars::Vector{…}, costs::NLLSsolver.VectorRepo{…}) @ NLLSsolver /mnt/data/git/NLLSsolver.jl/src/cost.jl:55
> │││││││┌ kwcall(::@NamedTuple{…}, ::typeof(sum), fun::NLLSsolver.Bind{…}, vr::NLLSsolver.VectorRepo{…}) @ NLLSsolver /mnt/data/git/NLLSsolver.jl/src/VectorRepo.jl:54
> ││││││││┌ sum(fun::NLLSsolver.Bind{…}, vr::NLLSsolver.VectorRepo{…}; kw::@Kwargs{…}) @ NLLSsolver /mnt/data/git/NLLSsolver.jl/src/VectorRepo.jl:54
> │││││││││┌ vrsum(fun::NLLSsolver.Bind{…}, vr::NLLSsolver.VectorRepo{…}, kw::@Kwargs{…}, T::Type{…}) @ NLLSsolver /mnt/data/git/NLLSsolver.jl/src/VectorRepo.jl:56
> ││││││││││┌ vrsum(fun::NLLSsolver.Bind{…}, kw::@Kwargs{…}, v::Vector{…}) @ NLLSsolver /mnt/data/git/NLLSsolver.jl/src/VectorRepo.jl:52
> │││││││││││┌ kwcall(::@NamedTuple{…}, ::typeof(sum), f::NLLSsolver.Bind{…}, a::Vector{…}) @ Base ./reducedim.jl:980
> ││││││││││││┌ sum(f::NLLSsolver.Bind{…}, a::Vector{…}; dims::Colon, kw::@Kwargs{…}) @ Base ./reducedim.jl:980
> │││││││││││││┌ kwcall(::@NamedTuple{…}, ::typeof(Base._sum), f::NLLSsolver.Bind{…}, a::Vector{…}, ::Colon) @ Base ./reducedim.jl:984
> ││││││││││││││┌ _sum(f::NLLSsolver.Bind{…}, a::Vector{…}, ::Colon; kw::@Kwargs{…}) @ Base ./reducedim.jl:984
> │││││││││││││││┌ kwcall(::@NamedTuple{…}, ::typeof(mapreduce), f::NLLSsolver.Bind{…}, op::typeof(Base.add_sum), A::Vector{…}) @ Base ./reducedim.jl:326
> ││││││││││││││││┌ mapreduce(f::NLLSsolver.Bind{…}, op::typeof(Base.add_sum), A::Vector{…}; dims::Colon, init::Float64) @ Base ./reducedim.jl:326
> │││││││││││││││││┌ _mapreduce_dim(f::NLLSsolver.Bind{…}, op::typeof(Base.add_sum), nt::Float64, A::Vector{…}, ::Colon) @ Base ./reducedim.jl:331
> ││││││││││││││││││┌ mapfoldl_impl(f::NLLSsolver.Bind{…}, op::typeof(Base.add_sum), nt::Float64, itr::Vector{…}) @ Base ./reduce.jl:36
> │││││││││││││││││││┌ foldl_impl(op::Base.MappingRF{NLLSsolver.Bind{…}, Base.BottomRF{…}}, nt::Float64, itr::Vector{SimpleError2{…}}) @ Base ./reduce.jl:40
> ││││││││││││││││││││┌ _foldl_impl(op::Base.MappingRF{NLLSsolver.Bind{…}, Base.BottomRF{…}}, init::Float64, itr::Vector{SimpleError2{…}}) @ Base ./reduce.jl:50
> │││││││││││││││││││││┌ (::Base.MappingRF{…})(acc::Float64, x::SimpleError2{…}) @ Base ./reduce.jl:92
> ││││││││││││││││││││││┌ (::NLLSsolver.Bind{typeof(NLLSsolver.costgradhess!), Tuple{…}})(args::SimpleError2{2, Float64, SVector{…}, SVector{…}}) @ NLLSsolver /mnt/data/git/NLLSsolver.jl/src/utils.jl:20
> │││││││││││││││││││││││┌ costgradhess!(linsystem::NLLSsolver.LinearSystem{…}, vars::Vector{…}, cost::SimpleError2{…}) @ NLLSsolver /mnt/data/git/NLLSsolver.jl/src/cost.jl:52
> ││││││││││││││││││││││││┌ gradhesshelper!(linsystem::NLLSsolver.LinearSystem{…}, cost::SimpleError2{…}, vars::Tuple{…}, blockind::SVector{…}, varflags::Int64) @ NLLSsolver /mnt/data/git/NLLSsolver.jl/src/cost.jl:37
> │││││││││││││││││││││││││┌ valuedispatch(lower::StaticInt{…}, upper::StaticInt{…}, val::Int64, fun::NLLSsolver.Bind{…}) @ NLLSsolver /mnt/data/git/NLLSsolver.jl/src/utils.jl:9
> ││││││││││││││││││││││││││┌ valuedispatch(lower::StaticInt{…}, upper::StaticInt{…}, val::Int64, fun::NLLSsolver.Bind{…}) @ NLLSsolver /mnt/data/git/NLLSsolver.jl/src/utils.jl:9
> │││││││││││││││││││││││││││┌ valuedispatch(lower::StaticInt{…}, upper::StaticInt{…}, val::Int64, fun::NLLSsolver.Bind{…}) @ NLLSsolver /mnt/data/git/NLLSsolver.jl/src/utils.jl:5
> ││││││││││││││││││││││││││││┌ (::NLLSsolver.Bind{typeof(NLLSsolver.gradhesshelper!), Tuple{…}})(args::StaticInt{0}) @ NLLSsolver /mnt/data/git/NLLSsolver.jl/src/utils.jl:20
> │││││││││││││││││││││││││││││┌ gradhesshelper!(linsystem::NLLSsolver.LinearSystem{…}, cost::SimpleError2{…}, vars::Tuple{…}, blockind::SVector{…}, varflags::StaticInt{…}) @ NLLSsolver /mnt/data/git/NLLSsolver.jl/src/cost.jl:25
> ││││││││││││││││││││││││││││││┌ computecost(residual::SimpleError2{2, Float64, SVector{…}, SVector{…}}, vars::Tuple{SVector{…}, SVector{…}}) @ NLLSsolver /mnt/data/git/NLLSsolver.jl/src/residual.jl:44
> │││││││││││││││││││││││││││││││┌ computerescost(residual::SimpleError2{2, Float64, SVector{…}, SVector{…}}, kernel::NoRobust, vars::Tuple{Tuple{…}}) @ NLLSsolver /mnt/data/git/NLLSsolver.jl/src/residual.jl:51
> ││││││││││││││││││││││││││││││││ no matching method found `computeresidual(::SimpleError2{2, Float64, SVector{6, Float64}, SVector{3, Float64}}, ::Tuple{SVector{6, Float64}, SVector{3, Float64}})`: r = computeresidual(tuple(residual::SimpleError2{2, Float64, SVector{6, Float64}, SVector{3, Float64}})::Tuple{SimpleError2{2, Float64, SVector{6, Float64}, SVector{3, Float64}}}, vars::Tuple{Tuple{SVector{6, Float64}, SVector{3, Float64}}}...)
> │││││││││││││││││││││││││││││││└────────────────────
> 
> ```

---

<div class="post-metadata">

**Author:** ![user664303](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/user664303/32/37843_2.png) [@user664303](https://discourse.julialang.org/u/user664303)\
**Post date:** [June 16, 2026, 4:58pm UTC](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645/5 "2026-06-16T16:58:01Z")

</div>

> [@matthias314](#):
>
> Also, are you sure that no mutable struct escapes? For example, one `MVector{3,Float64}` is created in line `@NLLSsolver/src/linearsystem.jl:71` as part of the struct `UniVariateLSstatic_x{3}` and then returned to line `@NLLSsolver/src/linearsystem.jl:25`. Ditto for the other one and the `MMatrix{3,3,Float64,9}`, both of which are created in line `@NLLSsolver/src/linearsystem.jl:63` and returned to `@NLLSsolver/src/linearsystem.jl:25`

My expectation in this case, and in other cases like this, is that since the parent struct is mutable, and the size of MMatrix is known at compile time, it will be inlined in the parent struct - though this may not even be relevant. It’s the parent struct that doesn’t escape. I’m expecting the compiler to understand that the parent struct doesn’t escape, and neither do any of its fields (and any subfields of them). But perhaps it’s not up to this.

> [@matthias314](#):
>
> In my experience, one also needs that all functions that are called with the mutable struct as an argument are inlined. Do you have that?

I haven’t inlined the functions explicitly. I hope this isn’t a requirement, but haven’t tested it.

---

<div class="post-metadata">

**Author:** ![user664303](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/user664303/32/37843_2.png) [@user664303](https://discourse.julialang.org/u/user664303)\
**Post date:** [June 16, 2026, 5:33pm UTC](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645/6 "2026-06-16T17:33:49Z")

</div>

> [@matthias314](#):
>
> [JET.jl](https://juliaregistries.github.io/General/packages/redirect_to_repo/JET) reports a possible type error exactly at the location of at least some of the allocations

Oh, that’s a handy tool. That was indeed a bug. Thank you @matthias314 for finding it. I’ve fixed it in v4.0.6 of the package (just released), and added a unit test to catch it (if it reoccurs)! I now get:

```julia-auto
julia> @report_call optimize!(problem, options, 4)
No errors detected

```

Sadly, however, it did not impact any of the allocations, all of which still exist.

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [June 16, 2026, 6:14pm UTC](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645/7 "2026-06-16T18:14:15Z")

</div>

> [@user664303](#):
>
> I haven’t inlined the functions explicitly. I hope this isn’t a requirement, but haven’t tested it.

Apparently so (v1.12.6), which is a bit surprising because I’ve heard of interprocedural escape analysis in Julia a while back:

```julia-auto
julia> function neg!(ri)
         ri[] = -ri[]
       end
neg! (generic function with 1 method)

julia> function neg(i)
         ri = Ref(i)
         neg!(ri)
         ri[]
       end
neg (generic function with 1 method)

julia> @code_llvm neg(1) # automatic inlining even optimized away the RefValue
; Function Signature: neg(Int64)
; @ REPL[27]:1 within `neg`
; Function Attrs: uwtable
define i64 @julia_neg_6659(i64 signext %"i::Int64") #0 {
top:
; @ REPL[27]:3 within `neg`
; ┌ @ REPL[28]:2 within `neg!`
; │┌ @ int.jl:85 within `-`
    %0 = sub i64 0, %"i::Int64"
    ret i64 %0
; └└
}

julia> using AllocCheck; check_allocs(neg, (Int,))
Any[]

julia> function neg(i)
         ri = Ref(i)
         @noinline neg!(ri)
         ri[]
       end
neg (generic function with 1 method)

julia> check_allocs(neg, (Int,))
1-element Vector{Any}:
 Allocation of Base.RefValue{Int64} in .\refvalue.jl:8
  | RefValue{T}(x) where {T} = new(x)

```

---

<div class="post-metadata">

**Author:** ![Oscar\_Smith](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/oscar_smith/32/25343_2.png) [@Oscar\_Smith](https://discourse.julialang.org/u/Oscar_Smith)\
**Post date:** [June 16, 2026, 7:11pm UTC](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645/8 "2026-06-16T19:11:16Z")

</div>

Julia has an implementation of inter-procedural escape analysis but it isn’t hooked up to the compiler (it’s still fairly beta). If anyone wants a fairly complicated but cool project, reviving [wip: overhaul EscapeAnalysis.jl - Pull Request #56849 - JuliaLang/julia - GitHub](https://github.com/JuliaLang/julia/pull/56849) would be the path to turning it on by default (@aviatesk is pretty much full time working on Jetls and so hasn’t had time to finish it up)

---

<div class="post-metadata">

**Author:** ![user664303](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/user664303/32/37843_2.png) [@user664303](https://discourse.julialang.org/u/user664303)\
**Post date:** [June 16, 2026, 7:30pm UTC](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645/9 "2026-06-16T19:30:54Z")

</div>

> [@Benny](#):
>
> [AllocCheck.jl](https://juliaregistries.github.io/General/packages/redirect_to_repo/AllocCheck) is nice but it doesn’t say much.

I’ve just tried this, but it doesn’t seem to provide any more information than profiling allocations.

> [@Benny](#):
>
> I’d be interested in tooling that exposes escape analysis

Yes! Me too.

---

<div class="post-metadata">

**Author:** ![user664303](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/user664303/32/37843_2.png) [@user664303](https://discourse.julialang.org/u/user664303)\
**Post date:** [June 18, 2026, 2:07pm UTC](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645/10 "2026-06-18T14:07:05Z")

</div>

Putting the mutable structs (and the vagaries of escape analysis) aside, there are two allocations for immutable structs that I find surprising:

1. An instance of NLLSOptions, a concretely typed, 64B immutable struct, that’s passed down through the call stack as an input argument, but at some point gets popped into the heap.
2. An instance of NLLSResult, a concretely typed, isbits, 112B immutable struct, that’s passed back up through the call stack as the sole return value, apparently on the heap.

There is nothing about either of these instances, or they way in which they’re used, that would lead me to imagine they could end up on the heap.

---

<div class="post-metadata">

**Author:** ![adienes](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/adienes/32/37459_2.png) [@adienes](https://discourse.julialang.org/u/adienes)\
**Post date:** [June 18, 2026, 2:15pm UTC](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645/11 "2026-06-18T14:15:35Z")

</div>

> [@user664303](#):
>
> 1. An instance of NLLSOptions, a concretely typed, 64B immutable struct, that’s passed down through the call stack as an input argument, but at some point gets popped into the heap.
> 2. An instance of NLLSResult, a concretely typed, isbits, 112B immutable struct, that’s passed back up through the call stack as the sole return value, apparently on the heap.

`iterator::Type{T}` in `NLLSOptions` may be a source? `Type{T}` is not a concrete type

---

<div class="post-metadata">

**Author:** ![user664303](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/user664303/32/37843_2.png) [@user664303](https://discourse.julialang.org/u/user664303)\
**Post date:** [June 18, 2026, 2:21pm UTC](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645/12 "2026-06-18T14:21:06Z")

</div>

> [@adienes](#):
>
> `iterator::Type{T}` in `NLLSOptions` may be a source? `Type{T}` is not a concrete type

Is it not concrete when T is concrete (which it is)? It makes the struct return `isbits = false`, but I have a patch that changes the type of the iterator field, such that `isbits = true`, and this makes no difference to the allocations.

---

<div class="post-metadata">

**Author:** ![adienes](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/adienes/32/37459_2.png) [@adienes](https://discourse.julialang.org/u/adienes)\
**Post date:** [June 18, 2026, 2:24pm UTC](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645/13 "2026-06-18T14:24:50Z")

</div>

it is not. see:

```julia-auto
julia> isconcretetype(Type{Int})
false

julia> isbits(Type{Int})
false

```

---

<div class="post-metadata">

**Author:** ![user664303](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/user664303/32/37843_2.png) [@user664303](https://discourse.julialang.org/u/user664303)\
**Post date:** [June 18, 2026, 2:30pm UTC](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645/14 "2026-06-18T14:30:44Z")

</div>

> [@adienes](#):
>
> it is not.

OK. Thanks.

As I say, unfortunately [fixing this](https://github.com/ojwoodford/NLLSsolver.jl/pull/53) doesn’t remove the allocation. All the allocations remain unchanged. So they remain a mystery.

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [June 18, 2026, 3:31pm UTC](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645/15 "2026-06-18T15:31:47Z")

</div>

The `::Type{T}` field does appear to be contributing a pointer to a type or something, but whatever allocation contributed by instantiating `options` occurred prior to profiling. It doesn’t explain the `Int64` allocation because it lacks such a field. When I tried `AllocCheck.check_allocs(optimize!, typeof.((problem, options, 4)))`, it reported those 2 as well as a dynamic dispatch, all of the `optimize!` method at line 2 of `optimize.jl`. Unless something has changed since last year when I asked, dynamic dispatches require boxed inputs. If an input wasn’t already on the heap, the call needs to allocate and copy it to the heap first. That’s my working theory, but something that bothers me is the only visible `Int64` input is `4`, and I was also told that Julia cached small boxed `Int`s (`-512:510` still incurs 0 allocations in the dynamic dispatch example I made then) so I don’t know why that showed up in runtime profiling. Maybe `optimize!` recurses to calling itself with a larger integer, maybe an “allocation” retrieving cached objects is still reported as one, but I’m baselessly speculating at this point.

I then tried `JET.@report_opt optimize!(problem, options, 4)`, which spit out a giant report about recursion and dynamic dispatches. No idea where to start there, I do see recursion in your package in the course of `optimize!`, but the reported problems seems to start at a `@warn` at line 26 of `cost.jl`, not type inference issues elsewhere as I expected.

Aside, you don’t need the `::Type{T}` field if all you’re doing is retrieving an associated type. A getter method can retrieve the parameter directly, and `getproperty` can wrap that in dot syntax.

---

<div class="post-metadata">

**Author:** ![user664303](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/user664303/32/37843_2.png) [@user664303](https://discourse.julialang.org/u/user664303)\
**Post date:** [June 18, 2026, 5:10pm UTC](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645/16 "2026-06-18T17:10:14Z")

</div>

> [@Benny](#):
>
> I then tried `JET.@report_opt optimize!(problem, options, 4)`, which spit out a giant report about recursion and dynamic dispatches. No idea where to start there, I do see recursion in your package in the course of `optimize!`, but the reported problems seems to start at a `@warn` at line 26 of `cost.jl`, not type inference issues elsewhere as I expected.

Thanks. All of the dynamic dispatches disappear when the @warn line is commented out, and I get

```julia-auto
julia> @report_opt optimize!(problem, options, 4)
No errors detected

```

> [@Benny](#):
>
> When I tried `AllocCheck.check_allocs(optimize!, typeof.((problem, options, 4)))`, it reported those 2 as well as a dynamic dispatch, all of the `optimize!` method at line 2 of `optimize.jl`

Thanks. Perhaps I wasn’t calling `check_allocs` correctly when I tried earlier. Now I get

```julia-auto
julia> check_allocs(optimize!, typeof.((problem, options, 4)))
3-element Vector{Any}:
 Allocation of NLLSOptions{:levenbergmarquardt, 1} in /Users/olly/.julia/dev/NLLSsolver.jl/src/optimize.jl:2
  | optimize!(problem::NLLSProblem, options::NLLSOptions, unfixed::Integer, callback=nullcallback) = setupsinglevarls(optimizeinternal!, problem, options, unfixed, Stats(), callback)

Stacktrace:
 [1] optimize!(problem::NLLSProblem{Union{SVector{6, Float64}, SVector{3, Float64}}, SimpleError2{2, Float64, SVector{6, Float64}, SVector{3, Float64}}}, options::NLLSOptions{:levenbergmarquardt, 1}, unfixed::Int64)
   @ NLLSsolver ~/.julia/dev/NLLSsolver.jl/src/optimize.jl:2

 Allocation of Int64 in /Users/olly/.julia/dev/NLLSsolver.jl/src/optimize.jl:2
  | optimize!(problem::NLLSProblem, options::NLLSOptions, unfixed::Integer, callback=nullcallback) = setupsinglevarls(optimizeinternal!, problem, options, unfixed, Stats(), callback)

Stacktrace:
 [1] optimize!(problem::NLLSProblem{Union{SVector{6, Float64}, SVector{3, Float64}}, SimpleError2{2, Float64, SVector{6, Float64}, SVector{3, Float64}}}, options::NLLSOptions{:levenbergmarquardt, 1}, unfixed::Int64)
   @ NLLSsolver ~/.julia/dev/NLLSsolver.jl/src/optimize.jl:2

 Dynamic dispatch to function optimize! in /Users/olly/.julia/dev/NLLSsolver.jl/src/optimize.jl:2
  | optimize!(problem::NLLSProblem, options::NLLSOptions, unfixed::Integer, callback=nullcallback) = setupsinglevarls(optimizeinternal!, problem, options, unfixed, Stats(), callback)

Stacktrace:
 [1] optimize!(problem::NLLSProblem{Union{SVector{6, Float64}, SVector{3, Float64}}, SimpleError2{2, Float64, SVector{6, Float64}, SVector{3, Float64}}}, options::NLLSOptions{:levenbergmarquardt, 1}, unfixed::Int64)
   @ NLLSsolver ~/.julia/dev/NLLSsolver.jl/src/optimize.jl:2

```

It shows two of the allocations the allocation profiler finds, and is also saying the initial call to `optimize!` is a dynamic dispatch. However, I think this is incorrect, because all the types are known, and `@report_opt` doesn’t flag it.

I’m fairly certain there aren’t any dynamic dispatches occurring, and I’m also sure that the NLLSOptions and NLLSResult instances being heap allocated are immutable, isbits structures. So their allocations remain a mystery to me.

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [June 18, 2026, 6:48pm UTC](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645/17 "2026-06-18T18:48:31Z")

</div>

Interesting, `check_allocs` and `@report_opt` appear to disagree on dynamic dispatches, and I can corroborate `@report_opt` passing when the `@warn` line is commented out. I’m not certain which is actually correct. Earlier I assumed `optimize!` was recursing to a dynamic dispatch of itself, but no, you’re right that it’s the initial call. I tried a pointless forwarding function `w(problem, options, i) = @inline optimize!(problem, options, i)`, and `check_allocs` _still_ thinks `optimize!` is dynamically dispatched (yet marked `[inlined]` in the stacktrace before `w`), so it’s not a generic initial call bug. Also worth mentioning that `@code_llvm w(problem, options, 4)` shows a `call` of `optimize!`, which is neither inlined nor the `@ijl_apply_generic` indicative of dynamic dispatch. Maybe worth an issue?

Aside, I also initially thought the dynamic dispatch would explain why `check_allocs` couldn’t statically catch all the other allocations, but that really depends on which static analysis tool is right.

---

<div class="post-metadata">

**Author:** ![user664303](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/user664303/32/37843_2.png) [@user664303](https://discourse.julialang.org/u/user664303)\
**Post date:** [June 18, 2026, 8:34pm UTC](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645/18 "2026-06-18T20:34:46Z")

</div>

> [@Benny](#):
>
> I tried a pointless forwarding function `w(problem, options, i) = @inline optimize!(problem, options, i)`, and `check_allocs` _still_ thinks `optimize!` is dynamically dispatched (yet marked `[inlined]` in the stacktrace before `w`), so it’s not a generic initial call bug. Also worth mentioning that `@code_llvm w(problem, options, 4)` shows a `call` of `optimize!`, which is neither inlined nor the `@ijl_apply_generic` indicative of dynamic dispatch. Maybe worth an issue?

Thanks for investigating. There is something suspicious there. However, I don’t know enough about Julia calls and llvm code to know exactly what isn’t working as expected, so I won’t raise any issues on that myself.

---

<div class="post-metadata">

**Author:** ![simsurace](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/simsurace/32/30216_2.png) [@simsurace](https://discourse.julialang.org/u/simsurace)\
**Post date:** [June 18, 2026, 9:41pm UTC](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645/19 "2026-06-18T21:41:22Z")

</div>

Interesting. I vaguely remember having seen something similar even with a `@debug` log message, which I believed to be free. Allocations that were unexplainable to be disappearing when commenting out the log statement. I don’t have a reproducer right now, but I’m very curious to see where this is going.

---

<div class="post-metadata">

**Author:** ![user664303](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/user664303/32/37843_2.png) [@user664303](https://discourse.julialang.org/u/user664303)\
**Post date:** [June 18, 2026, 10:12pm UTC](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645/20 "2026-06-18T22:12:22Z")

</div>

> [@simsurace](#):
>
> Allocations that were unexplainable to be disappearing when commenting out the log statement.

In this instance, no allocations disappear when the logging statement is commented out, since the log statement isn’t called, only compiled. What does disappear is the warnings of potential dynamic dispatches from the static analysis. But again, these dispatches would not have run since the logging statement was not hit. I don’t think there’s anything unexplainable in respect of the logging going on here. The warning statement should become free if I disable that level of logging during compilation, which would have the same effect as commenting it out.

[Next page](https://discourse.julialang.org/t/how-to-understand-reasons-for-some-unexpected-heap-allocations/137645.md?page=2)
