3

Equality between Value Tuple types was introduced in C# 7.3. It allows code like this:

var x = (1, 2);
var y = (1, 2);

if(x == y) ...

This works fine and gives the correct result. However, the compiler introduces hidden copies of the tuple objects, and compares those instead, giving the equivalent of this:

var V_3 = x;
var V_4 = y;
if(V_3.Item1 == V_4.Item1 && V_3.Item2 == V_4.Item2) ...

Are V_3 and V_4 just defensive copies? If so, what are they defending against?

This is the smallest e.g I could come up with, but I've tried with user-defined structs, and method returns/properties as tuple members as well with similar (not always identical) output.

I'm using v5.0.202 of the .Net SDK and C# v9, but have seen this behavior since .Net Core 3.

SteveLove
  • 3,137
  • 15
  • 17

1 Answers1

7

The answer is that MSIL, the intermediate language you're decompiling back into C#, is a stack-based language, and stack-based languages have difficulty referencing what's not at the very top of the stack. So instead you can add a local variable and store results you can't easily reach.

And the reason why this seeming inefficiency isn't getting optimized out is that it simply doesn't matter. When JITed to native code, the compiler emits direct comparisons and a jump like you'd expect:

    var rng = new Random();
    var v1=(rng.Next(), rng.Next());
    var v2=(rng.Next(), rng.Next());
    
    return v1 == v2;

generates (skipping the initialization and restoring the stack in the prologue):

L004f: cmp edi, ebp
L0051: jne short L0064
L0053: cmp ebx, eax
L0055: sete al                // == && ==?
L0058: movzx eax, al          // return true && 2nd equality result
L0063: ret
L0064: xor eax, eax           // return false
L006e: ret
Blindy
  • 65,249
  • 10
  • 91
  • 131
  • Thanks. Except it doesn't explain why the compiler puts them in. If it's due to accessing the stack, why not have hidden copies for comparing every value type? – SteveLove Apr 08 '21 at 06:01
  • @SteveLove What do you mean by hidden copies? Seems a lot easier to just throw them back on top of the stack so they're easily accessible by called functions. –  Apr 08 '21 at 13:50
  • 1
    @SteveLove you do have them for every value type that's created inline, unless it has specific optimizations to remove them from MSIL. And the reason why that optimization wasn't added to value types is because they're newer and by this time the JITer has been proven to be able to optimize this away. – Blindy Apr 08 '21 at 14:39
  • @Amy I mean copies added by the compiler that aren't visible in *my* source code. And what you suggest is exactly what happens when we pass arguments to those functions (conceptually - might be registers, or elsewhere). In my e.g. the compiler is making copies of ints, but the tuples could contain anything, so the copies might be larger. I'm not worried by that - just interested! :) – SteveLove Apr 08 '21 at 16:14
  • @Blindy Is there any way to observe them in the CIL? For didactic purposes. (accepting your answer in any case) – SteveLove Apr 08 '21 at 16:28
  • Observe what in what, specifically? I have no idea what you're asking. – Blindy Apr 08 '21 at 22:45