如何高效地将bool转换为int?

5 c# optimization assembly x86-64

我想将 a 转换boolint. “标准”选项是:

static int F(bool b) 
{
    int i = Convert.ToInt32(b);

    return i;
}

//ILSpy told me about this
public static int ToInt32(bool value)
{
    if (!value)
    {
        return 0;
    }
    return 1;
}
Run Code Online (Sandbox Code Playgroud)

此代码生成以下程序集:

<Program>$.<<Main>$>g__F|0_0(Boolean)
    L0000: test cl, cl
    L0002: jne short L0008
    L0004: xor eax, eax
    L0006: jmp short L000d
    L0008: mov eax, 1
    L000d: ret
Run Code Online (Sandbox Code Playgroud)

您可能已经注意到,这是将 a 转换boolint.

我尝试过的

寻找以下程序集,该程序集由 生成GCC

代码:

<Program>$.<<Main>$>g__F|0_0(Boolean)
    L0000: test cl, cl
    L0002: jne short L0008
    L0004: xor eax, eax
    L0006: jmp short L000d
    L0008: mov eax, 1
    L000d: ret
Run Code Online (Sandbox Code Playgroud)

汇编:

f(bool):
        movzx   eax, cl
        ret
Run Code Online (Sandbox Code Playgroud)
  • 在第一步中,我组合了这些功能:
__attribute__((ms_abi)) 
int
f(bool b) {
        int i;
        i = (int)b;

        return i;
}
Run Code Online (Sandbox Code Playgroud)

我认为这有点帮助(请参阅代码中的注释)。

<Program>$.<<Main>$>g__G|0_1(Boolean)
    L0000: test cl, cl
    L0002: jne short L0007
    L0004: xor eax, eax
    L0006: ret            ; This returns directly instead of jumping to RET instruction.
    L0007: mov eax, 1
    L000c: ret
Run Code Online (Sandbox Code Playgroud)
  • 在下一步中我尝试使用unsafe技巧:
f(bool):
        movzx   eax, cl
        ret
Run Code Online (Sandbox Code Playgroud)

这会生成:

<Program>$.<<Main>$>g__H|0_2(Boolean)
    L0000: mov [rsp+8], ecx           ; it looks better but I can't get rid of this line
    L0004: mov eax, [rsp+8]           ; it looks better but I can't get rid of this line
    L0008: movzx eax, al
    L000b: ret
Run Code Online (Sandbox Code Playgroud)
  • 在下一步中,我删除了临时变量(我认为这会有所帮助)。
static int G(bool b) 
{
    int i = b == true ? 1 : 0;

    return i;
}
Run Code Online (Sandbox Code Playgroud)

这会生成相同的ASM

<Program>$.<<Main>$>g__G|0_1(Boolean)
    L0000: test cl, cl
    L0002: jne short L0007
    L0004: xor eax, eax
    L0006: ret            ; This returns directly instead of jumping to RET instruction.
    L0007: mov eax, 1
    L000c: ret
Run Code Online (Sandbox Code Playgroud)

问题

正如你所看到的,我被困在这里(我不知道如何删除前两条指令)。有没有办法将bool变量转换为变量int

笔记

  • 如果您想玩示例:这里是 SharpLab 链接。

  • 基准测试结果:

进行x64/Release迭代5000000000

  1. H() 取~1320ms
  2. F() 取~1610ms
  • 包括基准测试代码:
static unsafe int H(bool b) 
{
    int i = *(int*)&b;         

    return i;
}
Run Code Online (Sandbox Code Playgroud)

Pet*_*des 4

从 a 中读取 4 个字节生成的代码首先溢出到内存,然后重新加载,这并不奇怪bool,因为这是一件奇怪的事情。

如果您打算使用不安全的指针强制转换来进行类型双关,那么您当然应该将 bool 读入相同大小的整数类型,例如unsigned charoruint8_t或任何等效的 C# 类型,然后强制转换(或隐式转换)该窄类型输入到int. 显然是这样Byte

using System;
static unsafe int H(bool b) 
{
    return *(Byte*)&b;         
}
Run Code Online (Sandbox Code Playgroud)

Sharplab 上的 asm,请参阅下面的内联到H(a == b).

<Program>$.<<Main>$>g__H|0_0(Boolean)
    L0000: mov eax, ecx
    L0002: ret
Run Code Online (Sandbox Code Playgroud)

显然,ABI/调用约定已经将“bool”符号或零扩展之类的狭窄参数传递到了 32 位。否则这比我意识到的更不安全,并且实际上会导致int不是0or 的值1

如果我们获取一个尚未在寄存器中的布尔指针,我们会得到一个 movzx-load :

