ARM霓虹灯优化-消除多余的负载

ARM neon optimization - getting rid of superfluous loads

本文关键字:多余 负载 霓虹灯 优化 ARM      更新时间:2023-10-16

我正在尝试使用arm neon构建一个优化的右手矩阵乘法。这个

void transform ( glm::mat4 const & matrix, glm::vec4 const & input, glm::vec4 & output )
{
float32x4_t &       result_local = reinterpret_cast < float32x4_t & > (*(&output[0]));
float32x4_t const & input_local  = reinterpret_cast < float32x4_t const & > (*(&input[0] ));
result_local = vmulq_f32 (               reinterpret_cast < float32x4_t const & > ( matrix[ 0 ] ), input_local );
result_local = vmlaq_f32 ( result_local, reinterpret_cast < float32x4_t const & > ( matrix[ 1 ] ), input_local );
result_local = vmlaq_f32 ( result_local, reinterpret_cast < float32x4_t const & > ( matrix[ 2 ] ), input_local );
result_local = vmlaq_f32 ( result_local, reinterpret_cast < float32x4_t const & > ( matrix[ 3 ] ), input_local );
}

编译器(gcc)确实产生了neon指令,然而,似乎在每次fmla调用后,输入参数(假定在x1中)都会重新加载到q1:

