Understanding allocations in GeometryBasics.jl

I am currently using GeometryBasics.jl for a project. While optimizing the code, I noticed lots of memory allocations that hinders performance. Some of them I could figure it out but this one I cannot. Can somebody help me understanding why in the following test code there is one allocation. The code is really trivial and I was expecting no allocation. I am using version 1.6.2.

julia> using GeometryBasics, BenchmarkTools

julia> function tt2(p::Point3)
           p[1]*p[1] + p[2]*p[2] + p[3]*p[3]
       end
tt2 (generic function with 1 method)

julia> a = Point3{Float64}(1,1,1)
3-element Point3{Float64} with indices SOneTo(3):
 1.0
 1.0
 1.0

julia> @btime tt2(a)
  20.325 ns (1 allocation: 16 bytes)
3.0

Thanks,

Pere

Interpolate your variables into benchmark tools macros, with $:

@btime tt2($a)

Your tta funciton is just math on Float64 - there’s nothing to allocate. So it’s the global variable that is causing those allocations. Interpolating the variable moves it to the scope of the benchmark.

Thanks. I didn’t know. Is there an equivalent for @time?

julia> @time tt2(a)
  0.000004 seconds (1 allocation: 16 bytes)
3.0

Now that I understand that just math functions should not allocate, I found that looping on existing and preallocated data structures does allocate. See the example:

using GeometryBasics, BenchmarkTools

function mod2(p::Point3)
    p[1]*p[1] + p[2]*p[2] + p[3]*p[3]
end

struct Triangle
    a::Point3
    b::Point3
    c::Point3
end
function mod2(t::Triangle)
    mod2(t.a) + mod2(t.b) + mod2(t.c)
end

struct Polygone
    triangles::Vector{Triangle}
end
function mod2(p::Polygone)
    sum = 0.
    for t in p.triangles
        sum += mod2(t)
    end
    sum
end

a = Point3{Float64}(0,0,0)
b = Point3{Float64}(1,0,0)
c = Point3{Float64}(0,1,0)
d = Point3{Float64}(0,0,1)
p = Polygone([Triangle(a,b,c),Triangle(a,b,d),Triangle(a,c,d), Triangle(b,c,d)])

@btime mod2($p)
  374.458 ns (20 allocations: 320 bytes)
9.0

Is there a way to avoid these allocations? Thanks.

What happens if you define Triangle as

struct Triangle{T}
    a::Point3{T}
    b::Point3{T}
    c::Point3{T}
end

That will make this code type stable.

If something simple like that is allocating, its usually because your code isn’t type stable. To add to oscars example, also Polygone triangles field will still have the abstract type Vector{Triangle}

Use a type parameter instead of an abstract typed field:

struct Polygone{T<:Vector{<:Triangle}}
    triangles::T
end

Or:

struct Polygone{T<:Triangle}}
    triangles::Vector{T}
end

You can check type stability of your funciton calls with the @code_warntype macro.

Thanks very much! Adding the type parameter T to Triangle and Polygone the allocations disappear.

struct Triangle{T}
    a::Point3{T}
    b::Point3{T}
    c::Point3{T}
end

struct Polygone{T}
    triangles::Vector{Triangle{T}}
end
1 Like