ARM neon 优化 - 摆脱多余的负载
ARM neon optimization - getting rid of superfluous loads
我正在尝试使用 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 指令,但是,输入参数(应该在 x1 中)似乎在每次 fmla 调用后都会重新加载到 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
这也可以规避吗?
编译器是 gcc-linaro-6.3.1-2017。05-x86_64_aarch64-linux-gnu 带有 O2 选项。
此致
编辑:
删除 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 的编译器并不是最好的。
可能有处理此问题的编译器选项,甚至编译器开箱即用地进行所需的优化,但最好的选择是手动进行:
- 声明局部向量(不带 &)
- 将指针中的值加载到相应的向量中(最好是整个矩阵加上向量)
- 用向量做数学运算
- 将向量存储到指针
上述过程也适用于非霓虹灯计算。编译器几乎总是会因(自动)内存操作的最轻微提示而严重瘫痪。
记住,局部变量是你最好的朋友。并且总是手动记忆 load/store。
编译器: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
,因此它会再次从内存中加载它。
因为Cpointer aliasing rules.
您可以向编译器保证 output
指向的内存永远不会通过任何其他指针(或引用,在本例中)使用 restrict
关键字访问:
void transform (
glm::mat4 const & matrix,
glm::vec4 const & input,
glm::vec4 & __restrict output)
然后多余的负载就消失了。这是 compiler output (godbolt)(尝试删除 __restrict
)。
我正在尝试使用 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 指令,但是,输入参数(应该在 x1 中)似乎在每次 fmla 调用后都会重新加载到 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
这也可以规避吗?
编译器是 gcc-linaro-6.3.1-2017。05-x86_64_aarch64-linux-gnu 带有 O2 选项。
此致
编辑: 删除 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 的编译器并不是最好的。
可能有处理此问题的编译器选项,甚至编译器开箱即用地进行所需的优化,但最好的选择是手动进行:
- 声明局部向量(不带 &)
- 将指针中的值加载到相应的向量中(最好是整个矩阵加上向量)
- 用向量做数学运算
- 将向量存储到指针
上述过程也适用于非霓虹灯计算。编译器几乎总是会因(自动)内存操作的最轻微提示而严重瘫痪。
记住,局部变量是你最好的朋友。并且总是手动记忆 load/store。
编译器: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
,因此它会再次从内存中加载它。
因为Cpointer aliasing rules.
您可以向编译器保证 output
指向的内存永远不会通过任何其他指针(或引用,在本例中)使用 restrict
关键字访问:
void transform (
glm::mat4 const & matrix,
glm::vec4 const & input,
glm::vec4 & __restrict output)
然后多余的负载就消失了。这是 compiler output (godbolt)(尝试删除 __restrict
)。