首页 > 解决方案 > 如何有效地将 bool 转换为 int?

问题描述

我想将 abool转换为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

您可能已经注意到,这是一种将 abool转换为int.

我试过的

搜寻是针对由以下程序生成的GCC

代码:

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

        return i;
}

汇编:

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

问题

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

笔记

开启迭代x64/Release5000000000

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

标签: c#assemblyoptimizationx86-64

解决方案


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

如果您要为类型双关语处理不安全的指针转换,那么您当然应该将 bool 读入相同大小的整数类型,例如C#unsigned charuint8_t任何等效的 C#,然后转换(或隐式转换)那个窄键入到int。显然是这样的Byte

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

asm 在 Sharplab 上,并在下面看到这个内联到调用者的H(a == b).

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

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

如果我们采用一个不在寄存器中的指向布尔值的指针,我们会得到一个 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# 实现中非常遗漏的优化;IMO,分支 asm 太疯狂了。 可能/希望这会随着热代码路径的第二阶段 JITing 而消失,在这种情况下,搞乱unsafe可能会使事情变得更糟。因此,测试真实用例有一些好处。

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

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

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

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


C 和 C++ 编译器可以movzx在将 bool 扩展为 int 时使用,因为ABI 保证/要求a 的对象表示为orbool01。我假设在大多数 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);
}

夏普实验室

<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 almovzx 零扩展从关键路径中移除延迟。(AL 和 EAX 是同一个寄存器的一部分,这意味着即使在 Intel CPU 上,mov-elimination 也不适用)。Clang、gcc 和 MSVC 执行此操作 ( https://godbolt.org/z/E9fKhh5K8 ),尽管在其他更复杂的情况下,旧 GCC 有时难以避免 movzx,可能会最大限度地减少寄存器压力。

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

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

如果没有unsafe在 C# 中,愚蠢的代码生成内联并仍然实现一个布尔值 for 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

推荐阅读