static unsafe int from_mem(bool *b) 
{
    return *(Byte*)b;
}
Run Code Online (Sandbox Code Playgroud)
<Program>$.<<Main>$>g__from_mem|0_1(Boolean*)
    L0000: movzx eax, byte ptr [rcx]
    L0003: ret
Run Code Online (Sandbox Code Playgroud)

回复:性能优势

评论中提出了一些关于哪个实际上更好的问题。(还有一些关于代码大小和前端获取的无意义性能声明,我在评论中回复了它们。)

如果分支通常更好,C 和 C++ 编译器会这样做,但他们没有。这在当前的 C# 实现中是一个非常缺失的优化;在我看来,分支汇编太疯狂了。 可能/希望这会随着热代码路径的第二阶段 JIT 而消失,在这种情况下,乱搞unsafe可能会让事情变得更糟。因此,测试真实用例有一些优点。

movzx eax, cl在当前的 Intel CPU 上具有零延迟x86 的 MOV 真的可以“免费”吗?为什么我根本无法重现这个?),或者在 AMD 上具有 1 个周期延迟。(https://uops.info/https://agner.org/optimize/)。因此,前端的唯一成本是 1 uop,以及对输入的数据依赖。(即,在int值准备好之前,该值尚未准备好供后续指令使用bool,就像正常操作一样,例如+

分支具有现在使用结果并在 bool 实际可用时验证其正确性的可能优点(分支预测 + 推测执行打破数据依赖性),但也有一个巨大的缺点,即分支错误预测会使管道停滞约 15 个周期,并且浪费了自分支以来所做的任何工作。除非它非常可预测,否则 movzx 会好得多。

“非常可预测”的最有可能的情况是一个永远不会改变的值,在这种情况下读取它应该很便宜(除非它在缓存中丢失)并且乱序执行可以很好且尽早地做到这一点,这将使 movzx很好,并且避免不必要地占用 CPU 分支预测器中的空间。

对 bool 进行分支来创建 0 / 1 基本上是使用分支预测来进行值预测。在极少数情况下,这当然可能是个好主意,但默认情况下这并不是您想要的。


movzxC 和 C++ 编译器可以在将 bool 扩展为 int 时使用,因为 ABI 保证/要求a 的对象表示bool0or1。我认为大多数 C# 实现也是如此,而不仅仅是带有 0 的字节/某个可能不是 1 的非零值。

(但即使你确实有一个任意的非零值,将其布尔化为 0 / 1 的正常方法是// xor eax, eax。即实现一个整数字节。)test cl,clsetnz alint retval = !!xx


内联时的真实用例:

static int countmatch(int total, int a, int b) {
    //return total + (a==b);   // C
    return total + H(a == b);
}
Run Code Online (Sandbox Code Playgroud)

夏普实验室

<Program>$.<<Main>$>g__countmatch|0_2(Int32, Int32, Int32)
    L0000: cmp edx, r8d
    L0003: sete al
    L0006: movzx eax, al
    L0009: add eax, ecx
    L000b: ret
Run Code Online (Sandbox Code Playgroud)

非常正常的代码生成;您对 C 编译器的期望是,只有一个错过的优化:应该使用xor eax,eax/ cmp /sete almovzx 零扩展从延迟的关键路径中删除。(AL 和 EAX 属于同一寄存器意味着即使在 Intel CPU 上,mov-elimination 也不适用)。Clang、gcc 和 MSVC 都这样做(https://godbolt.org/z/E9fKhh5K8),尽管较旧的 GCC 有时在其他更复杂的情况下无法避免使用 movzx,也许可以最大限度地减少寄存器压力。

Sharplab 似乎没有 AArch64 输出来让你看看它是否可以像 C 编译器那样编译为cmp w1, w2/ cinc w0, w0, eq。(除了条件选择之外,ARM64 还提供了csinc条件选择增量,它与零寄存器一起使用来构建cset(x86 setcc) 和cinc(添加 FLAG 条件)。)我不会太乐观;我猜想可能仍在将布尔值具体化到寄存器中并添加它。

static int countmatch_safe(int total, int a, int b) {
    return total + Convert.ToInt32(a == b);
}
Run Code Online (Sandbox Code Playgroud)

如果没有unsafeC#,愚蠢的代码生成内联并仍然具体化了一个布尔值add,而不是围绕inc. if(a==b) total++;这比按您期望的方式编译的情况还要糟糕。

<Program>$.<<Main>$>g__countmatch_safe|0_3(Int32, Int32, Int32)
    L0000: cmp edx, r8d
    L0003: je short L0009
    L0005: xor eax, eax
    L0007: jmp short L000e
    L0009: mov eax, 1
    L000e: add eax, ecx
    L0010: ret
Run Code Online (Sandbox Code Playgroud)