Repository: numpy/numpy
File: numpy/_core/src/multiarray/unique.cpp (C++)

Code before:
template <typename T>
inline size_t hash_nonan(const T *value, npy_bool equal_nan) {
    return std::hash<T>{}(*value);
}

template <
    typename T,
    int (*isnan)(T) = npy_isnan_wrapper<T>
>
size_t hash_maybenan(const T *value, npy_bool equal_nan) {
    if (equal_nan && isnan(*value)) {
        return 0;
    }
    return hash_nonan(value, equal_nan);
}

template <typename S, typename T, S (*real)(T), S (*imag)(T)>
size_t hash_complex(const T *value, npy_bool equal_nan) {
    S value_real = real(*value);
    S value_imag = imag(*value);
    int isnan = npy_isnan_wrapper<S>(value_real) || npy_isnan_wrapper<S>(value_imag);
    if (equal_nan && isnan) {
        return 0;
    }

    return hash_nonan<S>(&value_real, equal_nan) ^ hash_nonan<S>(&value_imag, equal_nan);
}

template <typename T>
inline int equal_nonan(const T *lhs, const T *rhs, npy_bool equal_nan) {
    return equal_default(*lhs, *rhs);
}

template <
    typename T,
    int (*isnan)(T) = npy_isnan_wrapper<T>,
    int (*iseq_nonan)(T, T) = equal_default<T>
>
int equal_maybenan(const T *lhs, const T *rhs, npy_bool equal_nan) {
    int lhs_isnan = isnan(*lhs);
    int rhs_isnan = isnan(*rhs);
    if (lhs_isnan && rhs_isnan) {
        return equal_nan;
    }
    if (lhs_isnan || rhs_isnan) {
        return false;
    }
    return iseq_nonan(*lhs, *rhs);
}

template <typename S, typename T, S (*real)(T), S (*imag)(T)>

Review comment (suggestion):
Mentioned at the triage meeting: using xor for hash combine might lead to some hash collisions. Since NumPy already uses fmv-hash, could we instead feed the raw bytes into this function instead? This retain the hash integrity of hash.

Code after:
    if (equal_nan && hasnan) {
        return 0;
    }

    // Now, equal_nan is false or neither of the values is not NaN.
    // So we don't need to worry about NaN here.
    const unsigned char* value_bytes = reinterpret_cast<const unsigned char*>(value);
    size_t hash = npy_fnv1a(value_bytes, sizeof(T));

    return hash;
}

size_t hash_complex_clongdouble(const npy_clongdouble *value, npy_bool equal_nan) {
    npy_longdouble value_real = npy_creall(*value);
    npy_longdouble value_imag = npy_cimagl(*value);
    int hasnan = npy_isnan(value_real) || npy_isnan(value_imag);
    if (equal_nan && hasnan) {
        return 0;
    }

    // Now, equal_nan is false or neither of the values is not NaN.
    // So we don't need to worry about NaN here.
    // Some floating-point complex dtypes (e.g., npy_complex256) include undefined or
    // unused bits in their binary representation
    // (see: https://github.com/numpy/numpy/blob/main/numpy/_core/src/npymath/npy_math_private.h#L254-L261).
    // Because hashing the raw bit pattern would make the hash depend on those
    // undefined bits, we extract the mantissa, exponent, and sign components
    // explicitly and pack them into a buffer to ensure the hash is well-defined.
    #if defined(HAVE_LDOUBLE_INTEL_EXTENDED_12_BYTES_LE) || \
        defined(HAVE_LDOUBLE_INTEL_EXTENDED_16_BYTES_LE) || \
        defined(HAVE_LDOUBLE_MOTOROLA_EXTENDED_12_BYTES_BE)
    constexpr size_t SIZEOF_LDOUBLE_MAN = sizeof(ldouble_man_t);
    constexpr size_t SIZEOF_LDOUBLE_EXP = sizeof(ldouble_exp_t);
    constexpr size_t SIZEOF_LDOUBLE_SIGN = sizeof(ldouble_sign_t);
    constexpr size_t SIZEOF_BUFFER = 2 * (SIZEOF_LDOUBLE_MAN + SIZEOF_LDOUBLE_MAN + SIZEOF_LDOUBLE_EXP + SIZEOF_LDOUBLE_SIGN);
    unsigned char buffer[SIZEOF_BUFFER];

    union IEEEl2bitsrep bits_real{value_real}, bits_imag{value_imag};
    size_t offset = 0;

    for (const IEEEl2bitsrep &bits: {bits_real, bits_imag}) {
        ldouble_man_t manh = GET_LDOUBLE_MANH(bits);
        ldouble_man_t manl = GET_LDOUBLE_MANL(bits);
        ldouble_exp_t exp = GET_LDOUBLE_EXP(bits);
        ldouble_sign_t sign = GET_LDOUBLE_SIGN(bits);

        std::memcpy(buffer + offset, &manh, SIZEOF_LDOUBLE_MAN);
        offset += SIZEOF_LDOUBLE_MAN;
        std::memcpy(buffer + offset, &manl, SIZEOF_LDOUBLE_MAN);
        offset += SIZEOF_LDOUBLE_MAN;
        std::memcpy(buffer + offset, &exp, SIZEOF_LDOUBLE_EXP);