Not understanding linear growing allocations

I’m trying to understand why returning a tuple of static vectors have the effect of constantly growing allocations. I have the following code to demonstrate

using BenchmarkTools
using StaticArrays

Vector2 = SVector{2}{Float64}

abstract type AbstractShape end

struct Box <: AbstractShape
    x::Float64
    y::Float64
end

function extent(b::Box)
    (Vector2(-b.x,-b.y), Vector2(b.x,b.y))
end

box = Box(1,1)

function area(box::Box)
    lower, upper =  extent(box)
    sum = 0.
    for i in 1:1000
        sum += (upper[1]-lower[1]) * (upper[2]-lower[2])
    end
    sum
end

@btime area($box)

It shows.

  139.743 ΞΌs (8011 allocations: 125.30 KiB)
4000.0

Changing the iterations it grows linearly, so the allocations happens on the getindex for lower and upper static vectors.

I see with @code_warntype that extent(box) returns Tuple{Any, Any}. I tried to qualify the definition of extent with function extent(b::Box)::Tuple{Vector2,Vector2} ... end but nothing changes for the allocations.

Is anything I am doing wrong?

This

const Vector2 = SVector{2,Float64}

works for me.

You will find the explanation in Performance Tips Β· The Julia Language This is required reading, IMO. The observed behavior pertains to performance tip number one.

Would it be possible for the compiler to give a warning when a non const global variable is referenced in a function?

Thanks very much. I read the performance tips, of course, but I was not expecting that a type alias Vector2 = SVector{2}{Float64} was not const implicitly. A simple const can make you waste hours :frowning:

Well, it’s not const unless you say so, it’s just a normal variable assignment. But yeah, it’s of course annoying when it happens.

I don’t think the compiler should warn against this, @goerch, but a linter should, perhaps.

FWIW GitHub - JunoLab/Traceur.jl would warn you.

julia> @trace area(box)
β”Œ Warning: uses global variable Main.Vector2
β”” @ REPL[7]:2
β”Œ Warning: uses global variable Main.Vector2
β”” @ REPL[7]:2
β”Œ Warning: dynamic dispatch to Main.Vector2(Base.neg_float(Base.getfield(_2, x)), Base.neg_float(Base.getfield(_2, y)))
β”” @ REPL[7]:2
β”Œ Warning: dynamic dispatch to Main.Vector2(Base.getfield(_2, x), Base.getfield(_2, y))
β”” @ REPL[7]:2
β”Œ Warning: sum is assigned as Float64
β”” @ REPL[9]:3
β”Œ Warning: sum is assigned as Any
β”” @ REPL[9]:5
β”Œ Warning:  is assigned as Tuple{Int64, Int64}
β”” @ REPL[9]:4
β”Œ Warning:  is assigned as Union{Nothing, Tuple{Int64, Int64}}
β”” @ REPL[9]:5
β”Œ Warning: dynamic dispatch to Base.getindex(Base.getfield($(Expr(:invoke, MethodInstance for extent(::Box), :(Main.extent), Core.Argument(2))), 2), 1)
β”” @ REPL[9]:5
β”Œ Warning: dynamic dispatch to Base.getindex(Base.getfield($(Expr(:invoke, MethodInstance for extent(::Box), :(Main.extent), Core.Argument(2))), 1), 1)
β”” @ REPL[9]:5
β”Œ Warning: dynamic dispatch to Base.getindex(Base.getfield($(Expr(:invoke, MethodInstance for extent(::Box), :(Main.extent), Core.Argument(2))), 2), 1) - Base.getindex(Base.getfield($(Expr(:invoke, MethodInstance for extent(::Box), :(Main.extent), Core.Argument(2))), 1), 1)
β”” @ REPL[9]:5
β”Œ Warning: dynamic dispatch to Base.getindex(Base.getfield($(Expr(:invoke, MethodInstance for extent(::Box), :(Main.extent), Core.Argument(2))), 2), 2)
β”” @ REPL[9]:5
β”Œ Warning: dynamic dispatch to Base.getindex(Base.getfield($(Expr(:invoke, MethodInstance for extent(::Box), :(Main.extent), Core.Argument(2))), 1), 2)
β”” @ REPL[9]:5
β”Œ Warning: dynamic dispatch to Base.getindex(Base.getfield($(Expr(:invoke, MethodInstance for extent(::Box), :(Main.extent), Core.Argument(2))), 2), 2) - Base.getindex(Base.getfield($(Expr(:invoke, MethodInstance for extent(::Box), :(Main.extent), Core.Argument(2))), 1), 2)
β”” @ REPL[9]:5
β”Œ Warning: dynamic dispatch to (Base.getindex(Base.getfield($(Expr(:invoke, MethodInstance for extent(::Box), :(Main.extent), Core.Argument(2))), 2), 1) - Base.getindex(Base.getfield($(Expr(:invoke, MethodInstance for extent(::Box), :(Main.extent), Core.Argument(2))), 1), 1)) * (Base.getindex(Base.getfield($(Expr(:invoke, MethodInstance for extent(::Box), :(Main.extent), Core.Argument(2))), 2), 2) - Base.getindex(Base.getfield($(Expr(:invoke, MethodInstance for extent(::Box), :(Main.extent), Core.Argument(2))), 1), 2))
β”” @ REPL[9]:5
β”Œ Warning: dynamic dispatch to Ο† (%4 => 0.0, %25 => %14) + (Base.getindex(Base.getfield($(Expr(:invoke, MethodInstance for extent(::Box), :(Main.extent), Core.Argument(2))), 2), 1) - Base.getindex(Base.getfield($(Expr(:invoke, MethodInstance for extent(::Box), :(Main.extent), Core.Argument(2))), 1), 1)) * (Base.getindex(Base.getfield($(Expr(:invoke, MethodInstance for extent(::Box), :(Main.extent), Core.Argument(2))), 2), 2) - Base.getindex(Base.getfield($(Expr(:invoke, MethodInstance for extent(::Box), :(Main.extent), Core.Argument(2))), 1), 2))
β”” @ REPL[9]:5
β”Œ Warning: area returns Any
β”” @ REPL[9]:1
4000.0

