How to access shared "inherited" fields without paying multiple dispatch performance price?

Ok, I just couldn’t leave this one alone. It is possible to read the the “shared” fields with no overhead, but it’s super unsafe. I used unsafe_load along with a modified version of pointer_from_objref that doesn’t check if classes are mutable or not (that check is really slow). I also made all the structs mutable, because they obviously need to be heap allocated for my use case (still learning! :grin:).

The performance is much better:

julia> @time speedTest()
10003000000
  0.082848 seconds (10.03 k allocations: 413.281 KiB)

That’s ~30x faster than the fastest method dispatch. Obviously this is totally a microbenchmark and does not represent real-world usage. Still, it does show the overhead was there, and it’s gone now. The higher count of allocations is from heap allocating the structs.

I benchmarked a C++ translation of this code, at it runs at the same speed as Julia. I’m honestly very impressed with Julia here – what I’m doing is totally against the design of the language, but Julia gives me the tools to do it anyway and compiles it down into a super-optimized result. And then I can write a macro to wrap it all up into something that looks nice. Awesome.

The code is below. This is just a quick proof-of-concept and very unsafe. getLength() is pulling from pointer offset 1 in the struct, you could get the offsets of the other fields with fieldoffset(). Also, for struct references (pointers) you’d need to convert them back to objrefs.

abstract type Car end

mutable struct HondaAccord <: Car
    length::Int64
end

mutable struct ToyotaCamry1 <: Car
    length::Int64
end

mutable struct ToyotaCamry2 <: Car
    length::Int64
end

function unsafe_pointer_from_objref(@nospecialize(x))
    ccall(:jl_value_ptr, Ptr{Cvoid}, (Any,), x)
end

function getLength(a::Car)::Int64
    unsafe_load(Core.Intrinsics.bitcast(Ptr{Int64}, unsafe_pointer_from_objref(a)), 1)
end

function speedTest()
    cars = Car[]
    while length(cars) < 10000
        push!(cars, HondaAccord(100))
        push!(cars, ToyotaCamry1(100))
        push!(cars, ToyotaCamry2(100))
    end

    sum = 0::Int64
    for j = 1:10000
        for i = 1:length(cars)
            sum = sum + getLength(cars[i])
        end
    end

    println(sum)
end
5 Likes