在C#中拆解元组会有性能惩罚吗?

5

如果我们执行

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

这会比只执行以下操作产生额外费用吗:

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

还是说这全部都是语法糖?最终生成的代码总是使用Item1和Item2。


比赛你的马(原文链接:https://ericlippert.com/2012/12/17/performance-rant/) - Zohar Peled
他们总是使用 Item1Item2。这些名称仅用于可读性,并且仅在编译时可用。IL 显示它们使用 Item1Item2,无论您将它们命名为什么(ValueTuple 是一个像其他结构体一样具有字段的结构体)-例如:valuetype [System.Runtime]System.ValueTuple``2<int32, int32>::Item1 - Zach Saw
1
没有性能惩罚,生成的代码是相同的。你也可以使用 if (helloAndWorld == ("hello","world")) - Panagiotis Kanavos
2个回答

7

它们实际上是相同的生成的 IL 代码,只有微小的 差异然而…它们可能会被 JIT编译器 和优化器编译成完全相同的指令。

鉴于此

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;
}

基本上会被表达为发出
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;
}
您可以在此处查看IL

以下是发布版本中的示例

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

简而言之,为此担忧所得的净收益是负5分钟,这是您永远无法回到过去的时间。

如果在元组的混合中添加2个int属性(onetwo),我认为差异更加明显,而且在发布版本中JIT(JIT ASM)最终显示了生成的代码的一些差异(Test1指令较少)。 - SpiritBob
很可能是由于值类型的复制。 - TheGeneral
是的,与仅通过引用传递不同。我有一个愚蠢的问题 - 你是如何解码Test2确实发出值元组结构的副本的?(string item = helloAndWorldStrings.Item1; string item2 = helloAndWorldStrings.Item1;) - SpiritBob

2

有第三个选项与第一个等效:

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

它同样翻译成(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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接