Memory allocation when creating struct with dynamic array

I am writing a package for simulating realistic quantum systems. A key component in these simulations is defining pulses. Defining several constant size structs NormPulse, SquarePulse, etc. behaves as expected. However, when I extend the abstract type with DiscretePulse which contains a dynamic array there are allocations. Here is a minimum example:

Create a project containing:

module Example1

using SpecialFunctions

export AbstractPulse, DiscretePulse, NormPulse, amp

abstract type AbstractPulse end

mutable struct NormPulse <: AbstractPulse
    t0::Float64
    t1::Float64
    σ::Float64
    A::Float64
    B::Float64
end

mutable struct DiscretePulse <: AbstractPulse
    t0::Float64
    t1::Float64
    res::Float64
    A::Vector{ComplexF64}
    N::Int
end



norm_pdf(x, μ, σ) = exp(-(x - μ)^2 / (2 * σ^2)) / (√(2pi) * σ)
norm_cdf(x, μ, σ) = (1 + erf((x - μ) / (√2 * σ))) / 2

function norm_coeff(t0, t1, σ, area)
    τ = t1 - t0
    a = 2norm_cdf(τ / 2, 0, σ) - 1
    b = norm_pdf(0, τ / 2, σ)
    area / (a - b * τ), -area / (a / b - τ)
end

function NormPulse(t0, t1, σ, area)
    @assert t1 > t0
    @assert σ > 0
    a, b = norm_coeff(t0, t1, σ, area)
    NormPulse(t0, t1, σ, a, b)
end

inbounds(p::AbstractPulse, t) = p.t0 ≤ t ≤ p.t1
function amp(p::DiscretePulse, t)::ComplexF64
    if !inbounds(p, t)
    return 0
    end

    n = min(1 + Int(floor((t - p.t0) / p.res)), p.N)
    @inbounds return p.A[n]
end

end

Next run:

activate .

using Example1

Ω1 = [NormPulse(0.0, 1+rand(), rand() / 4, pi / 2) for i = 1:5]

function test(Ω)
    for i = 1:5
        t1 = 1+rand()
        amp(Ω[i], t1)
    end
end

@btime test(Ω1)

If I comment out the function amp(p::DiscretePulse, t) there are no allocations. However, if I do not, there are 2 allocations during every call of amp(Ω[i], t1). Does anyone know why this happens?

The function is called in a tight loop where allocations may dramatically impact performance.

More information

In an important application that I cannot disclose, there is an order of magnitude reduction in performance because of this issue.

With DiscretePulse and amp(p::DiscretePulse, t) defined (not explicitly used in test):
5.882 ms (151286 allocations: 4.56 MiB)
Without:
709.623 μs (669 allocations: 35.34 KiB)

Welcome! Thanks for the MWE, but for others to help please make sure it can be run:

  • SquarePulse is not defined
  • There is an unexpected end in the calling code
  • For the array comprehension Ω1 you might have meant i = 1:5 instead of i = 5

My inital guess for the problem is that Ω1 is defined in the global. Try to const Ω1 = [... it.

1 Like

Good catch, I had quickly copied the code to create an example. I’ve since corrected it.

Why would defining Ω1 as a const fix this issue?

When you use const on a variable the compiler is able to assume the type and is therefore in the position to apply optimizations. See Performance Tips · The Julia Language

Small example showing the difference:

julia> a = 2
2

julia> b = 3
3

julia> typeof(a)
Int64

julia> typeof(b)
Int64

julia> noconst() = a + b
noconst (generic function with 1 method)

julia> noconst()
5

julia> @code_warntype noconst()
Variables
  #self#::Core.Compiler.Const(noconst, false)

Body::Any
1 ─ %1 = (Main.a + Main.b)::Any
└──      return %1

julia> const c = 2
2

julia> const d = 3
3

julia> withconst() = c + d
withconst (generic function with 1 method)

julia> withconst()
5

julia> @code_warntype withconst()
Variables
  #self#::Core.Compiler.Const(withconst, false)

Body::Int64
1 ─ %1 = (Main.c + Main.d)::Core.Compiler.Const(5, false)
└──      return %1

The variable is in the local scope, I meant to write @btime test($Ω1). Nevertheless, the issue persists.

Your MWE isn’t working on Julia 1.4.2:

julia> activate .
ERROR: syntax: space before "." not allowed in "activate ." at REPL[16]:1
Stacktrace:
 [1] top-level scope at REPL[15]:0

julia> @btime test(Ω1)
ERROR: MethodError: no method matching amp(::NormPulse, ::Float64)

So its unclear what the problem is. I’m also not sure if I understand the

The variable is in the local scope, I meant to write @btime test($Ω1).

comment. Do you mean that the $ in front of Ω1 somehow makes it local scope? A reproducible example will certainly help…