16 字节 NEON 操作数的 gcc arm 内联汇编器 %e0 和 %f0 操作数修饰符?
gcc arm inline assembler %e0 and %f0 operand modifiers for 16-byte NEON operands?
找到以下内联汇编器code
来计算向量叉积:
float32x4_t cross_test( const float32x4_t& lhs, const float32x4_t& rhs )
{
float32x4_t result;
asm volatile(
"vext.8 d6, %e2, %f2, #4 \n\t"
"vext.8 d7, %e1, %f1, #4 \n\t"
"vmul.f32 %e0, %f1, %e2 \n\t"
"vmul.f32 %f0, %e1, d6 \n\t"
"vmls.f32 %e0, %f2, %e1 \n\t"
"vmls.f32 %f0, %e2, d7 \n\t"
"vext.8 %e0, %f0, %e0, #4 "
: "+w" ( result )
: "w" ( lhs ), "w" ( rhs )
: "d6", "d7" );
return result;
}
'%'
之后的修饰符 e
和 f
是什么意思(例如 %e2
)?我找不到任何参考资料。
这是gcc生成的汇编代码:
vext.8 d6, d20, d21, #4
vext.8 d7, d18, d19, #4
vmul.f32 d16, d19, d20
vmul.f32 d17, d18, d6
vmls.f32 d16, d21, d18
vmls.f32 d17, d20, d7
vext.8 d16, d17, d16, #4
我现在明白了所用修饰语的含义了。现在我试着遵循叉积算法。为此,我在汇编代码中添加了一些额外的注释,但结果并不符合我的预期:
// History:
// - '%e' = lower register part
// - '%f' = higher register part
// - '%?0' = res = [ x2 y2 | z2 v2 ]
// - '%?1' = lhs = [ x0 y0 | z0 v0 ]
// - '%?2' = rhs = [ x1 y1 | z1 v1 ]
// - '%e0' = [ x2 y2 ]
// - '%f0' = [ z2 v2 ]
// - '%e1' = [ x0 y0 ]
// - '%f1' = [ z0 v0 ]
// - '%e2' = [ x1 y1 ]
// - '%f2' = [ z1 v1 ]
// Implemented algorithm:
// |x2| |y0 * z1 - z0 * y1|
// |y2| = |z0 * x1 - x0 * z1|
// |z2| |x0 * y1 - y0 * x1|
asm (
"vext.8 d6, %e2, %f2, #4 \n\t" // e2=[ x1 y1 ], f2=[ z1 v1 ] -> d6=[ v1 x1 ]
"vext.8 d7, %e1, %f1, #4 \n\t" // e1=[ x0 y0 ], f1=[ z0 v0 ] -> d7=[ v0 x0 ]
"vmul.f32 %e0, %f1, %e2 \n\t" // f1=[ z0 v0 ], e2=[ x1 y1 ] -> e0=[ z0 * x1, v0 * y1 ]
"vmul.f32 %f0, %e1, d6 \n\t" // e1=[ x0 y0 ], d6=[ v1 x1 ] -> f0=[ x0 * v1, y0 * x1 ]
"vmls.f32 %e0, %f2, %e1 \n\t" // f2=[ z1 v1 ], e1=[ x0 y0 ] -> e0=[ z0 * x1 - z1 * x0, v0 * y1 - v1 * y0 ] = [ y2, - ]
"vmls.f32 %f0, %e2, d7 \n\t" // e2=[ x1 y1 ], d7=[ v0 x0 ] -> f0=[ x0 * v1 - x1 * v0, y0 * x1 - y1 * x0 ] = [ -, - ]
"vext.8 %e0, %f0, %e0, #4 " //
: "+w" ( result ) // Output section: 'w'='VFP floating point register', '+'='read/write'
: "w" ( lhs ), "w" ( rhs ) // Input section : 'w'='VFP floating point register'
: "d6", "d7" ); // Temporary 64[bit] register.
首先,这很奇怪。 result
没有在 asm 语句之前初始化,但它被用作 "+w" ( result )
的 input/output 操作数。我认为 "=w" (result)
会更好。这也是没有意义的volatile
;输出是输入的纯函数,没有副作用或不依赖于任何 "hidden" 输入,因此相同的输入每次都会产生相同的结果。因此,省略 volatile
将允许编译器对其进行 CSE 并在可能的情况下将其提升到循环之外,而不是每次源代码使用相同的输入运行它时都强制它重新计算。
我也找不到任何参考; gcc 手册的扩展 ASM 页面仅记录 operand modifiers for x86,不记录 ARM。
但我认为我们可以通过查看 asm 输出来了解操作数修饰符的作用:
%e0
替换为d16
,%f0
替换为d17
。 %e1
是 d18
,%f1
是 d19
。 %2
在 d20
和 d21
中
您的输入是 q
寄存器中的 16 字节 NEON 向量。在 ARM32 中,每个 q
寄存器的上半部分和下半部分都可以作为 d
寄存器单独访问。 (与 AArch64 不同,其中每个 s / d 寄存器都是不同 q reg 的底部元素。)看起来这段代码正在利用它通过在 [=29 的高低对上使用 64 位 SIMD 免费洗牌=]s,在进行 4 字节 vext
洗牌以混合这些浮点对后。
%e[operand]
是一个操作数的低d
寄存器,%f[operand]
是高d
寄存器。他们'没有记录,但 gcc 源代码说(在 gcc/config/arm/arm.c#L22486
中的 arm_print_operand
:
These two codes print the low/high doubleword register of a Neon quad
register, respectively. For pair-structure types, can also print
low/high quadword registers.
我没有测试如果将这些修饰符应用于 float32x2_t
等 64 位操作数会发生什么,这只是我从一个示例进行的逆向工程。但是,对此会有修饰符是完全有道理的。
x86 修饰符包括一个用于整数寄存器的低 8 位和高 8 位的修饰符(因此如果您在 EAX 中输入,则可以获得 AL / AH),因此部分寄存器的东西绝对是 GNU C 内联 asm 操作数修饰符可以做到。
请注意,未记录意味着不受支持。
我正在寻找%e0
& %f0
的意思,这个题目很有帮助。 cross_test()
输出可以解释如下:
#include <arm_neon.h>
#include <stdio.h>
float32x4_t cross_test(const float32x4_t& lhs, const float32x4_t& rhs) {
float32x4_t result;
// | f | e
// -----------------------------
// 1 | a3(4) a2(3) | a1(2) a0(1)
// 2 | b3(5) b2(6) | b1(7) b0(8)
asm volatile (
"vext.8 d6, %e1, %f1, #4" "\n" // a2, a1
"vext.8 d7, %e2, %f2, #4" "\n" // b2, b1
"vmul.f32 %e0, %f1, %e2" "\n" // a3*b1, a2*b0
"vmul.f32 %f0, %e1, d7" "\n" // a1*b2, a0*b1
"vmls.f32 %e0, %f2, %e1" "\n" // a3*b1-a1*b3(18), a2*b0-a0*b2(18)
"vmls.f32 %f0, %e2, d6" "\n" // a1*b2-a2*b1(-9), a0*b1-a1*b0(-9)
"vext.8 %e0, %f0, %e0, #4" "\n" // a2*b0-a0*b2(18), a1*b2-a2*b1(-9)
: "+w"(result) // %0
: "w"(lhs), // %1
"w"(rhs) // %2
: "d6", "d7"
);
return result;
}
#define nforeach(i, count) \
for (int i = 0, __count = static_cast<int>(count); i < __count; ++i)
#define dump_f128(qf) do { \
float *fp = reinterpret_cast<float *>(&qf); \
puts(#qf ":"); \
nforeach(i, 4) { \
printf("[%d]%f\n", i, fp[i]); \
} \
} while (0)
int main() {
float fa[] = {1., 2., 3., 4.};
float fb[] = {8., 7., 6., 5.};
float32x4_t qa, qb, qres;
qa = vld1q_f32(const_cast<const float *>(&fa[0]));
qb = vld1q_f32(const_cast<const float *>(&fb[0]));
qres = cross_test(qa, qb);
dump_f128(qa);
puts("---");
dump_f128(qb);
puts("---");
// -9, 18, -9, -9
dump_f128(qres);
return 0;
}
找到以下内联汇编器code
来计算向量叉积:
float32x4_t cross_test( const float32x4_t& lhs, const float32x4_t& rhs )
{
float32x4_t result;
asm volatile(
"vext.8 d6, %e2, %f2, #4 \n\t"
"vext.8 d7, %e1, %f1, #4 \n\t"
"vmul.f32 %e0, %f1, %e2 \n\t"
"vmul.f32 %f0, %e1, d6 \n\t"
"vmls.f32 %e0, %f2, %e1 \n\t"
"vmls.f32 %f0, %e2, d7 \n\t"
"vext.8 %e0, %f0, %e0, #4 "
: "+w" ( result )
: "w" ( lhs ), "w" ( rhs )
: "d6", "d7" );
return result;
}
'%'
之后的修饰符 e
和 f
是什么意思(例如 %e2
)?我找不到任何参考资料。
这是gcc生成的汇编代码:
vext.8 d6, d20, d21, #4
vext.8 d7, d18, d19, #4
vmul.f32 d16, d19, d20
vmul.f32 d17, d18, d6
vmls.f32 d16, d21, d18
vmls.f32 d17, d20, d7
vext.8 d16, d17, d16, #4
我现在明白了所用修饰语的含义了。现在我试着遵循叉积算法。为此,我在汇编代码中添加了一些额外的注释,但结果并不符合我的预期:
// History:
// - '%e' = lower register part
// - '%f' = higher register part
// - '%?0' = res = [ x2 y2 | z2 v2 ]
// - '%?1' = lhs = [ x0 y0 | z0 v0 ]
// - '%?2' = rhs = [ x1 y1 | z1 v1 ]
// - '%e0' = [ x2 y2 ]
// - '%f0' = [ z2 v2 ]
// - '%e1' = [ x0 y0 ]
// - '%f1' = [ z0 v0 ]
// - '%e2' = [ x1 y1 ]
// - '%f2' = [ z1 v1 ]
// Implemented algorithm:
// |x2| |y0 * z1 - z0 * y1|
// |y2| = |z0 * x1 - x0 * z1|
// |z2| |x0 * y1 - y0 * x1|
asm (
"vext.8 d6, %e2, %f2, #4 \n\t" // e2=[ x1 y1 ], f2=[ z1 v1 ] -> d6=[ v1 x1 ]
"vext.8 d7, %e1, %f1, #4 \n\t" // e1=[ x0 y0 ], f1=[ z0 v0 ] -> d7=[ v0 x0 ]
"vmul.f32 %e0, %f1, %e2 \n\t" // f1=[ z0 v0 ], e2=[ x1 y1 ] -> e0=[ z0 * x1, v0 * y1 ]
"vmul.f32 %f0, %e1, d6 \n\t" // e1=[ x0 y0 ], d6=[ v1 x1 ] -> f0=[ x0 * v1, y0 * x1 ]
"vmls.f32 %e0, %f2, %e1 \n\t" // f2=[ z1 v1 ], e1=[ x0 y0 ] -> e0=[ z0 * x1 - z1 * x0, v0 * y1 - v1 * y0 ] = [ y2, - ]
"vmls.f32 %f0, %e2, d7 \n\t" // e2=[ x1 y1 ], d7=[ v0 x0 ] -> f0=[ x0 * v1 - x1 * v0, y0 * x1 - y1 * x0 ] = [ -, - ]
"vext.8 %e0, %f0, %e0, #4 " //
: "+w" ( result ) // Output section: 'w'='VFP floating point register', '+'='read/write'
: "w" ( lhs ), "w" ( rhs ) // Input section : 'w'='VFP floating point register'
: "d6", "d7" ); // Temporary 64[bit] register.
首先,这很奇怪。 result
没有在 asm 语句之前初始化,但它被用作 "+w" ( result )
的 input/output 操作数。我认为 "=w" (result)
会更好。这也是没有意义的volatile
;输出是输入的纯函数,没有副作用或不依赖于任何 "hidden" 输入,因此相同的输入每次都会产生相同的结果。因此,省略 volatile
将允许编译器对其进行 CSE 并在可能的情况下将其提升到循环之外,而不是每次源代码使用相同的输入运行它时都强制它重新计算。
我也找不到任何参考; gcc 手册的扩展 ASM 页面仅记录 operand modifiers for x86,不记录 ARM。
但我认为我们可以通过查看 asm 输出来了解操作数修饰符的作用:
%e0
替换为d16
,%f0
替换为d17
。 %e1
是 d18
,%f1
是 d19
。 %2
在 d20
和 d21
您的输入是 q
寄存器中的 16 字节 NEON 向量。在 ARM32 中,每个 q
寄存器的上半部分和下半部分都可以作为 d
寄存器单独访问。 (与 AArch64 不同,其中每个 s / d 寄存器都是不同 q reg 的底部元素。)看起来这段代码正在利用它通过在 [=29 的高低对上使用 64 位 SIMD 免费洗牌=]s,在进行 4 字节 vext
洗牌以混合这些浮点对后。
%e[operand]
是一个操作数的低d
寄存器,%f[operand]
是高d
寄存器。他们'没有记录,但 gcc 源代码说(在 gcc/config/arm/arm.c#L22486
中的 arm_print_operand
:
These two codes print the low/high doubleword register of a Neon quad register, respectively. For pair-structure types, can also print low/high quadword registers.
我没有测试如果将这些修饰符应用于 float32x2_t
等 64 位操作数会发生什么,这只是我从一个示例进行的逆向工程。但是,对此会有修饰符是完全有道理的。
x86 修饰符包括一个用于整数寄存器的低 8 位和高 8 位的修饰符(因此如果您在 EAX 中输入,则可以获得 AL / AH),因此部分寄存器的东西绝对是 GNU C 内联 asm 操作数修饰符可以做到。
请注意,未记录意味着不受支持。
我正在寻找%e0
& %f0
的意思,这个题目很有帮助。 cross_test()
输出可以解释如下:
#include <arm_neon.h>
#include <stdio.h>
float32x4_t cross_test(const float32x4_t& lhs, const float32x4_t& rhs) {
float32x4_t result;
// | f | e
// -----------------------------
// 1 | a3(4) a2(3) | a1(2) a0(1)
// 2 | b3(5) b2(6) | b1(7) b0(8)
asm volatile (
"vext.8 d6, %e1, %f1, #4" "\n" // a2, a1
"vext.8 d7, %e2, %f2, #4" "\n" // b2, b1
"vmul.f32 %e0, %f1, %e2" "\n" // a3*b1, a2*b0
"vmul.f32 %f0, %e1, d7" "\n" // a1*b2, a0*b1
"vmls.f32 %e0, %f2, %e1" "\n" // a3*b1-a1*b3(18), a2*b0-a0*b2(18)
"vmls.f32 %f0, %e2, d6" "\n" // a1*b2-a2*b1(-9), a0*b1-a1*b0(-9)
"vext.8 %e0, %f0, %e0, #4" "\n" // a2*b0-a0*b2(18), a1*b2-a2*b1(-9)
: "+w"(result) // %0
: "w"(lhs), // %1
"w"(rhs) // %2
: "d6", "d7"
);
return result;
}
#define nforeach(i, count) \
for (int i = 0, __count = static_cast<int>(count); i < __count; ++i)
#define dump_f128(qf) do { \
float *fp = reinterpret_cast<float *>(&qf); \
puts(#qf ":"); \
nforeach(i, 4) { \
printf("[%d]%f\n", i, fp[i]); \
} \
} while (0)
int main() {
float fa[] = {1., 2., 3., 4.};
float fb[] = {8., 7., 6., 5.};
float32x4_t qa, qb, qres;
qa = vld1q_f32(const_cast<const float *>(&fa[0]));
qb = vld1q_f32(const_cast<const float *>(&fb[0]));
qres = cross_test(qa, qb);
dump_f128(qa);
puts("---");
dump_f128(qb);
puts("---");
// -9, 18, -9, -9
dump_f128(qres);
return 0;
}