Manually implementing `ne` in `PartialEq` trait

The ne method docs of the PartialEq trait mention:

This method tests for != . The default implementation is almost always sufficient, and should not be overridden without very good reason.

(Source)

Just out of curiosity, are there any crates using a very good reason to implement this method by hand? What constitutes a very good reason?

In practice there isn't one. You see, ne and eq carry an invariant with them, i.e. that they are each other's complementary operations.

Given that, there is only the wiggle room of optimization. But the default implementation of ne is to just invert the result of a self.eq(other) call. Barring anything even more optimal than that (a very high bar when keeping in mind the invariant), there's no reason to manually implement it.

5 Likes

I suppose there could be a type where the comparison logic more naturally lends itself to implementing ne directly, and then you could implement eq as !self.ne(...). I can't think of one off the top of my head, though.

2 Likes

Here are some examples of ne implementations that I could find in the standard library:

impl<T: ?Sized + PartialEq, A: Allocator> PartialEq for Box<T, A> {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        PartialEq::eq(&**self, &**other)
    }
    #[inline]
    fn ne(&self, other: &Self) -> bool {
        PartialEq::ne(&**self, &**other)
    }
}

Here a container type Box<T, A> explicitly implements ne, because the contained type could have a custom ne implementation, and Box<T, A> must properly delegate to it. A similar reason would exist for other wrapper types and smart pointers.

// core_simd::vector
impl<T, const LANES: usize> PartialEq for Simd<T, LANES>
where
    LaneCount<LANES>: SupportedLaneCount,
    T: SimdElement + PartialEq,
{
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        // Safety: All SIMD vectors are SimdPartialEq, and the comparison produces a valid mask.
        let mask = unsafe {
            let tfvec: Simd<<T as SimdElement>::Mask, LANES> = intrinsics::simd_eq(*self, *other);
            Mask::from_int_unchecked(tfvec)
        };

        // Two vectors are equal if all lanes tested true for vertical equality.
        mask.all()
    }

    #[allow(clippy::partialeq_ne_impl)]
    #[inline]
    fn ne(&self, other: &Self) -> bool {
        // Safety: All SIMD vectors are SimdPartialEq, and the comparison produces a valid mask.
        let mask = unsafe {
            let tfvec: Simd<<T as SimdElement>::Mask, LANES> = intrinsics::simd_ne(*self, *other);
            Mask::from_int_unchecked(tfvec)
        };

        // Two vectors are non-equal if any lane tested true for vertical non-equality.
        mask.any()
    }
}

SIMD vectors use optimized instructions for comparison. The ISA provides separate instructions for equality and inequality, so the safe wrapper types should also use them.

There are also many cases where the ne method is implemented for more guaranteed performance. For example, tuples explicitly say that all fields must be equal in the eq impl, and at least one must be unequal in the ne impl. The performance benefits are obvious. Technically the compiler could inline the eq impl into the ne impl and simplify the boolean expression. However, inlining can be quite unreliable, because it's based on heuristics, and because it works in a "bottom-up" way. If the impls of eq for the tuple's components are inlined, the eq impl for the tuple itself may become larger than some threshold and ineligible for inlining. This would penalize ne for no good reason.

5 Likes

Those are still both "return early on inequality".

4 Likes

I believe that the #[derive(PartialEq)] also implements ne to delegate to each fields' ne, for similar reasons that the Box implementation does. (Or at least it did at some point; I recall some derives being simplified to remove some unnecessary function impls and reduce compile time impact, and this might've been one of them.)

The only case which isn't just delegating to other ne implementations would likely be some odd type which actually wants !(a == b) and a != b to have different results[1]. (Similar to how a == a isn't always true for floats, although note floats do maintain this property. Fuzzy logic may be a valid application, potentially.) But, of course, note that this will confuse consumers of the type, who are freely allowed to assume that those two expressions are functionally equivalent (so long as the assumption cannot lead to UB).

Optimization could be a reason to override the default ne implementation, but given Rust's already large reliance on optimization to cut through abstraction layers and the fact that !(a && b) and !a || !b have the same shortcircuiting behavior, this seems exceedingly unlikely to occur in practice. (Trying to prevent optimization, e.g. for constant-time primitives, may be one.)


  1. One exceedingly strange and almost certainly ill-advised application could be some test assertion DSL which allows you to write e.g. asserting(x) == y/asserting(x) != y instead of assert_eq!(x, y)/assert_ne!(x, y). Basically, if the operators should have side effects. ↩ī¸Ž

1 Like

Wouldn't such a type violate the contract of PartialEq and therefore shoulnd't implement PartialEq at all?

2 Likes

Wouldn't such a type violate the contract of PartialEq and therefore shoulnd't implement PartialEq at all?

Yeah. I was thinking about semidecidable logic as a use case, where true would map to "known to be true" and false to "unsure/unknown" (or "unsure" and "known to be false" resp.) , but it would be abuse of booleans and PartialEq and a custom enum and a "SemiEq" etc trait should be written instead.

1 Like

Yup, the derives now only do the required methods, and leave everything else (ne, lt, etc) to the default implementations. (See https://github.com/rust-lang/rust/pull/98655 .) That's much faster in the proc macro part, though it does have some implications on codegen quality for things like gt on a 2-field struct.

If a == b executes in constant time, then so does !(a == b). It's an easier solution to constant-time issues than a manual a != b impl.

One reason would be debug build performance. It won't be optimized otherwise, unless you mark the method impls as #[inline(always)], which is likely a bad idea, and the boolean expression wouldn't be simplified anyway.

For common simple operations, like tuple, array or slice comparisons, I wouldn't be surprised if explicit impls helped even in release builds, just because an optimizer can have hiccups, and their performance would likely be critical.

I couldn't make up a realistic use case with the current trait definition, apart from some contrived cases like "log all calls of Foobar::ne". But I have wanted to use comparison operators in DSLs for various embedded languages. For example, we could have an embedded virtual machine, with usual Rust code used to write programs for it. Primitive types on the VM would correspond to some special types in Rust, with operations like + performing VM thunk construction, instead of directly performing the addition. Similarly, a == b would mean a thunk of that computation in the VM, to be evaluated sometime later.

In this case, !(a == b) and a != b would be different thunks, even if they are semantically the same and evaluate to the same final value on the VM. Unfortunately, the current desugaring makes this DSL impossible, since it hardcodes that a == b must be a bool.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.