== 运算符到底是做什么的?

5

我使用ILDASM查看了一个看起来像这样的.exe文件:

int a = 2;
Int32 b = 1;
if(b == 1)
{

}

现在,CIL代码看起来是这样的:
IL_0005:  ldloc.1
IL_0006:  ldc.i4.1
IL_0007:  ceq
IL_0009:  ldc.i4.0
IL_000a:  ceq
IL_000c:  stloc.2

我知道首先加载的是b(存储在[1]位置),然后是一个常数1,它们被比较。我不理解的是为什么要加载另一个值为0的常数进行比较,然后再将该比较结果存储。
由于第一个比较已经产生了一个真值,检查这个值是否为0会翻转结果,对吗?
我的问题是:为什么要反转结果? 我认为这与我使用的“==”操作符有关,我的理论是它返回差异。如果这个差异是0,那么这些值是相同的,因此结果应该是true。但是0代表false,所以它需要被反转。
我似乎找不到关于这个主题的任何信息,只是一些关于类似“==~”的操作符的内容。 希望你能为我解惑 :)
最好的问候,
Wilsu
PS:这是完整的代码:
.method private hidebysig instance void  Form1_Load(object sender,
                                                class [mscorlib]
System.EventArgs e) cil managed
{
// Code size       19 (0x13)

.maxstack  2
.locals init ([0] int32 a,
       [1] int32 b,
       [2] bool CS$4$0000)
IL_0000:  nop
IL_0001:  ldc.i4.2
IL_0002:  stloc.0
IL_0003:  ldc.i4.1
IL_0004:  stloc.1
IL_0005:  ldloc.1
IL_0006:  ldc.i4.1
IL_0007:  ceq
IL_0009:  ldc.i4.0
IL_000a:  ceq
IL_000c:  stloc.2
IL_000d:  ldloc.2
IL_000e:  brtrue.s   IL_0012
IL_0010:  nop
IL_0011:  nop
IL_0012:  ret
} // end of method Form1::Form1_Load

1
我完全不理解那个IL。我猜测它来自代码的另一部分。请发布一个简短但完整的示例。 - Jon Skeet
我只在主函数中使用这段代码,就得到了相同的(或至少类似的)结果。 - Rob
你是在发布模式下编译吗?(我没有看到任何“nop”,但是...)还有:这是整个代码吗?它似乎进行了一些有趣的重新排序;p但是:我没有看到第三个变量,所以对我来说stloc.2甚至是什么意思都不明显... - Marc Gravell
我非常确信你正在查看一个调试版本;简短的答案是:它会执行很多不必要的操作,以便更容易进行调试。你不应该过于关注调试版本中的IL代码。 - Marc Gravell
这不是完整的代码。我从IL_0005开始,这之前有4行,还有.maxstack 2和.locals init。代码的其余部分太长了,无法在评论中添加... 我会将其添加到我的问题中。 - Wilsu
2个回答

3

ceq从堆栈中获取两个值,如果它们被认为是相等的,则结果为1,如果它们不相等,则结果为0。但是,在C#中,==是否导致ceq取决于很多因素:

  • 数据类型
    • 它们是原始类型吗?
    • 它们有自定义的==运算符吗?
    • 它们是引用类型吗?
  • 上下文
    • 它可以优化成其他东西吗?(在类似的示例中,我得到了bne.un.s;还有beq*br*switch等)
    • 它可以完全删除吗?

关于数据类型:这就是为什么我使用整数的原因。原始数据类型,没有重载运算符。我从未想过像==运算符这样看似简单的东西可以进一步优化...你让我很感兴趣,我现在会去看看bne.un.s是什么。谢谢你的时间。 - Wilsu

1
据我所知,它正在执行一次跳跃到函数末尾的操作。
void Main()
{
    int a = 2;
    Int32 b = 1;
    if(b == 1)
    {
        Console.WriteLine("A");
    }
}

给我:
IL_0000:  nop         
IL_0001:  ldc.i4.2    
IL_0002:  stloc.0     // a
IL_0003:  ldc.i4.1    
IL_0004:  stloc.1     // b
IL_0005:  ldloc.1     // b
IL_0006:  ldc.i4.1    
IL_0007:  ceq         
IL_0009:  ldc.i4.0    
IL_000A:  ceq         
IL_000C:  stloc.2     // CS$4$0000
IL_000D:  ldloc.2     // CS$4$0000
IL_000E:  brtrue.s    IL_001D
IL_0010:  nop         
IL_0011:  ldstr       "A"
IL_0016:  call        System.Console.WriteLine
IL_001B:  nop         
IL_001C:  nop         
IL_001D:  ret         

从IL_0005开始,我们有:

加载b
加载1
ceq(如果相等,则推送1,否则推送0)- 结果将为1
加载0
ceq - 结果将为0
brtrue.s IL_001D - 如果值为非零,则跳转到IL_001D(函数结尾)

因此,它的编译结果如下:

int a = 2;
Int32 b = 1;
if(!(b == 1))
    goto end;
Console.WriteLine("A");
:end
return;

2
当我编译这段代码(经过优化)后,得到的结果是:ldc.i4.1ldc.i4.1bne.un.s {to the ret}ldstr "A"call void [mscorlib]System.Console::WriteLine(string)ret - 这与原来的代码截然不同。 - Marc Gravell
尽管坦率地说,我有点失望它没有完全删除并编译为“ret”,因为在运行时比较两个常量似乎很愚蠢。 - Marc Gravell
非常感谢,这正是我想知道的。 - Wilsu

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