A significant difference in Julia is that a NamedTuple is its own type, and therefore the compiler can specialize on that particular named tuple signature, while the Dictionary approach has to look up the value from the key. In addition, each value of a NamedTuple can be a different type itself, allowing for further optimization and type stability beyond what can be achieved in a dictionary. If we change it a bit more so that the type of the dictionary is Heterogeneous so that it is of type Dict{Symbol,Any}
, you can see how it can still be type stable.
d=Dict(:k1=>"v1",:k2=>2.0)
nt=(k1="v1",k2=2.0)
foo(x) = x[:k2]
Now if a function uses this dictionary or named tuple directly, we can see for the dictionary type, the result is not type stable, as the value in the Dictionary can only be guaranteed to by the type of Any
.
@code_warntype foo(d)
Body::Any
4 1 ─ %1 = invoke Base.ht_keyindex(_2::Dict{Symbol,Any}, :k2::Symbol)::Int64 │╻ getindex
│ %2 = (Base.slt_int)(%1, 0)::Bool ││╻ <
└── goto #3 if not %2 ││
2 ─ %4 = %new(Base.KeyError, :k2)::KeyError ││╻ Type
│ (Base.throw)(%4) ││
└── $(Expr(:unreachable)) ││
3 ─ %7 = (Base.getfield)(x, :vals)::Array{Any,1} ││╻ getproperty
│ %8 = (Base.arrayref)(false, %7, %1)::Any ││╻ getindex
└── goto #5 ││
4 ─ $(Expr(:unreachable)) ││
5 ┄ return %8
On the other hand, a NamedTuple can be type stable. Because the field was known by the function, the type is also known to be a Float64
.
@code_warntype foo(nt)
Body::Float64
4 1 ─ %1 = (Base.getfield)(x, :k2)::Float64 │╻ getindex
└── return %1