我正在尝试使用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)确实会产生霓虹灯指令,但是,似乎在每个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_aarch64-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调用中似乎仍然有很大的开销。
答案 0 :(得分:3)
您直接在指针上进行操作(按引用调用)。如果您对指针进行操作,则应意识到自己完全受编译器的摆布。而且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
超出了我。
答案 1 :(得分:0)
您的输出glm::vec4 & output
可能与相同类型的input
指向相同的内存。每当您写入输出时,编译器都会假定您可能正在更改input
,因此它将再次从内存中加载它。
这是因为C pointer aliasing rules。
您可以向编译器保证output
所指向的内存将永远不会通过使用restrict
关键字的任何其他指针(在这种情况下为引用)进行访问:
void transform (
glm::mat4 const & matrix,
glm::vec4 const & input,
glm::vec4 & __restrict output)
然后,多余的负载消失了。这是compiler output (godbolt)(尝试删除__restrict
)。