c# - 如何有效地将 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
- 在下一步中,我尝试使用
unsafe
技巧:
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
- 在下一步中,我删除了 temp 变量(我认为它会有所帮助)。
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
一个?
笔记
如果您想玩示例:这里是 SharpLab 链接。
基准测试结果:
开启迭代x64/Release
:5000000000
- H() 拿走了
~1320ms
- 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);
解决方案
从 a 读取 4 个字节bool
会生成首先溢出到内存然后重新加载的代码,这并不奇怪,因为这是一件很奇怪的事情。
如果您要为类型双关语处理不安全的指针转换,那么您当然应该将 bool 读入相同大小的整数类型,例如C#unsigned char
或uint8_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
值不是0
or 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 的对象表示为orbool
0
1
。我假设在大多数 C# 实现中也是如此,而不仅仅是具有 0 / 一些可能不是 1 的非零值的字节。
(但即使你确实有一个任意的非零值,将其布尔化为 0 / 1 的正常方法是// xor eax, eax
。即实现整数字节。)test cl,cl
setnz al
int retval = !!x
x
内联时的真实用例:
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 al
将movzx 零扩展从关键路径中移除延迟。(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
推荐阅读
- reactjs - MongoDB 与 MongoDB Atlas
- javascript - FileSaver.js -.saveAs:如何添加监听器
- python - 使用 starlette 配置的 Fastapi 数据库测试隔离
- rust - 为什么 Rust 编译器没有检测到未使用的借用引用?
- arrays - Hackerrank 数组操作辅助
- python - 根据包含公共值的索引过滤列表列表
- c# - Unity3d中的圆锥截头体
- ios - 导航栏颜色/斯威夫特
- android - 错误:无法找到或加载主类 org.gradle.wrapper.GradleWrapperMain 并且不存在 gradle-wrapper.jar
- android - 使用 Kotlin 解析 JSON 的关键过滤器 HashMap?改造 - GSON