Defining types of Structs attributes can sometimes be negatively impacting performance?

mutable struct Type4
    c
    a
    b
    function Type4(x=1,y=1,z=1)
        new(x,y,z)
    end
end

mutable struct Type5
    a
    b
    c
    function Type5(x::Int64=1, y::Int64=1, z::Int64=1)
        new(x,y,z)
    end
end

mutable struct Type8
    a::Real
    b::Real
    c::Real
    function Type8(x=1,y=1,z=1)
        new(x,y,z)
    end
end

mutable struct Type9
    a::Real
    b::Real
    c::Real
    function Type9(x::Int64=1,y::Int64=1,z::Int64=1)
        new(x,y,z)
    end
end


using BenchmarkTools
@benchmark Type4()
@benchmark Type5()
@benchmark Type8()
@benchmark Type9()

Not a huge difference but consistent difference.

@kwdef mutable struct Type10
    a::Real=1
    b::Real=1
    c::Real=1
end

mutable struct Type11
    a
    b
    c
    function Type11(x::Real=1, y::Real=1, z::Real=1)
        new(x,y,z)
    end
    function Type11(x::Number, y::Number,z::Number)
        new(x,y,z)
    end
end

mutable struct Type12
    a::Number
    b::Number
    c::Number
    function Type12(x::Real=1, y::Real=1, z::Real=1)
        new(x,y,z)
    end
    function Type12(x::Number, y::Number,z::Number)
        new(x,y,z)
    end
end


@benchmark Type10()
@benchmark Type11()
@benchmark Type12()

To me, it seemed like not defining attribute types while defining the struct attributes, but later in the constructor is the best approach.

(Forgive me for my lingo… I am not well acquainted with the actual terminology we use and am still a beginner.)

I don´t think those differences in performance are meaningful. But, for performance, what you need is to make the fields of the structs concrete. Real, for example, is an abstract type (it can be an integer, a float, etc).

You need either something like:

struct A
    x::Int
    y::Float64
end

where all the fields are concretely typed to specific types, or

struct A{T1<:Real,T2<:Real}
    x::T1
    y::T2
end

where an instance of the struct will have concrete types, but those can be of different types and different subypes of real.

Or some variation of the above constructs, always taking care that the fields are all concretely typed.

3 Likes

wait… so using <:Real is better than just Real?

So whether you define the types in constructors or the actual definition is meaningless eh? (As long as you ARE defining some type to help Julia figure out what functions to dispatch)

Both are important. The declared type in the struct definition is more important, though.

The difference is having that directly on the structure field, as you did, or as a type parameter, as I did. If you set the field to be Real, you are saying that the field can be changed from one type to the other in the same instance of the struct:

julia> mutable struct A x::Real end

julia> a = A(1)
A(1)

julia> a.x = 1.3
1.3

When you use a parametric type, you can initialize the structure with the subtypes, but then the structure has a concrete type associate with it:

julia> mutable struct B{T<:Real} x::T end

julia> b = B(1)
B{Int64}(1)

julia> b.x = 1.3
ERROR: InexactError: Int64(1.3)
4 Likes

Of note is that the declared field type has no bearing on the dispatch behavior of a value retrieved from that field. For example:

julia> struct Foo
           a::Real
       end

julia> bar(::Real) = "Real fallback"
bar (generic function with 1 method)

julia> bar(::Int) = "Int specialization"
bar (generic function with 2 methods)

julia> bar(Foo(1).a)
"Int specialization"

julia> bar(Foo(1.0).a)
"Real fallback"

When you say a struct field has type Real, all you’re saying is “this field can hold objects of any subtype of Real”. Type inference will use that information to narrow down the possibilities for dispatching, but it won’t otherwise influence dispatch behavior.

One consequence of declaring a field with an abstract type is that the object actually stored there is no longer stored in-line, because the original type needs to be preserved for dynamic dispatch to work. It’s this indirection & dynamic type checks that cause a slowdown in hot loops.

2 Likes

Ahhhhhh I see!

You are only timing the constructor. It could be there are some differences there, but they don’t seem to be significant.

The big performance difference is when you are using the struct. And the difference is whether the types of the fields are concrete or not.

struct MyStruct1
    x::Real
end

struct MyStruct2{T <: Real}
    x::T
end

Now, consider these calls:

f(arg) = 2arg.x

x1 = MyStruct1(2)
x2 = MyStruct2(2)

f(x1)
f(x2)

When f(x1) is about to be run, the compiler looks at the f, and at x1. Ok, the x1 is of type MyStruct1, it has a field of type Real. I’ll compile a version of f for MyStruct1. It does, and figures out that arg.x should be multiplied by 2. But it doesn’t know the type of arg.x, only that it’s a Real. So instead of simple mul instruction it creates code for finding a multiply function for whatever type arg.x happens to be. It’ll take some hundreds of clock ticks.

On the other hand, the f(x2) is easier for the compiler. It sees that x2 is of type MyStruct2{Int}. It compiles an instance of f for the type MyStruct2{Int}. It finds that the 2arg.x is multiplying the Int arg.x by the Int 2. So the compiler emits a single mul instruction (or even just a left shift). The function takes at most a clock tick.

That makes it even more clear. Thank you!