5 c# optimization assembly x86-64
我想将 a 转换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;
}
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 转换bool为int.
寻找以下程序集,该程序集由 生成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:
~1320ms~1610msstatic unsafe int H(bool b)
{
int i = *(int*)&b;
return i;
}
Run Code Online (Sandbox Code Playgroud)
从 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 的对象表示bool为0or1。我认为大多数 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 al将movzx 零扩展从延迟的关键路径中删除。(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)
| 归档时间: |
|
| 查看次数: |
3814 次 |
| 最近记录: |