JuliaServices / AutoHashEquals.jl

A Julia macro to add == and hash() to composite types.
Other
57 stars 12 forks source link

use generated function instead of macro on struct definition #22

Closed goretkin closed 1 year ago

goretkin commented 3 years ago

I like the idea of reusing methods on Tuple for hashing and ==, isequal, which leaves a question of how to turn a struct into a tuple of its fields. What I did in https://github.com/andrewcooke/AutoHashEquals.jl/pull/21 uses ntuple, but there is an alternative using generated functions, which produces better LLVM code, though I did not detect any performance difference I discussed it here

Currently, the macro is defined to take a struct definition and extract the field names given the definition. getfield can just take a field index, so parsing the names is not necessary. We should be able to avoid parsing the struct definition at all, and just use fieldcount in a generated function

# Based off https://discourse.julialang.org/t/slowness-of-fieldnames-and-propertynames/55364/2
@generated function  _tuple(obj::T) where {T}
    return :((tuple($((
        :(getfield(obj, $i)) for i in 1:fieldcount(obj)
    )...))))
end

What if this package instead worked like:

struct Foo
# define the struct as normal
end

@auto_hash_equals Foo

This has the benefit of not interfering with other struct-level macros (e.g. what https://github.com/andrewcooke/AutoHashEquals.jl/pull/19 tries to address). Other struct-level macros might want to define inner constructors, but that's not the case for this package.

matthias314 commented 2 years ago

It seems to me that Julia's compiler optimize enough to get by with regular instead of generated functions. The following code appears to work well. Both the comparison (using ==) and the hash are as fast as in AutoHashEquals.jl. (Empty structs also work.)

_hash(x, h, ::Val{0}) = h

function _hash(x, h, ::Val{i}) where i
    _hash(x, hash(getfield(x, i), h), Val(i-1))
end

macro auto_hash_equals(def)

    @assert def isa Expr && def.head === :struct
    T = def.args[2]
    T isa Expr && (T = T.args[1])

    quote
        $(esc(def))

        function Base.:(==)(x::$(esc(T)), y::$(esc(T)))
            all(1:fieldcount(typeof(x))) do i
                getfield(x, i) == getfield(y, i)
            end
        end

        function Base.hash(x::$(esc(T)), h::UInt)
            _hash(x, hash($(esc(QuoteNode(T))), h), Val(fieldcount(typeof(x))))
        end
    end

end

EDIT: made hash faster

gafter commented 1 year ago

@auto_hash_equals now permits the user to specify which fields are significant. While it might still be possible to use this kind of coding pattern, the benefit is mainly to the package developer (me). I would welcome a PR if someone wanted to offer simpler code with the same performance, but I'm not tempted to spend the time to do it myself. So I'm going to close this issue.