Field types

Hmmm, but in my first example, when I get an instance of the object and check its type

m = test(5.2)
typeof(m) # yields test{Float64}

but in my second example typeof(m) just yields test. Maybe I understood this explanation in “perfromance tips” incorrectly but I thought that meant that the first example is preferred over the second for optimization purposes.

I think that what’s confusing you here is that you’ve written two types with the same name. Name them Test1{T} and Test2.

struct Test1{T}
    a::T
end 

struct Test2
    a::Float64
end

Now, if I have an object of type Test2 I always know that the a field is of type Float64.

julia> let t = Test2(1.0)
           @code_typed t.a
       end
CodeInfo(
1 ─ %1 = Base.getfield(x, f)::Float64
└──      return %1
) => Float64
1 Like

This is my exact code

struct Test1
    a:: Float64
end

struct Test2{T}
    a::T
end
a = Test1(5.5)
b = Test2(5.5)
typeof(a) #yields Test1
typeof(b) #yields Test2{Float64}

Good. So what we’re telling you is that there is no performance difference between operating on a vs b, because all the same information is available to the compiler.

E.g. these methods have identical typed IR

julia> f(t) = t.a + 1.0

julia> code_typed(f, (Test1{Float64},))
1-element Vector{Any}:
 CodeInfo(
1 ─ %1 = Base.getfield(t, :a)::Float64
│   %2 = Base.add_float(%1, 1.0)::Float64
└──      return %2
) => Float64

julia> code_typed(f, (Test2,))
1-element Vector{Any}:
 CodeInfo(
1 ─ %1 = Base.getfield(t, :a)::Float64
│   %2 = Base.add_float(%1, 1.0)::Float64
└──      return %2
) => Float64

So what am I not understanding about the following portion of the documentation:

The difference is that Float64 is a concrete type! AbstractFloat is an abstract type, so the compiler has no clue what could actually be stored there just based on type information. The data could have any layout.

With a Float64 is knows exactly what is stored.

1 Like

Consider the following code:

struct Type1 # No type parameter, but concrete field type
    a::Float64
end

struct Type2{T<:Real} # Type parameter
    a::T
end

struct Type3 # No type parameter, abstract field type
    a::Real
end

function f(x)
    return x.a + 1
end

x1 = Type1(1.0)
x2 = Type2(1.0)
x3 = Type3(1.0)

When calling f with either of x1, x2, or x3, compilation will occur. In this case, compilation involves figuring out the appropriate method of + to call, which depends on the type of x.a.

When f(x1) is called, the compiler can directly insert the code for the correct method +(::Float64, ::Int) because the type of x1.a is known from the type of x1 (i.e., x1 has type Type1, which has a field a of type Float64).

Similarly, when f(x2) is called, the compiler can directly insert the code for the correct method +(::Float64, ::Int) because the type of x2.a is known from the type of x2 (i.e., x2 has type Type2{Float64}, which has a field a of type Float64 – the same as the type parameter).

In contrast, when f(x3) is called, the compiler cannot directly insert the code for the correct method +(::Float64, ::Int) because the type of x3.a is not known from the type of x3. Running typeof(x3.a) yields Float64, but this is a runtime check, not a compile time certainty; in this case, the type of x3.a is determined by the value of x3 (known at runtime), not from the type of x3 (known at compile time). In this case, dynamic dispatch must be used to select the correct method of + to use, resulting in worse performance.

3 Likes

Ah. I think you might be right. That might be the source of all of my confusion: abstract types. So I really shouldn’t use the output of typeof as an optimization check I guess. The wording in the manual seemed to indicate that maybe I should.

typeof(a) and typeof(b) here do not tell you anything about performance of those types. Test1 and Test2{Float64} are essentially just the names of the structs (a.k.a. types) you defined. In the Test3 example below, I get the same output from typeof as your Test2, but Type3 will perform badly because 2/3 of the fields are abstractly typed.

julia> struct Test3{T}
           a::Real
           b::T
           c::AbstractString
       end

julia> c = Test3(5.5, 5.6, "hello")
Test3{Float64}(5.5, 5.6, "hello")

julia> typeof(c)
Test3{Float64}

The point is that you want the fields of your struct to have concrete types (not abstract types). If you only need to allow Float64, then it is fine to assign that type to a field directly. Parametric types allow the definition of multiple concrete types at once. The example below will be concretely typed parametrically.

julia> struct Test4{T}
           a::T
           b::T
           c::String
       end

julia> Test4(5.5, 5.6, "hello")
Test4{Float64}(5.5, 5.6, "hello")

julia> Test4("hello", "welcome", "home")
Test4{String}("hello", "welcome", "home")

The struct Test5 below is exactly the same as Test4 above except that Test5 will error if I try to enter something other than a subtype of Real for the first two fields. There is no performance difference.

julia> struct Test5{T <: Real}
           a::T
           b::T
           c::String
       end

julia> Test5(5.5, 5.6, "hello")
Test5{Float64}(5.5, 5.6, "hello")

julia> Test5("hello", "welcome", "home")
ERROR: MethodError: no method matching Test5(::String, ::String, ::String)
Closest candidates are:
  Test5(::T, ::T, ::String) where T<:Real at REPL[12]:2
Stacktrace:
 [1] top-level scope
   @ REPL[14]:1
3 Likes

Thank you. (to everyone!!). It’s starting to come together.