1

Is it possible to implement IEquatable from a struct, but pass the compared argument by reference?

Example:

struct BigToCopy : IEquatable<BigToCopy>
{
    int x, y, z, w, foo bar, etc;

    bool Equals(in BigToCopy other) // does not compile with 'in' parameter
    {
    }
}

It does not compile, because seemingly it does not implement IEquatable<BigToCopy> when there is the 'in' keyword.

This one is ok, but it copies by value:

struct BigToCopy : IEquatable<BigToCopy>
{
    int x, y, z, w, foo bar, etc;

    bool Equals(BigToCopy other) // does compile
    {
    }
}

IEqualityComparer has the same problem as far as I can tell. It requires the arguments to be passed by value.

Would compiler optimizations avoid copying even if we pass by value? Note: I'm on Unity. Using mono and il2cpp.

Edit: ok, let's be more concrete. We have a number of classes in our code base that are implemented like this:

struct Vertex
{
    public Point3d Position; // 3 doubles
    public Vector3d Normal; // 3 doubles
    public Vector2d UV; // 2 doubles
    public Color Color; // 4 bytes
    // possibly some other vertex data

    public override bool Equals(object obj) { ... }
    public bool Equals(in Vertex vertex) { ... }
}

There are multiple such classes. They are put in collections as HashSets, Dictionaries, etc. They are also being compared by explicit calls of Equals in some cases. These objects can be created in thousands, maybe even millions, and are processed in some way. They are usually in a hot code path.

Recently, I have found out that dictionary lookup using these objects can lead to object allocation. The reason: these objects don't implement IEquatable. Therefore, the dictionary calls Equals(object obj) overload instead, which leads to boxing (= memory allocation).

I'm fixing these classes right now, by implementing the IEquatable interface and removing the 'in' keyword. But there is some existing code that calls these Equals methods directly, and I was not sure whether I was affecting the performance badly. I could try, but there are many classes I'm fixing this way, so it is too much work to check. Instead, I add another method:

public bool EqualsPassByRef(in Vertex vertex) { ... }

and replace explicit calls of Equals by EqualsPassByRef.

It works ok this way. The performance would be better compared to before. I just wondered: maybe there is a way to make C# call the 'in' version from the dictionary. Then the 'EqualsPassByRef' versions would not be needed, making the code look better (and possibly also faster in dictionary lookup). From the answers I conclude that it is not possible. Ok, that's still fine.

Btw: I'm new to C#. I'm coming from a C++ world.

myavuzselim
  • 365
  • 5
  • 8
  • 5
    Sorry for not answering but asking a counter question. If its "BigToCopy" is it a wise decision to use a struct in the first place? Is there a requirement to make it a struct? – Ralf Dec 16 '22 at 10:46
  • 5
    Technically, you can try declaring `BigToCopy` as `ref struct`, but this way of doing things imposes many *restrictions*. Why don't you declare `BigToCopy` as `class`? – Dmitry Bychenko Dec 16 '22 at 11:10
  • 1
    What problem are you trying to solve here? Dictionary key lookup etc will use the standard equals method. So do you have custom code trying to call `Equals` with a value type by reference? If so, why can't you just declare a second `Equals` method? `public bool /* IEquatable*/ Equals(BigToCopy other) { ... } public bool /* other */ Equals(ref BigToCopy other) { ... }` – BurnsBA Dec 16 '22 at 15:58
  • There are multiple instances of structs in our code base that could be 'BitToCopy'. I might be exaggerating with 'big to copy', though. Possible examples: a matrix that contains 16 double values. Vertex data that contains 3 doubles for vertex position, 3 doubles for vertex normals, 4 bytes for color, maybe some extra vertex data, etc. Many instances (say millions) of these data can be created. They are used as keys in hash sets, dictionaries, etc. There is also code that explicitly compare equality between these objects. I'm reviewing and fixing their Equals implementation. – myavuzselim Dec 16 '22 at 18:49
  • @BurnsBA I have tried to define `bool Equals(BigToCopy other)` and `bool Equals(in BigToCopy other)` side by side. It gave a CLS compliance warning. I preferred renaming the 'in' version instead. Since I control the callers, I didn't see a reason to overload the 'Equals' method. – myavuzselim Dec 16 '22 at 19:40
  • @DmitryBychenko I'm not sure how `ref struct` would help (new to C#). Anyway, I would not want to restrict these classes from being part of heap-allocated classes. – myavuzselim Dec 16 '22 at 19:42
  • @Ralf @ DmitryBychenko Well, maybe (some of) these structs would work ok as classes. But they are implemented as structs in the first place, and they are many of these. Doing such changes in bulk looks error prone. Again, I might have been exaggerating when calling these classes as 'BigToCopy'. – myavuzselim Dec 16 '22 at 19:48
  • 1
    It sounds like you answered your question. You want to implement `IEquatable` to improve performance for value types, as it says here: https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1?view=net-7.0 . You can't change the dictionary implementation, but can create your own custom dictionary class if you really want to change how it checks for equality. But probably you should just fix your code to not call an equality check method by passing a value type by reference. – BurnsBA Dec 16 '22 at 20:07
  • `"You can't change the dictionary implementation, but can create your own custom dictionary class if you really want to change how it checks for equality."` Yes. Just got an idea how to do this (just in case if I see it is slow to copy objects while comparing). Put the original objects in a list/array etc (and usually these objects are already inside such a collection). Than store indices to that array in a HashSet / Dictionary, and use a custom IEqualityComparer. – myavuzselim Dec 18 '22 at 19:08
  • Edit: ok, my previous suggestion does not work if the queried key is not already part of the storage list. It is not a general solution. – myavuzselim Dec 18 '22 at 19:32

0 Answers0