NEON Intrinsics 练习题
Posted 芥末的无奈
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NEON Intrinsics 练习题相关的知识,希望对你有一定的参考价值。
系列文章目录
前言
关于 SIMD,或者说 NEON,我已经发布了几篇文章来介绍它了,如果你看过了这些内容,相信你对于 NEON 有了一定的了解。在此之前,我们更多停留在理论阶段:介绍了 NEON 的 API,举了几个简单的例子。
今天,我们将通过一些练习,这些任务在实际开发中你也可能会遇到,它们足够简单,作为 NEON 入门教学示例非常合适。我们将向你演示,如何使用 NEON 来优化现有代码,以及通过 Benchmark 来测试优化前与优化后的性能差异。
令人遗憾的是,本以为掌握的 SIMD 可以让你算法性能得到成倍的提升,但实际测试下来却发现编译器实在太聪明了,对于一些简单的任务,编译器优化后的代码比你手写的 SIMD 更快更好。说实话,这让我有些沮丧,让我学习 SIMD 的动力降低了不少,但又觉得庆幸,作为程序员可以比较放心的将一部分工作交给编译器了,无需再卷。
本来我预计的博客内容流程应该是这样的:
- 提出一个问题,用基础的实现给出 baseline
- 使用 NEON 进行优化
- 哇塞,优化后的性能得到了成倍的提升,SIMD 真牛逼!
但实际上优化后的性能基本是负优化(吐血~),而且不同编译器表现不同,同样一份代码在 A 平台下性能提升,但换到 B 平台下可能就是负优化了。这就导致「优化」这项工作甚至要与编译器版本、操作系统绑定,事情就变得越来越复杂了,不单单是代码层面上的事情了。当然,发生这一切的原因可能是因为我给的示例任务太过简单,简单到聪明的编译器一眼看透了。
无论如何,我还是决定将整个过程整理出来,给各位看官一个参考。以下示例运行将运行在笔者的 Mac M1 和 android 荣耀 50 上,所有代码你可以在 neon_intrinsics_exercise - github 找到。
一、通用流程
当想要对某个算法做性能优化时,首先考虑有没有更优的实现方式,例如将冒泡排序改为快排,算法复杂度从 O ( N 2 ) O(N^2) O(N2) 降到了 O ( N log N ) O(N\\log N) O(NlogN)。
当算法实现已经固定,实现上没有更优的方式后,这时候就考虑使用 SIMD 技术进行性能优化了。假设现在有个一个算法 A 要进行优化,那么整体流程大致为:
- 对 A 算法进行 profiling,将结果作为优化的基线
- 对 A 算法进行 SIMD 优化,得到优化后的算法 A_SIMD
- 将 A_SIMD 与 A 算法输出进行对比,确保 A_SIMD 结果与优化前结果保持一致
- 对 A_SIMD 进行 profiling,与基线做对比,确保做了正向的优化
总而言之,我们既要保证优化后的结果是正确的,又要保证性能的的确确得到了提升。
二、一些示例
1. 向量累加和
任务描述:实现一个函数,使用 NEON 指令集,对一个数组中的所有数字求和,并返回结果。
1.1 baseline
这个任务非常简单,聪明的你可能脑海中已经有了 NEON 的实现思路,但请停一停。饭一口一口的吃,我们先从最简单的开始,使用 C/C++ 实现一个最简单的实现,与 NEON 无关。代码如下:
float sum(float* array, size_t size)
float s = 0.0f;
for(int i = 0; i < size; ++i)
s += array[i];
return s;
1.2 NEON 实现
先对 baseline 代码做循环展开:
float sum_expand(float* array, size_t size)
float s = 0.0f;
int i = 0;
for(; i < size; i += 4)
s += array[i];
s += array[i + 1];
s += array[i + 2];
s += array[i + 3];
for(; i < size; ++i)
s += array[i];
return s;
其中循环展开部分,可以使用 SIMD 向量操作来完成:
float sum_neon(float* array, size_t size)
int i = 0;
float32x4_t out_chunk0.0f,0.0f,0.0f,0.0f;
for(; i < size; i+=4)
float32x4_t chunk = vld1q_f32(array + i);
out_chunk = vaddq_f32(out_chunk, chunk);
float x = out_chunk[0] + out_chunk[1] + out_chunk[2] + out_chunk[3];
for(;i < size; ++i)
x += array[i];
return x;
其中:
vld1q_f32(array + i)
从内存中加载数据到向量chunk
中vaddq_f32
,进行向量加法- 最后用一个
for
循环对剩下的数据进行累加
1.3 性能对比
Mac M1 | Android 荣耀 50 | |
---|---|---|
baseline | 16 ns | 3167 us |
neon | 16 ns | 2445 us |
在 Android 性能优化了 23% 左右;在 Mac M1 下没有性能上的提升。
2. 左右声道混音
任务描述:给你左右声道的数据和两个声道的音量,分别是两个 float
的数组和两个 float
值,将左右声道进行 mix,输出 mix 后的数据
2.1 baseline
void mix(float *left, float left_volume,
float *right, float right_volume,
float *output, size_t size)
for (int i = 0; i < size; ++i)
output[i] = left[i] * left_volume + right[i] * right_volume;
2.2 NEON 实现
同样的,先做循环展开
void mix_expand(float *left, float left_volume,
float *right, float right_volume,
float *output, size_t size)
int i = 0;
for (; i < size; i += 4)
output[i] = left[i] * left_volume + right[i] * right_volume;
output[i + 1] = left[i + 1] * left_volume + right[i + 1] * right_volume;
output[i + 2] = left[i + 2] * left_volume + right[i + 2] * right_volume;
output[i + 3] = left[i + 3] * left_volume + right[i + 3] * right_volume;
for (; i < size; ++i)
output[i] = left[i] * left_volume + right[i] * right_volume;
根据循环展开,大致可以知道有三个向量,分别是左声道数据、右声道数据、以及输出数据:
void mix_neon(float *left, float left_volume,
float *right, float right_volume,
float *output, size_t size)
int i = 0;
for (; i < size; i += 4)
float32x4_t left_chunk = vld1q_f32(left + i);
float32x4_t right_chunk = vld1q_f32(right + i);
left_chunk = vmulq_n_f32(left_chunk, left_volume);
right_chunk = vmulq_n_f32(right_chunk, right_volume);
float32x4_t output_chunk = vaddq_f32(left_chunk, right_chunk);
vst1q_f32(output + i, output_chunk);
for (; i < size; ++i)
output[i] = left[i] * left_volume + right[i] * right_volume;
其中:
vld1q_f32
从内存中导入左右声道数据vmulq_n_f32
即向量乘上一个常数vaddq_f32
使用向量加法将左右声道数据相加
2.3 性能对比
Mac M1 | Android 荣耀 50 | |
---|---|---|
baseline | 136 us | 3329 us |
neon | 227 us | 5401 us |
在这个 case 下,M1 和 Android 下都是负优化
3. FIR 滤波器
关于 FIR 滤波器和 SIMD 实现请参考 用 NEON 实现高效的 FIR 滤波器,细节不再赘述。
3.1 baseline
float* applyFirFilterSingle(FilterInput& input)
const auto* x = input.x;
const auto* c = input.c;
auto* y = input.y;
for (auto i = 0u; i < input.outputLength; ++i)
y[i] = 0.f;
for (auto j = 0u; j < input.filterLength; ++j)
y[i] += x[i + j] * c[j];
return y;
3.1 VIL
float* applyFirFilterInnerLoopVectorizationARM(FilterInput& input)
const auto* x = input.x;
const auto* c = input.c;
auto* y = input.y;
for (auto i = 0u; i < input.outputLength; ++i)
y[i] = 0.f;
float32x4_t outChunk = vdupq_n_f32(0.0f);
for (auto j = 0u; j < input.filterLength; j += 4)
float32x4_t xChunk = vld1q_f32(x + i + j);
float32x4_t cChunk = vld1q_f32(c + j);
float32x4_t temp = vmulq_f32(xChunk, cChunk);
outChunk = vaddq_f32(outChunk, temp);
y[i] = vaddvq_f32(outChunk);
return y;
3.2 VOL
float* applyFirFilterOuterLoopVectorizationARM(FilterInput& input)
const auto* x = input.x;
const auto* c = input.c;
auto* y = input.y;
// Note the increment by 4
for (auto i = 0u; i < input.outputLength; i += 4)
float32x4_t yChunk0.0f, 0.0f, 0.0f, 0.0f;
for (auto j = 0u; j < input.filterLength; ++j)
float32x4_t xChunk = vld1q_f32(x + i + j);
float32x4_t temp = vmulq_n_f32(xChunk, c[j]);
yChunk = vaddq_f32(yChunk, temp);
// store to memory
vst1q_f32(y + i, yChunk);
return y;
3.3 VOIL
float* applyFirFilterOuterInnerLoopVectorizationARM(FilterInput& input)
const auto* x = input.x;
const auto* c = input.c;
auto* y = input.y;
const int K = 4;
std::array<float32x4_t, K> outChunk;
for (auto i = 0u; i < input.outputLength; i += K)
for(auto k = 0; k < K; ++k)
outChunk[k] = vdupq_n_f32(0.0f);
for (auto j = 0u; j < input.filterLength; j += 4)
float32x4_t cChunk = vld1q_f32(c + j);
for(auto k = 0; k < K; ++k)
float32x4_t xChunk = vld1q_f32(x + i + j +k);
float32x4_t temp = vmulq_f32(cChunk, xChunk);
outChunk[k] = vaddq_f32(temp, outChunk[k]);
for(auto k = 0; k < K; ++k)
y[i + k] = vaddvq_f32(outChunk[k]);
return input.y;
3.4 性能对比
Mac M1 | Android 荣耀 50 | |
---|---|---|
baseline | 10420 us | 51119 us |
VIL | 2297 us | 55947 us |
VOL | 2524 us | 54134 us |
VOIL | 689 us | 69341 us |
在 Mac M1 下 SIMD 取得了不错的优化,但在 Android 下却都是负优化。
总结
由于现代编译器过于牛逼,一些不复杂的任务编译器已经能够自动识别并进行向量化,导致 SIMD 优化技巧需要斟酌使用,我们在做优化前要确定好基线,优化后要确保算法输出与原来一致,且与基线性能做对比,确保做了正向的优化。本来我还从 webrtc 中找了几个 NEON 实现的算法,但测试下来仍然是负优化,就不放上来了。
以上是关于NEON Intrinsics 练习题的主要内容,如果未能解决你的问题,请参考以下文章