mao*_*ofu 2 c++ assembly templates conditional-statements
有一个名为“Enable”的布尔变量,当“Enable”为false时,我想创建以下函数:
void test_false()
{
float dst[4] = {1.0, 1.0, 1.0, 1.0};
float src[4] = {1.0, 2.0, 3.0, 4.0};
float * dst_addr = dst;
float * src_addr = src;
asm volatile (
"vld1.32 {q0}, [%[src]] \n"
"vld1.32 {q1}, [%[dst]] \n"
"vadd.f32 q0, q0, q1 \n"
"vadd.f32 q0, q0, q1 \n"
"vst1.32 {q0}, [%[dst]] \n"
:[src]"+r"(src_addr),
[dst]"+r"(dst_addr)
:
: "q0", "q1", "q2", "q3", "memory"
);
for (int i = 0; i < 4; i++)
{
printf("%f, ", dst[i]);//0.0 0.0 0.0 0.0
}
}
Run Code Online (Sandbox Code Playgroud)
当“启用”为真时,我想创建以下功能:
void test_true()
{
float dst[4] = {1.0, 1.0, 1.0, 1.0};
float src[4] = {1.0, 2.0, 3.0, 4.0};
float * dst_addr = dst;
float * src_addr = src;
asm volatile (
"vld1.32 {q0}, [%[src]] \n"
"vld1.32 {q1}, [%[dst]] \n"
"vadd.f32 q0, q0, q1 \n"
"vadd.f32 q0, q0, q1 \n"
"vadd.f32 q0, q0, q1 \n" //Only here is different from test_false()
"vst1.32 {q0}, [%[dst]] \n"
:[src]"+r"(src_addr),
[dst]"+r"(dst_addr)
:
: "q0", "q1", "q2", "q3", "memory"
);
for (int i = 0; i < 4; i++)
{
printf("%f, ", dst[i]);//0.0 0.0 0.0 0.0
}
}
Run Code Online (Sandbox Code Playgroud)
但是我不想保存两份代码,因为大部分都是一样的。我想用“c++模板+条件编译”来解决我的问题。代码如下。但它没有用。无论 Enable 是 true 还是 false,编译器都会创建与 test_true() 相同的代码。
template<bool Enable>
void test_tmp()
{
float dst[4] = {1.0, 1.0, 1.0, 1.0};
float src[4] = {1.0, 2.0, 3.0, 4.0};
float * dst_addr = dst;
float * src_addr = src;
if (Enable)
{
#define FUSE_
}
asm volatile (
"vld1.32 {q0}, [%[src]] \n"
"vld1.32 {q1}, [%[dst]] \n"
"vadd.f32 q0, q0, q1 \n"
"vadd.f32 q0, q0, q1 \n"
#ifdef FUSE_
"vadd.f32 q0, q0, q1 \n"
#endif
"vst1.32 {q0}, [%[dst]] \n"
:[src]"+r"(src_addr),
[dst]"+r"(dst_addr)
:
: "q0", "q1", "q2", "q3", "memory"
);
for (int i = 0; i < 4; i++)
{
printf("%f, ", dst[i]);//0.0 0.0 0.0 0.0
}
#undef FUSE_
}
template void test_tmp<true>();
template void test_tmp<false>();
Run Code Online (Sandbox Code Playgroud)
似乎不可能编写像函数 test_tmp() 这样的代码。有谁知道如何解决我的问题?非常感谢。
如果您对前半部分的所有实时寄存器使用 C 临时变量和输出操作数,这些寄存器与后半部分的输入约束一致,您应该能够将其拆分为内联 asm,而不会造成任何性能损失,尤其是如果您使用特定的内存输入/output 约束而不是"memory"包罗万象的破坏。但它会变得更加复杂。
这显然不起作用,因为 C 预处理器在C++ 编译器甚至查看if()语句之前运行。
if (Enable) {
#define FUSE_ // always defined, regardless of Enable
}
Run Code Online (Sandbox Code Playgroud)
但是 GNU 汇编器有自己的宏/条件汇编指令,例如.if在将文本替换到asm()模板中后编译器发出的 asm 上运行的指令,包括直接输入操作数的实际数值。
bool用作汇编指令的输入操作数.if使用"i" (Enable)输入约束。通常,%0or 的%[enable]扩展将是#0or #1,因为这是立即打印 ARM 的方法。但是 GCC 有一个%c0/%c[enable]修饰符,可以打印一个没有标点符号的常量。(它已针对 x86 进行了记录,但对 ARM 和大概所有其他体系结构的工作方式相同。正在处理 ARM / AArch64 操作数修饰符的文档;我一直在写一封关于此的电子邮件......)
".if %c[enable] \n\t"for[enable] "i" (c_var)将替换为.if 0或.if 1到内联汇编模板中,这正是我们在组装时需要制作.if/.endif工作的内容。
完整示例:
template<bool Enable>
void test_tmp(float dst[4])
{
//float dst[4] = {1.0, 1.0, 1.0, 1.0};
// static const // non-static-const so we can see the memory clobber vs. dummy src stop this from optimizing away init of src[] on the stack
float src[4] = {1.0, 2.0, 3.0, 4.0};
float * dst_addr = dst;
const float * src_addr = src;
asm (
"vld1.32 {q1}, [%[dst]] @ dummy dst = %[dummy_memdst]\n" // hopefully they pick the same regs?
"vld1.32 {q0}, [%[src]] @ dummy src = %[dummy_memsrc]\n"
"vadd.f32 q0, q0, q1 \n" // TODO: optimize to q1+q1 first, without a dep on src
"vadd.f32 q0, q0, q1 \n" // allowing q0+=q1 and q1+=q1 in parallel if we need q0 += 3*q1
// #ifdef FUSE_
".if %c[enable]\n" // %c modifier: print constant without punctuation, same as documented for x86
"vadd.f32 q0, q0, q1 \n"
".endif \n"
// #endif
"vst1.32 {q0}, [%[dst]] \n"
: [dummy_memdst] "+m" (*(float(*)[4])dst_addr)
: [src]"r"(src_addr),
[dst]"r"(dst_addr),
[enable]"i"(Enable)
, [dummy_memsrc] "m" (*(const float(*)[4])src_addr)
: "q0", "q1", "q2", "q3" //, "memory"
);
/*
for (int i = 0; i < 4; i++)
{
printf("%f, ", dst[i]);//0.0 0.0 0.0 0.0
}
*/
}
float dst[4] = {1.0, 1.0, 1.0, 1.0};
template void test_tmp<true>(float *);
template void test_tmp<false>(float *);
Run Code Online (Sandbox Code Playgroud)
在 Godbolt 编译器浏览器上使用 GCC 和 Clang 进行编译
使用 gcc,您只能获得编译器的.s输出,因此您必须关闭一些常用的编译器-资源管理器过滤器并查看指令。版本中包含所有 3vadd.f32条指令false,但其中一条指令被.if 0/包围.endif。
但是,如果请求输出,clang 的内置汇编器会在内部处理汇编器指令,然后再将其转回 asm。(通常 clang/LLVM 直接进入机器代码,不像 gcc 总是运行单独的汇编程序)。
需要明确的是,这适用于 gcc和clang,但在 Godbolt 上使用 clang 更容易看到它。(因为除了 x86,Godbolt 没有实际组装然后反汇编的“二进制”模式)。 版本的Clang 输出false
...
vld1.32 {d2, d3}, [r0] @ dummy dst = [r0]
vld1.32 {d0, d1}, [r1] @ dummy src = [r1]
vadd.f32 q0, q0, q1
vadd.f32 q0, q0, q1
vst1.32 {d0, d1}, [r0]
...
Run Code Online (Sandbox Code Playgroud)
请注意,clang 为原始指针选择了与用于内存操作数相同的 GP 寄存器。(gcc 似乎[sp]为 src_mem选择,但为您在寻址模式中手动使用的指针输入选择了不同的 reg)。如果您没有强迫它在寄存器中使用指针,它可能会使用 SP 相对寻址模式,并为向量加载提供偏移量,从而可能利用 ARM 寻址模式。
如果您真的不打算修改 asm 内的指针(例如使用后增量寻址模式),那么"r"仅输入操作数最有意义。如果我们留在printf循环中,编译器将dst在 asm 之后再次需要,因此将它保留在寄存器中会受益。甲"+r"(dst_addr)输入力编译器假定那个寄存器不再可用作副本dst。无论如何,gcc总是复制寄存器,即使它以后不需要它,无论是我做的"r"还是"+r",所以这很奇怪。
使用(虚拟)内存输入/输出意味着我们可以删除volatile,因此编译器可以正常优化它作为其输入的纯函数。(如果结果未使用,则将其优化掉。)
希望这不会比使用"memory"clobber 的代码生成更糟糕。但它可能会更好,如果你只是使用的"=m"和"m"存储器操作数,并没有要求在所有的寄存器指针。(不过,如果您要使用内联 asm 遍历数组,那将无济于事。)
另请参阅使用内联汇编循环遍历数组