5

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.

SpiritBob
  • 2,355
  • 3
  • 24
  • 62
  • [race your horses](https://ericlippert.com/2012/12/17/performance-rant/) – Zohar Peled Aug 19 '20 at 07:52
  • 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::Item1` – Zach Saw Aug 19 '20 at 08:01
  • 1
    There's no performance penalty, the generated code is identical. You can also use `if (helloAndWorld == ("hello","world"))` – Panagiotis Kanavos Aug 19 '20 at 08:20

2 Answers2

7

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

TheGeneral
  • 79,002
  • 9
  • 103
  • 141
  • 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). – SpiritBob Aug 19 '20 at 08:44
  • @SpiritBob likely because of the copy of the value type. – TheGeneral Aug 19 '20 at 08:47
  • 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;`) – SpiritBob Aug 19 '20 at 08:50
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
Paulo Morgado
  • 14,111
  • 3
  • 31
  • 59