首页 > 解决方案 > How to us CIL opcode bne.un vs beq, and bne.un.s vs beq.s, unsigned vs signed equality

问题描述

I understand that beq is applicable to more scenarios than bne.un (and it's equality vs inequality), but I'm wondering if there's any difference. For instance, suppose I use a char and have x == y, I would expect beq.un, but that doesn't exist, so I get bne.un, but when i use x != y, I get beq. How is it that an unsigned comparison can lead to a signed comparison instruction? Does it even matter? (hint: probably not, because that is what the compilers do, but why then the difference?).

Example:

  .method public static char  test(char x) cil managed
  {
    // Code size       11 (0xb)
    .maxstack  8
    IL_0000:  ldc.i4.s   97
    IL_0002:  ldarg.0
    IL_0003:  beq.un.s   IL_0008

    IL_0005:  ldc.i4.s   65
    IL_0007:  ret

    IL_0008:  ldc.i4.s   66
    IL_000a:  ret
  }

vs:

  .method public static char  test(char x) cil managed
  {
    // Code size       11 (0xb)
    .maxstack  8
    IL_0000:  ldc.i4.s   97
    IL_0002:  ldarg.0
    IL_0003:  beq.s      IL_0008

    IL_0005:  ldc.i4.s   65
    IL_0007:  ret

    IL_0008:  ldc.i4.s   66
    IL_000a:  ret
  }

Example C# code:

// this compiles to `bne.un.s`
public class C {
    public bool M(char x) {
        if(x == 'A')
            return true;
        return false;
    }
}

// this compiles to `beq.s`
public class C {
    public bool M(char x) {
        if(x != 'A')
            return true;
        return false;
    }
}

See the sharplab code:

I'm surprised at the difference here between how == gets translated to the (seemingly more correct) bne.un.s, and that != gets translated to the more general beq.s without using any unsigned instruction.

The compiled JITted machine code doesn't show that this leads to different assembly (which is good):

    L0000: movzx eax, dx
    L0003: cmp eax, 0x41
    L0006: jne short L000e   ; will be je for `!=`
    L0008: mov eax, 1
    L000d: ret
    L000e: xor eax, eax
    L0010: ret

But is this always the case? The documentation on these opcodes doesn't suggest it is wrong to use either opcode as long as the outcome is correct.

Background: I came across this idiosyncrasy while working on this PR for F# on the not function and got curious.

标签: c#f#jitcil

解决方案


The "unsigned" bit is odd. The docs for beq don't mention signed/unsigned values or floats, but the docs for bne.un explicitly say that it applies to unsigned values and floats. However, the compiler uses bne.un for signed values as well as unsigned values.

Regardless, the docs for beq do say:

The effect is the same as performing a ceq instruction followed by a brtrue branch to the specific target instruction

while the docs for bne.un say:

The effect is identical to performing a ceq instruction followed by a brfalse branch to the specific target instruction

so I think it's safe to assume that they're exact opposites of each other, and can be used in any place that ceq can be used.

The docs for ceq mention floats, but not signed / unsigned ints.


My suspicion is that beq.un is so named because it branches if the values it's given are equal, or are unordered floats ("branch if equal or unordered floats). It's possible that an over-zealous documentation writer misinterpreted "un" to mean "unsigned" (as it does for the rest of the bxx.un instructions), and this propagated.

Signedness matters when comparing ints (i.e. asking whether one is greater than or less than another), but not for testing whether they're equal: if both operands have the same signedness, you just need to check whether their bit patterns are identical. This is why you have e.g. both ble and ble.un, but there's no beq.un.


推荐阅读