Performance penalties to deconstructing tuples in c#?
Asked Answered
R

2

6

If we do

var (hello, world) = GetHelloAndWorldStrings();
if (hello == "hello" && world == "world")
  Environment.Exit(0);

Does that incur any additional costs than just doing the following:

var helloAndWorld = GetHelloAndWorldStrings();
if (helloAndWorld.Hello == "hello" && helloAndWorld.World == "world")
  Environment.Exit(0);

Or is it all syntatic sugar - the code that ends up being generated always uses Item1 and Item2.

Raines answered 19/8, 2020 at 7:41 Comment(3)
race your horsesPouliot
They always use Item1 and Item2. The names are there for readability only and they are only available at compile time. IL shows they use Item1 and Item2 regardless of what you've named them (ValueTuple is a struct with fields like any other structs) - e.g. valuetype [System.Runtime]System.ValueTuple``2<int32, int32>::Item1Utopia
There's no performance penalty, the generated code is identical. You can also use if (helloAndWorld == ("hello","world"))Parkins
A
8

They are effectively the same generated IL with only minor emitted differences, However... they would likely be jitted and optimized to exactly the same instructions.

Given

private (String hello,string world) GetHelloAndWorldStrings() 
{ 
   return ("asdsd","sadfsdf");
}
    
...
        

public int Test1()
{
   var asd = GetHelloAndWorldStrings();
   if (asd.hello == "hello" && asd.world == "world")
      return 1;
   return 0;
}

public int Test2()
{
   var (hello, world) = GetHelloAndWorldStrings();
   if (hello == "hello" && world == "world")
      return 1;
   return 0;
}

Would basically be emitted as

public int Test1()
{
    ValueTuple<string, string> helloAndWorldStrings = GetHelloAndWorldStrings();
    if (helloAndWorldStrings.Item1 == "hello" && helloAndWorldStrings.Item2 == "world")
    {
        return 1;
    }
    return 0;
}

public int Test2()
{
    ValueTuple<string, string> helloAndWorldStrings = GetHelloAndWorldStrings();
    string item = helloAndWorldStrings.Item1;
    string item2 = helloAndWorldStrings.Item2;
    if (item == "hello" && item2 == "world")
    {
        return 1;
    }
    return 0;
}

You can check the IL out here

Here is an example of the JIT ASM in release

C.Test1()
    L0000: push ebp
    L0001: mov ebp, esp
    L0003: push esi
    L0004: mov ecx, [0x11198648]
    L000a: mov esi, [0x1119864c]
    L0010: mov edx, [0x11198650]
    L0016: call System.String.Equals(System.String, System.String)
    L001b: test eax, eax
    L001d: je short L0038
    L001f: mov edx, [0x11198654]
    L0025: mov ecx, esi
    L0027: call System.String.Equals(System.String, System.String)
    L002c: test eax, eax
    L002e: je short L0038
    L0030: mov eax, 1
    L0035: pop esi
    L0036: pop ebp
    L0037: ret
    L0038: xor eax, eax
    L003a: pop esi
    L003b: pop ebp
    L003c: ret

vs

C.Test2()
    L0000: push ebp
    L0001: mov ebp, esp
    L0003: push esi
    L0004: mov ecx, [0x11198648]
    L000a: mov esi, [0x1119864c]
    L0010: mov edx, [0x11198650]
    L0016: call System.String.Equals(System.String, System.String)
    L001b: test eax, eax
    L001d: je short L0038
    L001f: mov edx, [0x11198654]
    L0025: mov ecx, esi
    L0027: call System.String.Equals(System.String, System.String)
    L002c: test eax, eax
    L002e: je short L0038
    L0030: mov eax, 1
    L0035: pop esi
    L0036: pop ebp
    L0037: ret
    L0038: xor eax, eax
    L003a: pop esi
    L003b: pop ebp
    L003c: ret

In short, the net gain of worrying about this.. minus 5 minutes of your life you'll never get back

Anecdotic answered 19/8, 2020 at 7:52 Comment(3)
If you add 2 int properties into the mix of the tuple (one and two), I think the difference is more clearer, and the JIT (JIT ASM) in release finally shows some differences in generated code (Test1 has less instructions).Raines
@Raines likely because of the copy of the value type.Anecdotic
Yes, versus just passing by reference. I have a silly question - how did you decode that Test2 indeed emits a copy of the value tuple struct? (string item = helloAndWorldStrings.Item1; string item2 = helloAndWorldStrings.Item1;)Raines
M
2

There's a third option that is equivalent to the first:

  public int Test3()
  {
     var asd = GetHelloAndWorldStrings();
     if (asd == ("hello", "world"))
        return 1;
     return 0;
  }

It equally thranslates to (sharplab.io):

public int Test3()
{
    ValueTuple<string, string> helloAndWorldStrings = GetHelloAndWorldStrings();
    if (helloAndWorldStrings.Item1 == "hello" && helloAndWorldStrings.Item2 == "world")
    {
        return 1;
    }
    return 0;
}

(sharplab.io)

.method public hidebysig 
    instance int32 Test3 () cil managed 
{
    // Method begins at RVA 0x2064
    // Code size 47 (0x2f)
    .maxstack 2
    .locals init (
        [0] valuetype [System.Private.CoreLib]System.ValueTuple`2<string, string>
    )

    IL_0000: ldarg.0
    IL_0001: call instance valuetype [System.Private.CoreLib]System.ValueTuple`2<string, string> C::GetHelloAndWorldStrings()
    IL_0006: stloc.0
    IL_0007: ldloc.0
    IL_0008: ldfld !0 valuetype [System.Private.CoreLib]System.ValueTuple`2<string, string>::Item1
    IL_000d: ldstr "hello"
    IL_0012: call bool [System.Private.CoreLib]System.String::op_Equality(string, string)
    IL_0017: brfalse.s IL_002d

    IL_0019: ldloc.0
    IL_001a: ldfld !1 valuetype [System.Private.CoreLib]System.ValueTuple`2<string, string>::Item2
    IL_001f: ldstr "world"
    IL_0024: call bool [System.Private.CoreLib]System.String::op_Equality(string, string)
    IL_0029: brfalse.s IL_002d

    IL_002b: ldc.i4.1
    IL_002c: ret

    IL_002d: ldc.i4.0
    IL_002e: ret
} // end of method C::Test3
Maura answered 19/8, 2020 at 9:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.