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

Code before:
    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_IBM_DOUBLE_DOUBLE_BE) || defined(HAVE_LDOUBLE_IBM_DOUBLE_DOUBLE_LE)
    constexpr size_t SIZEOF_BUFFER = NPY_SIZEOF_CLONGDOUBLE;
    const unsigned char* buffer = reinterpret_cast<const unsigned char*>(value);
    #else
    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];
    size_t offset = 0;

    union IEEEl2bitsrep bits_real, bits_imag;
    bits_real.e = value_real;
    bits_imag.e = value_imag;

    // Copy real part
    ldouble_man_t manh_real = GET_LDOUBLE_MANH(bits_real);
    ldouble_man_t manl_real = GET_LDOUBLE_MANL(bits_real);
    ldouble_exp_t exp_real = GET_LDOUBLE_EXP(bits_real);
    ldouble_sign_t sign_real = GET_LDOUBLE_SIGN(bits_real);

    std::memcpy(buffer + offset, &manl_real, SIZEOF_LDOUBLE_MAN);
    offset += SIZEOF_LDOUBLE_MAN;
    std::memcpy(buffer + offset, &manh_real, SIZEOF_LDOUBLE_MAN);
    offset += SIZEOF_LDOUBLE_MAN;
    std::memcpy(buffer + offset, &exp_real, SIZEOF_LDOUBLE_EXP);
    offset += SIZEOF_LDOUBLE_EXP;
    std::memcpy(buffer + offset, &sign_real, SIZEOF_LDOUBLE_SIGN);
    offset += SIZEOF_LDOUBLE_SIGN;

Review comment (performance):
The approach below looks correct to me. My only comment is that you might be able to add an `#else if` block for platforms where longdouble and double are identical, avoiding the possibly slightly more expensive but standards-compliant version below for 80-bit longdouble on Linux. I don't know offhand if we already have preprocessor macros for platforms where longdouble is double already defined. If not, then this is fine as-is and no need for you to figure that out to merge this PR.

Code after:
    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);
        offset += SIZEOF_LDOUBLE_EXP;
        std::memcpy(buffer + offset, &sign, SIZEOF_LDOUBLE_SIGN);
        offset += SIZEOF_LDOUBLE_SIGN;
    }
    #else
    constexpr size_t SIZEOF_BUFFER = NPY_SIZEOF_CLONGDOUBLE;