Del*_*mer 13 c++ llvm compiler-optimization
对于上下文,假设我正在编写一个字节码解释器,其中指令遵循两种格式之一:
\n+---+--------+------+------+----------------+\n| 1 | opcode | treg | areg | unused |\n+---+--------+------+------+----------------+\n32 31 20 16 12 0\n\n+---+--------+------+-----------------------+\n| 0 | opcode | treg | imm |\n+---+--------+------+-----------------------+\n32 31 20 16 0\nRun Code Online (Sandbox Code Playgroud)\n对于每个操作码,有两个选项来提供其第二个操作数:它可以是寄存器或立即值。该选择由指令字的位 31 执行。如果 MSB 为 0 \xe2\x80\x94,则第二个操作数是寄存器的内容,由指令字的位 [12:16] 索引。如果 MSB 为 1 \xe2\x80\x94,则第二个操作数是指令本身的位 [0:16],符号扩展为完整机器字。
\n检索第二个操作数的简单算法只需根据 MSB 在两个单独的解析器之间切换:
\nreg_t decode_arg2(uint32_t instr, reg_t const* regs) {\n if (instr & (1 << 31)) {\n return regs[(instr >> 12) & 15];\n } else {\n return (int16_t)instr;\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n另一种可能性是使用无分支算法,我们将两个可能的值加载到临时变量中,然后使用位魔法选择其中一个:
\nreg_t decode_arg2_branchless(uint32_t instr, reg_t const* regs) {\n reg_t mask = (int32_t)instr >> 31;\n reg_t reg = regs[(instr >> 12) & 15];\n reg_t imm = (int16_t)instr;\n return (reg & mask) | (imm & ~mask);\n}\nRun Code Online (Sandbox Code Playgroud)\n使用 GCC 编译这两个函数并在人工压力测试中比较它们的性能后,无分支版本比分支版本快约 2.5 倍。因此,根据这一观察,希望位魔法保留在最终实现中似乎是合理的。
\n然而,当我们用 Clang 编译相同的代码时,它会尝试巧妙地使用无分支版本 \xe2\x80\x94,它正确地检测到这两个函数本质上执行相同的操作,但同样的知识使其发出具有完整分支的代码对于这两种情况。在 Godbolt 上,使用 Clang 16.0.0-O2生成此程序集:
decode_arg2(unsigned int, long const*):\n test edi, edi\n js .LBB0_1\n movsx rax, di\n ret\n.LBB0_1:\n shr edi, 12\n and edi, 15\n mov rax, qword ptr [rsi + 8*rdi]\n ret\ndecode_arg2_branchless(unsigned int, long const*):\n test edi, edi\n jns .LBB1_1\n shr edi, 12\n and edi, 15\n mov rax, qword ptr [rsi + 8*rdi]\n ret\n.LBB1_1:\n movsx rax, di\n ret\nRun Code Online (Sandbox Code Playgroud)\n似乎没有明显的方法来阻止 Clang 这样做,所以这就是主要问题出现的地方:如何阻止 Clang 将位掩码转换为完整分支?
\n到目前为止我自己发现的一些花絮:
\n在 Godbolt 上,您可以在“LLVM Opt Pipeline”窗口中详细查看每个优化过程。使用这个,我们可以看到:
\nInstCombinePass检测我们使用的掩码模式并将其替换为select内在函数(对应于三元运算符)。X86 DAG->DAG Instruction Selection更改select为CMOV。这不是我们最初写的,但应该没问题。X86 cmov Conversion出于某种原因,认为这CMOV实际上并不适合这种情况,并将其扩展为真正的分支。因此,如果我们可以禁用这些通道之一,问题就应该得到解决。但是,我没有找到禁用特定优化通道的官方方法。即使有一个,我希望它能够适用于单个功能的规模(这样它们仍然可以在其他地方工作,在那里它们更有可能受益),并通过整个程序优化“生存”在链接阶段。
\n一些消息来源建议用 标记分支条件__builtin_unpredictable,以阻止X86 cmov Conversion扩大选择范围。由于源代码中没有实际的分支,我什至不知道将其放在哪里。将其挂在mask\ 的值上似乎没有效果。
确实可以将函数体硬编码为内联汇编,但我认为这是最后的手段,只有在没有其他方法使 LLVM 发出我想要的代码时才使用。
\n如果用算术替换按位运算,如下所示:
\ndecode_arg2(unsigned int, long const*):\n test edi, edi\n js .LBB0_1\n movsx rax, di\n ret\n.LBB0_1:\n shr edi, 12\n and edi, 15\n mov rax, qword ptr [rsi + 8*rdi]\n ret\ndecode_arg2_branchless(unsigned int, long const*):\n test edi, edi\n jns .LBB1_1\n shr edi, 12\n and edi, 15\n mov rax, qword ptr [rsi + 8*rdi]\n ret\n.LBB1_1:\n movsx rax, di\n ret\nRun Code Online (Sandbox Code Playgroud)\n它仍然比按位版本慢大约 15%(再次使用 GCC 进行测试),但它现在已经足够模糊,可以让 LLVM 使用实际的imul和add“插值”按原样转换代码。因此,这种方法可以用作内联汇编之后的下一个最佳方法。尽管如此,还是有 15% 的速度下降,而且这是在我的特定 CPU 上(所以在其他 CPU 上情况可能会更糟);并且 LLVM 的未来版本也有可能开始检测这种模式。
如果您想自己测试性能,这里是我使用的源代码:
\nreg_t decode_arg2_interp(uint32_t instr, reg_t const* regs) {\n reg_t t = instr >> 31;\n reg_t reg = regs[(instr >> 12) & 15];\n reg_t imm = (int16_t)instr;\n return reg * t + imm * (1 - t);\n}\nRun Code Online (Sandbox Code Playgroud)\n编译它g++ -DBRANCH decode.cpp -o decode_branch -O2(替换-DBRANCH为适当的变体)并运行time ./decode_branch。请注意,要获取实际-DBIT版本,您需要专门使用 GCC \xe2\x80\x94,因为这是这个问题的主题,Clang (LLVM) 在优化过程中将其替换为等效的-DBRANCH.
这是一种有效的方法,不依赖于特定于 x86 的行为(例如廉价的原子加载),并且据我估计很有可能继续工作:添加asm引用reg变量的空语句。
这似乎工作正常并生成良好、简短的代码。
reg_t decode_arg2(uint32_t instr, reg_t const* regs) {
reg_t reg = regs[(instr >> 12) & 15];
asm("" : "+r"(reg));
reg_t imm = (int16_t)instr;
return (int32_t) instr < 0 ? reg : imm;
}
Run Code Online (Sandbox Code Playgroud)
使用 clang-16 生成的程序集-O2:
mov eax, edi
shr eax, 12
and eax, 15
mov rcx, qword ptr [rsi + 8*rax]
movsx rax, di
test edi, edi
cmovs rax, rcx
ret
Run Code Online (Sandbox Code Playgroud)
只要 clang 没有获得检查内联汇编的能力,我认为我们就没有问题。我对其工作原理的假设(有更好洞察力的人可能会纠正我):与具有分支的版本相比,通过内存负载的依赖链非常长。通过添加一些程序集,我们可以防止 clang 评估依赖链。
在查看了其他编译目标(例如 RISC-V)之后,即使具有显式屏蔽的代码版本也会创建分支,并将代码与asm volatilejust进行比较asm,我认为这实际上只是对依赖链的评估。
| 归档时间: |
|
| 查看次数: |
370 次 |
| 最近记录: |