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

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

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 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.

    std::complex<S> tmp = *reinterpret_cast<const std::complex<S> *>(value);
    // Get rid of -0.0 in tmp.
    if (value_real == 0) {
        tmp.real(0.0);
    }
    if (value_imag == 0) {
        tmp.imag(0.0);
    }
    const unsigned char* value_bytes = reinterpret_cast<const unsigned char*>(&tmp);
    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)

    // Get rid of -0.0.

Review comment (performance):
```suggestion
    value_real = (value_real == NPY_NZERO) ? NPY_PZERO : value_real;
    value_imag = (value_imag == NPY_NZERO) ? NPY_PZERO : value_imag;
    S value_array[2] = {value_real, value_imag};
    const unsigned char* value_bytes = reinterpret_cast<const unsigned char*>(value_array);
    size_t hash = npy_fnv1a(value_bytes, sizeof(S) * 2);
```

Instead of creating a new complex value, it might be more efficient to construct the array (and buffer) directly from the real and imaginary parts after normalizing -0.0 to +0.0.
After applying this change, the benchmark results showed "BENCHMARKS NOT SIGNIFICANTLY CHANGED".

Code after:
size_t hash_integer(const T *value, npy_bool equal_nan) {
    return std::hash<T>{}(*value);
}

template <typename S, typename T, S (*real)(T), S (*imag)(T)>
size_t hash_complex(const T *value, npy_bool equal_nan) {
    std::complex<S> z = *reinterpret_cast<const std::complex<S> *>(value);
    int hasnan = npy_isnan(z.real()) || npy_isnan(z.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.

    // Convert -0.0 to 0.0.
    if (z.real() == 0.0) {
        z.real(NPY_PZERO);
    }
    if (z.imag() == 0.0) {
        z.imag(NPY_PZERO);
    }

    size_t hash = npy_fnv1a(reinterpret_cast<const unsigned char*>(&z), sizeof(z));
    return hash;
}

size_t hash_complex_clongdouble(const npy_clongdouble *value, npy_bool equal_nan) {
    std::complex<long double> z =
        *reinterpret_cast<const std::complex<long double> *>(value);
    int hasnan = npy_isnan(z.real()) || npy_isnan(z.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.

    // Convert -0.0 to 0.0.
    if (z.real() == 0.0) {
        z.real(NPY_PZEROL);
    }
    if (z.imag() == 0.0) {
        z.imag(NPY_PZEROL);
    }

    // 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