为啥 C# 编译器将这个 != 比较翻译为 > 比较?

Posted

技术标签:

【中文标题】为啥 C# 编译器将这个 != 比较翻译为 > 比较?【英文标题】:Why does the C# compiler translate this != comparison as if it were a > comparison?为什么 C# 编译器将这个 != 比较翻译为 > 比较? 【发布时间】:2015-05-01 03:43:07 【问题描述】:

我偶然发现 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)

!= 怎么会被翻译成“>”?

【问题讨论】:

【参考方案1】:

简答:

IL中没有“比较不等于”指令,所以C#!=操作符没有精确对应关系,不能直译。

但是有一个“比较相等”指令(ceq,直接对应于 == 运算符),所以在一般情况下,x != y 被翻译成稍长的等效 (x == y) == false

还有在 IL (cgt) 中有一个“compare-greater-than”指令,它允许编译器采用某些快捷方式(即生成更短的 IL 代码),其中之一是不等式比较针对 null 的对象,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

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

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

编译器可以利用有符号整数存储在two's complement 中这一事实(其中,如果结果位模式被解释为无符号整数——这就是.un 的意思——0 具有最小的可能值),所以它会将x == 0 翻译成unchecked((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 引用的位模式是任何对象引用可能的最小位模式。

Common Language Infrastructure Annotated Standard (1st edition from Oct 2003)(第 491 页,作为表 6-4 “二进制比较或分支操作”的脚注)明确提到了此快捷方式:

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

【讨论】:

很好的答案,只有一点点:二进制补码在这里不相关。重要的是,有符号整数的存储方式应使int 范围内的非负值在int 中具有与uint 中相同的表示形式。这比二进制补码要弱得多。 无符号类型永远不会有任何负数,因此与零进行比较的比较操作不能将任何非零数视为小于零。 int的非负值对应的所有表示都已经被uint中的同一个值占用了,所以int的负值对应的所有表示都必须对应于some uint 的值大于 0x7FFFFFFF,但它并不重要。 (实际上,真正需要的是在intuint 中以相同的方式表示零。) @hvd:感谢您的解释。你是对的,重要的不是二进制补码;这是that you mentioned 的要求,cgt.unint 视为uint 而不会更改底层位模式。 (想象一下cgt.un 会首先尝试通过将所有负数映射到 0 来修复下溢。在这种情况下,您显然不能用> 0 替换!= 0。) 我发现使用 > 将对象引用与另一个对象引用进行比较是可验证的 IL,这让我感到惊讶。这样就可以比较两个非空对象并得到一个布尔结果(这是非确定性的)。这不是内存安全问题,但感觉像是不干净的设计,不符合安全托管代码的一般精神。这种设计泄露了对象引用被实现为指针的事实。似乎是 .NET CLI 的设计缺陷。 @usr:当然! CLI standard 的第 III.1.1.4 节说 “对象引用(O 型)是完全不透明的” 并且 “唯一允许的比较操作是相等和不等......” 可能是因为对象引用没有根据内存地址来定义,所以该标准还注意在概念上将空引用与 0 分开(参见例如ldnullinitobj 的定义和newobj)。因此,使用cgt.un 将对象引用与空引用进行比较似乎在不止一个方面与第 III.1.1.4 节相矛盾。

以上是关于为啥 C# 编译器将这个 != 比较翻译为 > 比较?的主要内容,如果未能解决你的问题,请参考以下文章

如何将 curl -T 命令翻译为邮递员?

为啥 Python 在编译为字节码之前不评估常数算术?

编译原理实战入门:用 JavaScript 写一个简单的四则运算编译器词法分析

为啥将class反编译为java后,java直接编译时有错误

将 protobuf-net RuntimeTypeModel 编译为 C#,而不是 DLL

编译器入门