Tried that once, gives me too many false positives.

Well, in this case the good old @code_warntype is enough to detect this quickly (note that the red color highlighting is lost here on discourse):

julia> @code_warntype area(box)
MethodInstance for area(::Box)
  from area(box::Box) in Main at REPL[9]:1
Arguments
  #self#::Core.Const(area)
  box::Box
Locals
  @_3::Union{Nothing, Tuple{Int64, Int64}}
  @_4::Int64
  sum::Any
  upper::Any
  lower::Any
  i::Int64
Body::Any
1 ─ %1  = Main.extent(box)::Tuple{Any, Any}
β”‚   %2  = Base.indexed_iterate(%1, 1)::Core.PartialStruct(Tuple{Any, Int64}, Any[Any, Core.Const(2)])
β”‚         (lower = Core.getfield(%2, 1))
β”‚         (@_4 = Core.getfield(%2, 2))
β”‚   %5  = Base.indexed_iterate(%1, 2, @_4::Core.Const(2))::Core.PartialStruct(Tuple{Any, Int64}, Any[Any, Core.Const(3)])
β”‚         (upper = Core.getfield(%5, 1))
β”‚         (sum = 0.0)
β”‚   %8  = (1:1000)::Core.Const(1:1000)
β”‚         (@_3 = Base.iterate(%8))
β”‚   %10 = (@_3::Core.Const((1, 1)) === nothing)::Core.Const(false)
β”‚   %11 = Base.not_int(%10)::Core.Const(true)
└──       goto #4 if not %11
2 β”„ %13 = @_3::Tuple{Int64, Int64}
β”‚         (i = Core.getfield(%13, 1))
β”‚   %15 = Core.getfield(%13, 2)::Int64
β”‚   %16 = sum::Any
β”‚   %17 = Base.getindex(upper, 1)::Any
β”‚   %18 = Base.getindex(lower, 1)::Any
β”‚   %19 = (%17 - %18)::Any
β”‚   %20 = Base.getindex(upper, 2)::Any
β”‚   %21 = Base.getindex(lower, 2)::Any
β”‚   %22 = (%20 - %21)::Any
β”‚   %23 = (%19 * %22)::Any
β”‚         (sum = %16 + %23)
β”‚         (@_3 = Base.iterate(%8, %15))
β”‚   %26 = (@_3 === nothing)::Bool
β”‚   %27 = Base.not_int(%26)::Bool
└──       goto #4 if not %27
3 ─       goto #2
4 β”„       return sum

The above tells us that extent(box) is type unstable. Checking that function we get

julia> @code_warntype extent(box)
MethodInstance for extent(::Box)
  from extent(b::Box) in Main at REPL[7]:1
Arguments
  #self#::Core.Const(extent)
  b::Box
Body::Tuple{Any, Any}
1 ─ %1 = Base.getproperty(b, :x)::Float64
β”‚   %2 = -%1::Float64
β”‚   %3 = Base.getproperty(b, :y)::Float64
β”‚   %4 = -%3::Float64
β”‚   %5 = Main.Vector2(%2, %4)::Any
β”‚   %6 = Base.getproperty(b, :x)::Float64
β”‚   %7 = Base.getproperty(b, :y)::Float64
β”‚   %8 = Main.Vector2(%6, %7)::Any
β”‚   %9 = Core.tuple(%5, %8)::Tuple{Any, Any}
└──      return %9

So Main.Vector2(%2, %4)::Any is the culprit, i.e. the global variable (note the Main).

Yeah, but this is not very accessible to beginners. Therefore my question of asking for a compiler warning.

What would a compiler warning do? Is there such a thing? I’ve seen errors, but I can’t remember warnings.

Similar to

x = 0
for i in 1:10
    x = 1
end

which results in

β”Œ Warning: Assignment to `x` in soft scope is ambiguous because a global variable by the same name exists: `x` will be treated as a new local. Disambiguate by using `local x` to suppress this warning or `global x` to assign to the existing global variable.
β”” 

It doesn’t?

Are you using Jupyter notebooks or something similar by any chance?

UPDATE: Hm, no, in Jupyter I also don’t get this warning.

I’m still on 1.6.3. Are we happy with this change? (I could live with the warning before)

So that’s a compiler warning, is it?

Anyway, I think it should either be allowed, or not. Issuing a performance warning seems to me like the job of a linter.

That discussion was done to death many times over. It came after enormous popular demand.

No warning for me on 1.6.3 either.

Not sure what you’re / I’m doing differently.

Here working with Juno

The warning is shown when running from a file. It isn’t shown in the REPL since it’s more likely that a user intended the access to a global variable and writing functions purely in the REPL is kind of hard to do.

Thanks, that makes sense. In any case, I agree with @DNF that a performance related warning is qualitatively different from a scoping warning. But I’m open to a --performance-warnings mode that shows all kinds of performance related warnings or similar.

Sorry, but making differences when running a script from include or interactive shell simply sounds stupid to me.