卷积可以通过im2col转化为矩阵乘。
而矩阵乘法,结果矩阵中的每个元素是等长度的一行与一列向量乘再累加求结果。
因此核心的矩阵算法如下:
float test(float* a, float* b, int n)
{
float c = 0;
for(int i=0; i<n; i++)
{
c += (*a++) * (*b++);
}
return c;
}
这段代码,如果手工优化,无非是循环展开等等...不过现代编译器的优化了得,在使用-ffast-math和-ftree-vectorize优化选项后,编译器也能做到循环展开和向量化(使用SIMD)指令,如下是上述代码用g++ -S 生成的汇编代码
_Z4testPfS_i:
subs w8, w2, #1 // w8 = n-1, if w8 < 0, return 0
b.lt .LBB0_3
add x8, x8, #1 // x8+1
cmp x8, #7
fmov s0, wzr
b.hi .LBB0_4 // 如果数量超过8个,走循环
mov w8, wzr
b .LBB0_8 // 跳转到尾巴处, 处理尾巴
.LBB0_3:
mov w0, wzr
ret
.LBB0_4:
and w9, w2, #0x7 // w9 = w2 & 0x7, 循环展开
sub x8, x8, x9 // 计算i的下标
cbz x8, .LBB0_8
lsl x13, x8, #2 // 根据i的下标计算偏移量,x13存放的是float的地址偏移量 左移2位,因为float是4B
add x10, x1, #16 // x1是b的地址, 不明白+16是何用意,x10是本次首地址
add x11, x0, #16 // x0是a的地址, 不明白+16是何用意,x11是本次首地址
movi v0.2d, #0000000000000000 // 2个long等于4个float,赋值为0
mov x12, x8
add x0, x0, x13 // a的下一个地址
add x1, x1, x13 // b的下一个地址
movi v1.2d, #0000000000000000 // 2个long等于4个float,赋值为0
.LBB0_6:
ldp q2, q3, [x11, #-16] // 从x11-16的地址取8个float,放在q2和q3 中
ldp q4, q5, [x10, #-16] // 从x11-16的地址取8个float,放在q2和q3 中
add x10, x10, #32 // =32 // x10向上走8个float
sub x12, x12, #8 // =8 // 下标i减8
add x11, x11, #32 // =32 // x11向上走8个float
fmla v0.4s, v2.4s, v4.4s // 0 = 2 + 4 + 0,向量累加
fmla v1.4s, v3.4s, v5.4s // 1 = 3 + 5 + 1,向量累加
cbnz x12, .LBB0_6 // 判断循环是否到0
fadd v0.4s, v1.4s, v0.4s
ext v1.16b, v0.16b, v0.16b, #8
fadd v0.4s, v0.4s, v1.4s
dup v1.4s, v0.s[1]
fadd v0.4s, v0.4s, v1.4s
cbz w9, .LBB0_10
.LBB0_8:
sub w8, w2, w8
.LBB0_9:
ldr s1, [x0], #4 // 不足8个的,还剩一点尾巴,1个1个的取
ldr s2, [x1], #4
sub w8, w8, #1 // i下标1个1个处理
fmadd s0, s2, s1, s0 // 0 = 2 + 1 + 0, 单个float相加
cbnz w8, .LBB0_9
.LBB0_10:
fcvtzs w0, s0
ret