为什么C#编译器将此!=比较翻译为>比较?


147

我偶然发现C#编译器启用了此方法:

static bool IsNotNull(object obj)
{
    return obj != null;
}

…进入此CIL

.method private hidebysig static bool IsNotNull(object obj) cil managed
{
    ldarg.0   // obj
    ldnull
    cgt.un
    ret
}

…或者,如果您希望查看反编译的C#代码,请执行以下操作:

static bool IsNotNull(object obj)
{
    return obj > null;   // (note: this is not a valid C# expression)
}

怎么把这些!=翻译成“ >”?

Answers:


201

简短答案:

IL中没有“比较不等于”指令,因此C#!=运算符没有确切的对应关系,因此不能按字面意义进行翻译。

但是,有一条“比较等价”指令(ceq==运算符直接对应),因此在一般情况下,x != y它的翻译时间稍长一些(x == y) == false

也是一个“比较,大于”在IL(指令cgt),它允许编译采取一定的快捷键(即产生较短的IL代码),一个是对空的对象,不平等的比较,obj != null,得到翻译,好像他们是“ obj > null”。

让我们更详细一些。

如果IL中没有“比较不等于”指令,那么编译器将如何转换以下方法?

static bool IsNotEqual(int x, int y)
{
    return x != y;
}

如上所述,编译器会将x != y变成(x == y) == false

.method private hidebysig static bool IsNotEqual(int32 x, int32 y) cil managed 
{
    ldarg.0   // x
    ldarg.1   // y
    ceq
    ldc.i4.0  // false
    ceq       // (note: two comparisons in total)
    ret
}

事实证明,编译器并不总是产生这种相当冗长的模式。让我们看看y用常数0 替换会发生什么:

static bool IsNotZero(int x)
{
    return x != 0;
}

产生的IL比一般情况下短:

.method private hidebysig static bool IsNotZero(int32 x) cil managed 
{
    ldarg.0    // x
    ldc.i4.0   // 0
    cgt.un     // (note: just one comparison)
    ret
}

编译器可以利用以下事实:有符号整数存储在二进制补码中(如果将所得的位模式解释为无符号整数(这.un意味着-0具有最小可能的值)),因此它x == 0unchecked((uint)x) > 0

事实证明,编译器可以针对null以下项进行不等式检查:

static bool IsNotNull(object obj)
{
    return obj != null;
}

编译器产生的IL与以下代码几乎相同IsNotZero

.method private hidebysig static bool IsNotNull(object obj) cil managed 
{
    ldarg.0
    ldnull   // (note: this is the only difference)
    cgt.un
    ret
}

显然,允许编译器假定null引用的位模式是任何对象引用可能的最小位模式。

在“ 公共语言基础结构注释的标准”(2003年10月第1版)中(第491页,表6-4“二进制比较或分支操作”的脚注明确提到了此快捷方式:

cgt.un是在ObjectRefs(O)上允许且可验证的。这在将ObjectRef与null进行比较时通常使用(不存在“ compare-not-equal”指令,否则将是更明显的解决方案)。


3
很好的答案,只有一个尼特:二进制补码在这里不相关。它唯一要紧的是有符号整数存储以这样的方式,在非负值int的范围有相同的表示int,因为他们在做uint。这比二的补码要弱得多。

3
无符号类型永远不会有任何负数,因此与零进行比较的比较操作不能将任何非零数视为小于零。中与的非负值相对应的所有表示形式int已被中的相同值所占据uint,因此与的负值相对应的所有表示形式int都必须对应于大于的某个值,但哪个值实际上并不重要是。(实际上,实际上所需uint0x7FFFFFFFintuint

3
@hvd:感谢您的解释。没错,重要的不是二进制补码。这是您提到 的要求并且在不更改基础位模式的情况下cgt.un将an int视为事实uint。(想象一下,cgt.un将首先通过映射所有负数为0。你显然不能代替这种情况下试图修复下溢> 0!= 0。)
stakx -不再贡献

2
我感到惊讶的是,将一个对象引用与另一个使用的对象引用进行比较>是可验证的IL。这样一来,可以比较两个非空对象并获得布尔结果(这是不确定的)。这不是内存安全问题,但是感觉就像不干净的设计,这并不是安全托管代码的普遍精神。这种设计泄漏了将对象引用实现为指针的事实。似乎是.NET CLI的设计缺陷。
usr 2015年

3
@usr:绝对!CLI标准的 III.1.1.4节指出“对象引用(类型O)是完全不透明的”,并且“唯一允许的比较操作是相等和不相等……”。也许是因为对象引用在内存地址定义,标准还需要照顾到概念,从0保持空引用分开(例如见的定义ldnullinitobjnewobj)。因此,使用cgt.un比较对象引用与空引用来比较似乎与III.1.1.4节相抵触。
stakx-不再贡献2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.