AVX2 优化 C++ 混响算法

这里 是一份 C++ 混响算法的实现,修正掉索引下溢 BUG 后大概是这样:

1
2
3
4
5
6
7
8
9
size_t samples = file.file_size / 4;
size_t ir_samples = sizeof(ir) / 4;
for (size_t i = 0; i < samples; ++i)
{
for (size_t j = 0; j < ir_samples && i >= j; ++j)
{
file.out_file_data[i] += file.in_file_data[i - j] * ir[j];
}
}

在我的 Intel i7-7700 4 核机器上,混响一段 60s、单声道、float 采样的 pcm 音频需要 12452 ms 的时间。

内层循环是一个一维卷积,累加当前采样点之前所有采样点的混响(乘法)效果,展开后寻找规律:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
out[0] = in[0] * ir[0]

out[1] = in[1] * ir[0]
+ in[0] * ir[1]

out[2] = in[2] * ir[0]
+ in[1] * ir[1]
+ in[0] * ir[2]

......

out[7] = in[7] * ir[0]
+ in[6] * ir[1]
+ in[5] * ir[2]
+ in[4] * ir[3]
+ in[3] * ir[4]
+ in[2] * ir[5]
+ in[1] * ir[6]
+ in[0] * ir[7]

......

out[n] = in[n] * ir[0]
+ in[n-1] * ir[1]
+ ...
+ in[0] * ir[n]

AVX2 一次处理 8 个 float 数据,所以前 7 个采样的混响需要用 0 补齐,7 之后的采样就可以自后往前每次处理 8 个采样的混响和累加了;j 的步进也应该从 1 变成 8。

优化后,代码变成这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
size_t samples = file.file_size / 4;
size_t ir_samples = sizeof(ir) / 4;
for (size_t i = 0; i < samples; ++i)
{
__m256 out = { 0 };
for (size_t j = 0; j < ir_samples && i >= j; j += 8)
{
__m256 mm256_ir = _mm256_load_ps(ir + j);
__m256 in;
switch (i - j)
{
// _mm256_set_ps 以逆序加载数据,即第一个参数对应 out.m256_f32[7],最后一个参数对应 out.m256_f32[0]
case 0:
in = _mm256_set_ps(0, 0, 0, 0, 0, 0, 0, file.in_file_data[0]);
break;
case 1:
in = _mm256_set_ps(0, 0, 0, 0, 0, 0, file.in_file_data[0], file.in_file_data[1]);
break;
case 2:
in = _mm256_set_ps(0, 0, 0, 0, 0, file.in_file_data[0], file.in_file_data[1], file.in_file_data[2]);
break;
case 3:
in = _mm256_set_ps(0, 0, 0, 0, file.in_file_data[0], file.in_file_data[1], file.in_file_data[2], file.in_file_data[3]);
break;
case 4:
in = _mm256_set_ps(0, 0, 0, file.in_file_data[0], file.in_file_data[1], file.in_file_data[2], file.in_file_data[3], file.in_file_data[4]);
break;
case 5:
in = _mm256_set_ps(0, 0, file.in_file_data[0], file.in_file_data[1], file.in_file_data[2], file.in_file_data[3], file.in_file_data[4], file.in_file_data[5]);
break;
case 6:
in = _mm256_set_ps(0, file.in_file_data[0], file.in_file_data[1], file.in_file_data[2], file.in_file_data[3], file.in_file_data[4], file.in_file_data[5], file.in_file_data[6]);
break;
default:
in = _mm256_set_ps(file.in_file_data[i - j - 7], file.in_file_data[i - j - 6], file.in_file_data[i - j - 5], file.in_file_data[i - j - 4], file.in_file_data[i - j - 3], file.in_file_data[i - j - 2], file.in_file_data[i - j - 1], file.in_file_data[i - j]);
break;
}
out = _mm256_fmadd_ps(in, mm256_ir, out);
}
file.out_file_data[i] = out.m256_f32[0] + out.m256_f32[1] + out.m256_f32[2] + out.m256_f32[3] + out.m256_f32[4] + out.m256_f32[5] + out.m256_f32[6] + out.m256_f32[7];
}

耗时优化到了 1285 ms,性能提升了约 9.7 倍,超过了 AVX2 8 倍的理论时间上限,也可见编译器对 switch 分支优化的厉害程度。

完整测试项目在这里

评论