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

How to convert bool to int efficiently?

我想将 bool 转换为 int。 “标准”选项是:

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;
}

此代码生成以下程序集:

<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

您可能已经注意到,将 bool 转换为 int.

的方法有问题(我认为是,我不是专家)

我试过的

搜寻由 GCC 生成的以下程序集:

代码:

__attribute__((ms_abi)) 
int
f(bool b) {
        int i;
        i = (int)b;

        return i;
}

asm:

f(bool):
        movzx   eax, cl
        ret
static int G(bool b) 
{
    int i = b == true ? 1 : 0;

    return i;
}

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

<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
static unsafe int H(bool b) 
{
    int i = *(int*)&b;         

    return i;
}

这会生成:

<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
static unsafe int Y(bool b) 
{
    return *(int*)&b;
}

这生成相同的 ASM:

<Program>$.<<Main>$>g__Y|0_3(Boolean)
    L0000: mov [rsp+8], ecx
    L0004: mov eax, [rsp+8]
    L0008: movzx eax, al
    L000b: ret

问题

如您所见,我被困在这里(我不知道如何删除前 2 条指令)。有没有办法将 bool 变量转换为 int 变量?

备注

x64/Release 上进行 5000000000 次迭代:

  1. H() 占了 ~1320ms
  2. F() 占了 ~1610ms
var w = new Stopwatch();

long r = 0;
for (int i = 0; i < 10; ++i)
{
    w.Restart();
    for (long j = 0; j < 5000000000; ++j)
    {
        F(true);
        F(false);
    }
    w.Stop();
    r += w.ElapsedMilliseconds;
    Console.WriteLine(w.ElapsedMilliseconds);
}

Console.WriteLine("AV" + r / 10);

在转换可能产生影响的系统中,最有效的方法是不转换并将 true 和 false 保留为目标类型(intbyte,等等)。

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

如果您要为类型双关而使用不安全的指针转换,当然您应该将 bool 读入相同大小的整数类型,例如 unsigned charuint8_t 或 C# 具有的任何等效项,然后 然后 将缩小类型转换(或隐式转换)为 int。显然是 Byte.

using System;
static unsafe int H(bool b) 
{
    return *(Byte*)&b;         
}

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

的调用者
<Program>$.<<Main>$>g__H|0_0(Boolean)
    L0000: mov eax, ecx
    L0002: ret

显然,ABI/调用约定已经将窄参数(如“bool”符号或零扩展)传递到 32 位。否则这比我意识到的更不安全,实际上会导致 int 值不是 01!

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

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

回复:性能优势

评论中提出了一些关于哪个更好的问题。 (以及我在评论中回复的关于代码大小和前端获取的一些荒谬的性能声明。)

如果分支总体上更好,C 和 C++ 编译器会这样做,但它们不会。在当前的 C# 实现中,这是一个非常错过的​​优化;那个分支 asm 是疯了,IMO。 可能/希望这会随着热代码路径的第二阶段 JIT 消失,在这种情况下,乱用 unsafe 可能会使事情变得更糟 。因此,测试真实的用例是有一些好处的。

movzx eax, cl 在当前英特尔 CPUs (), or 1 cycle latency on AMD. (https://uops.info/ and https://agner.org/optimize/) 上的延迟为零。因此,前端的唯一成本是 1 uop,以及对输入的数据依赖性。 (即,在 bool 值准备就绪之前,int 值无法供后续指令使用,就像 + 等正常操作一样)

分支有 可能 现在使用结果的好处,并在 bool 实际可用时验证它是否正确(分支预测 + 推测执行 break the data dependency),但有一个巨大的缺点,即分支错误预测会使管道停止约 15 个周期,并浪费自分支以来完成的所有工作。除非它非常可预测,否则movzx要好得多。

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

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


C 和 C++ 编译器在将 bool 扩展为 int 时可以使用 movzx,因为 。我假设在大多数 C# 实现中也是如此,而不仅仅是一个带有 0 / 一些可能不是 1 的非零值的字节。

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


内联时的实际用例:

static int countmatch(int total, int a, int b) {
    //return total + (a==b);   // C
    return total + H(a == b);
}

Sharplab

<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

相当正常的代码生成;你对 C 编译器的期望是什么,只是错过了一个优化:应该使用 xor eax,eax / cmp / sete al. (AL and EAX being part of the same register mean that even on Intel CPUs, mov-elimination doesn't apply). Clang, gcc, and MSVC do this (https://godbolt.org/z/E9fKhh5K8),尽管旧的 GCC 有时难以避免其他更多的 movzx复杂的情况,也许可以最大限度地减少套准压力。

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

static int countmatch_safe(int total, int a, int b) {
    return total + Convert.ToInt32(a == b);
}

在 C# 中没有 unsafe,愚蠢的代码生成内联并仍然具体化 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