[英]ASM In c++ project …How this little asm code will be in c++
[英]How to use c++ template to conditionally compile asm code?
有一个名为“ 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
}
}
当“启用”为true时,我想创建以下函数:
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
}
}
但是我不想保存两个代码副本,因为它们大多数都是相同的。 我想使用“ 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>();
似乎不可能编写类似函数test_tmp()的代码。 有人知道如何解决我的问题吗? 非常感谢。
如果您对前半部分的所有活动寄存器使用C临时变量并输出操作数,并与后半部分的输入约束对齐,则应该能够将其拆分为内联汇编,而不会造成任何性能损失,特别是如果您使用特定的内存输入/ output约束,而不是所有"memory"
破坏器。 但是它将变得更加复杂。
这显然是行不通的,因为C预处理程序会在 C ++编译器甚至查看if()
语句之前就运行。
if (Enable) {
#define FUSE_ // always defined, regardless of Enable
}
但是GNU汇编程序具有自己的宏/条件汇编指令,例如.if
,它对asm进行操作,在将文本替换为asm()
模板后,编译器会发出编译器发出的指令,包括立即输入操作数的实际数值。
bool
用作汇编器.if
指令的输入操作数 使用"i" (Enable)
输入约束。 通常,其%0
或%[enable]
扩展将为#0
或#1
,因为这是立即打印ARM的方式。 但是GCC具有%c0
/ %c[enable]
修饰符,该修饰符将打印常量而不标点。 (已针对x86进行了文档记录 ,但对ARM以及大概所有其他体系结构都以相同的方式进行工作。有关ARM / AArch64操作数修饰符的文档正在编写中 ;我一直在就此进行电子邮件咨询...)
".if %c[enable] \\n\\t"
代表[enable] "i" (c_var)
将.if 0
或.if 1
替换为 inline-asm模板,正是我们需要制作.if
/的内容.endif
在组装时进行.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 *);
在Godbolt编译器资源管理器中使用GCC和Clang进行编译
使用gcc,您只能获得编译器的.s
输出,因此您必须关闭一些常用的编译器-浏览器过滤器并查看指令。 所有3条vadd.f32
指令均以false
版本存在,但其中之一被.if 0
/ .endif
。
但是clang的内置汇编器在内部处理汇编器指令,如果请求该输出,则将其转换回asm。 (通常,clang / LLVM直接使用机器代码,不像gcc总是运行单独的汇编程序)。
需要明确的是,这适用于gcc 和 clang,但是在带有clang的Godbolt上更容易看到它。 (因为除了x86,Godbolt没有“ binary”模式,该模式实际上会进行组装然后反汇编)。 false
版本的Clang输出
...
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]
...
注意,clang为原始指针选择了与用于内存操作数相同的GP寄存器。 (gcc似乎为src_mem选择[sp]
,但是为您在寻址模式下手动使用的指针输入设置了另一个reg)。 如果没有强迫它将指针放在寄存器中,它可能已经使用了相对于SP的寻址模式,并为矢量加载添加了偏移量,这可能利用了ARM寻址模式。
如果您真的不想修改asm中的指针(例如,使用后递增寻址模式),那么"r"
仅输入操作数最有意义。 如果我们留在printf
循环中,则编译器将在asm之后再次需要dst
,因此将其保留在寄存器中将会受益。 "+r"(dst_addr)
输入强制编译器假定该寄存器不再可用作dst
的副本。 无论如何,即使我以后将它"r"
或"+r"
,gcc 总是会复制寄存器,即使以后不再需要它也是如此。
使用(虚拟)内存输入/输出意味着我们可以删除volatile
,因此编译器可以正常地将其优化为输入的纯函数。 (如果未使用结果,请对其进行优化。)
希望这不会比使用"memory"
破坏器更糟糕的代码生成。 但是,如果只使用 "=m"
和"m"
内存操作数,并且根本不要求寄存器中提供指针,那可能会更好。 (但是,如果您要使用内联asm遍历数组,则无济于事。)
另请参见使用内联汇编循环遍历数组
几年来我一直没有做过ARM汇编,而且我从来没有真正为正确地学习GCC内联汇编而烦恼,但是我认为可以使用内在函数像这样重写您的代码:
#include <cstdio>
#include <arm_neon.h>
template<bool Enable>
void test_tmp()
{
const float32x4_t src = {1.0, 2.0, 3.0, 4.0};
const float32x4_t src2 = {1.0, 1.0, 1.0, 1.0};
float32x4_t z;
z = vaddq_f32(src, src2);
z = vaddq_f32(z, src2);
if (Enable) z = vaddq_f32(z, src2);
float result[4];
vst1q_f32(result, z);
for (int i = 0; i < 4; i++)
{
printf("%f, ", result[i]);//0.0 0.0 0.0 0.0
}
}
template void test_tmp<true>();
template void test_tmp<false>();
您可以在以下网址实时查看生成的机器代码和玩具: https : //godbolt.org/z/Fg7Tci
使用ARM gcc8.2和命令行选项“ -O3 -mfloat-abi = softfp -mfpu = neon”进行编译时,“ true”变体为:
void test_tmp<true>():
vmov.f32 q9, #1.0e+0 @ v4sf
vldr d16, .L6
vldr d17, .L6+8
# and the FALSE variant has one less vadd.f32 in this part
vadd.f32 q8, q8, q9
vadd.f32 q8, q8, q9
vadd.f32 q8, q8, q9
push {r4, r5, r6, lr}
sub sp, sp, #16
vst1.32 {d16-d17}, [sp:64]
mov r4, sp
ldr r5, .L6+16
add r6, sp, #16
.L2:
vldmia.32 r4!, {s15}
vcvt.f64.f32 d16, s15
mov r0, r5
vmov r2, r3, d16
bl printf
cmp r4, r6
bne .L2
add sp, sp, #16
pop {r4, r5, r6, pc}
.L6:
.word 1065353216
.word 1073741824
.word 1077936128
.word 1082130432
.word .LC0
.LC0:
.ascii "%f, \000"
为什么gcc不能简单地用输入值作为输出字符串来计算最终字符串,因为输入是恒定的,这仍然让我感到非常困惑。 也许这是关于精度的一些数学规则,以防止在编译时执行该操作,因为结果可能与实际目标硬件平台FPU略有不同? 即与一些快速开关,它可能会完全删除该代码,只产生一个输出字符串...
但是我想您的代码实际上并不是您正在做的事情的“ MCVE”,并且测试值将被馈送到您正在测试的某些实际函数中,或类似的东西。
无论如何,如果您正在进行性能优化,则可能应该完全避免内联汇编,而应使用内在函数,因为这可以使编译器更好地分配寄存器并优化计算周围的代码(我没有精确地跟踪它,但是我认为该实验在Godbolt中的最新版本比使用内联汇编的原始指令短/简单了2-4条指令。
另外,您将避免使用示例代码所具有的错误的asm约束,如果您经常修改内联代码,那么获取正确的纯PITA总是很棘手。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.