0x0000000000400a78 <+0>:    ldr q1, [x1]
0x0000000000400a7c <+4>:    ldr q0, [x0]
0x0000000000400a80 <+8>:    fmul    v0.4s, v0.4s, v1.4s
0x0000000000400a84 <+12>:   str q0, [x2]
0x0000000000400a88 <+16>:   ldr q2, [x0,#16]
0x0000000000400a8c <+20>:   ldr q1, [x1]
0x0000000000400a90 <+24>:   fmla    v0.4s, v2.4s, v1.4s
0x0000000000400a94 <+28>:   str q0, [x2]
0x0000000000400a98 <+32>:   ldr q2, [x0,#32]
0x0000000000400a9c <+36>:   ldr q1, [x1]
0x0000000000400aa0 <+40>:   fmla    v0.4s, v2.4s, v1.4s
0x0000000000400aa4 <+44>:   str q0, [x2]
0x0000000000400aa8 <+48>:   ldr q2, [x0,#48]
0x0000000000400aac <+52>:   ldr q1, [x1]
0x0000000000400ab0 <+56>:   fmla    v0.4s, v2.4s, v1.4s
0x0000000000400ab4 <+60>:   str q0, [x2]
0x0000000000400ab8 <+64>:   ret

有可能也回避吗?

编译器是带有O2选项的gcc-linaro-6.3.1-2017.05-x86_64_arch64-linux-gnu。

问候

编辑:删除input_local上的引用成功了:

0x0000000000400af0 <+0>:    ldr q1, [x1]
0x0000000000400af4 <+4>:    ldr q0, [x0]
0x0000000000400af8 <+8>:    fmul    v0.4s, v1.4s, v0.4s
0x0000000000400afc <+12>:   str q0, [x2]
0x0000000000400b00 <+16>:   ldr q2, [x0,#16]
0x0000000000400b04 <+20>:   fmla    v0.4s, v1.4s, v2.4s
0x0000000000400b08 <+24>:   str q0, [x2]
0x0000000000400b0c <+28>:   ldr q2, [x0,#32]
0x0000000000400b10 <+32>:   fmla    v0.4s, v1.4s, v2.4s
0x0000000000400b14 <+36>:   str q0, [x2]
0x0000000000400b18 <+40>:   ldr q2, [x0,#48]
0x0000000000400b1c <+44>:   fmla    v0.4s, v1.4s, v2.4s
0x0000000000400b20 <+48>:   str q0, [x2]
0x0000000000400b24 <+52>:   ret

编辑2:这是我目前获得的最多的一次。

0x0000000000400ea0 <+0>:    ldr q1, [x1]
0x0000000000400ea4 <+4>:    ldr q0, [x0,#16]
0x0000000000400ea8 <+8>:    ldr q4, [x0]
0x0000000000400eac <+12>:   ldr q3, [x0,#32]
0x0000000000400eb0 <+16>:   fmul    v0.4s, v0.4s, v1.4s
0x0000000000400eb4 <+20>:   ldr q2, [x0,#48] 
0x0000000000400eb8 <+24>:   fmla    v0.4s, v4.4s, v1.4s
0x0000000000400ebc <+28>:   fmla    v0.4s, v3.4s, v1.4s
0x0000000000400ec0 <+32>:   fmla    v0.4s, v2.4s, v1.4s
0x0000000000400ec4 <+36>:   str q0, [x2]
0x0000000000400ec8 <+40>:   ret

根据性能,在ldr调用中似乎仍然有很大的开销

您直接在指针上操作(根据引用调用)。若您对指针进行操作,您应该意识到您完全受编译器的支配。ARM的编译器并不是最好的。

可能有编译器选项可以处理此问题,甚至编译器可以开箱即用地进行所需的优化,但最好手动进行:

  • 声明局部向量(不带&)
  • 将指针中的值加载到相应的向量中(最好是整个矩阵加上向量)
  • 用矢量计算
  • 将矢量存储到指针

上述过程也适用于非氖计算。编译器几乎总是被(自动)内存操作的最轻微提示严重削弱。

记住,局部变量是你最好的朋友。并且始终手动进行内存加载/存储。


编译器:Android clang 8.0.2-o2

void transform(const float *matrix, const float *input, float *output)
{
const float32x4_t input_local = vld1q_f32(input);
const float32x4_t row0 = vld1q_f32(&matrix[0*4]);
const float32x4_t row1 = vld1q_f32(&matrix[1*4]);
const float32x4_t row2 = vld1q_f32(&matrix[2*4]);
const float32x4_t row3 = vld1q_f32(&matrix[3*4]);
float32x4_t rslt;
rslt = vmulq_f32(row0, input_local);
rslt = vmlaq_f32(rslt, row1, input_local);
rslt = vmlaq_f32(rslt, row2, input_local);
rslt = vmlaq_f32(rslt, row3, input_local);
vst1q_f32(output, rslt);
}
; void __fastcall transform(const float *matrix, const float *input, float *output)
EXPORT _Z9transformPKfS0_Pf
_Z9transformPKfS0_Pf
matrix = X0             ; const float *
input = X1              ; const float *
output = X2             ; float *
; __unwind {
LDR             Q0, [input]
LDP             Q1, Q2, [matrix]
LDP             Q3, Q4, [matrix,#0x20]
FMUL            V1.4S, V0.4S, V1.4S
FMUL            V2.4S, V0.4S, V2.4S
FMUL            V3.4S, V0.4S, V3.4S
FADD            V1.4S, V1.4S, V2.4S
FADD            V1.4S, V3.4S, V1.4S
FMUL            V0.4S, V0.4S, V4.4S
FADD            V0.4S, V0.4S, V1.4S
STR             Q0, [output]
RET
; } // starts at 4

正如你所看到的,Android clang 8.0.2在霓虹灯代码方面比以前的版本有了很大的改进。最后,编译器生成加载多个寄存器的代码。为什么它不喜欢FMLA,我无法理解。

您的输出glm::vec4 & output可能是对与相同类型的input相同内存的引用。每当您对输出进行写入时,编译器都会假设您可能正在更改input,因此它会从内存中再次加载它。

这是因为C指针的混叠规则。

您可以向编译器承诺,output指向的内存永远不会通过任何其他带有restrict关键字的指针(在这种情况下是引用)访问:

void transform (
glm::mat4 const & matrix,
glm::vec4 const & input,
glm::vec4 & __restrict output)

然后额外的负载就消失了。这是编译器的输出(godbolt)(尝试删除__restrict)。