系列文章目录

  • 数字信号处理中的 SIMD
  • Neon intrinsics 简明教程
  • 用 NEON 实现高效的 FIR 滤波器

前言

关于 SIMD,或者说 NEON,我已经发布了几篇文章来介绍它了,如果你看过了这些内容,相信你对于 NEON 有了一定的了解。在此之前,我们更多停留在理论阶段:介绍了 NEON 的 API,举了几个简单的例子。

今天,我们将通过一些练习,这些任务在实际开发中你也可能会遇到,它们足够简单,作为 NEON 入门教学示例非常合适。我们将向你演示,如何使用 NEON 来优化现有代码,以及通过 Benchmark 来测试优化前与优化后的性能差异。

令人遗憾的是,本以为掌握的 SIMD 可以让你算法性能得到成倍的提升,但实际测试下来却发现编译器实在太聪明了,对于一些简单的任务,编译器优化后的代码比你手写的 SIMD 更快更好。说实话,这让我有些沮丧,让我学习 SIMD 的动力降低了不少,但又觉得庆幸,作为程序员可以比较放心的将一部分工作交给编译器了,无需再卷。

本来我预计的博客内容流程应该是这样的:

  1. 提出一个问题,用基础的实现给出 baseline
  2. 使用 NEON 进行优化
  3. 哇塞,优化后的性能得到了成倍的提升,SIMD 真牛逼!

但实际上优化后的性能基本是负优化(吐血~),而且不同编译器表现不同,同样一份代码在 A 平台下性能提升,但换到 B 平台下可能就是负优化了。这就导致「优化」这项工作甚至要与编译器版本、操作系统绑定,事情就变得越来越复杂了,不单单是代码层面上的事情了。当然,发生这一切的原因可能是因为我给的示例任务太过简单,简单到聪明的编译器一眼看透了。

无论如何,我还是决定将整个过程整理出来,给各位看官一个参考。以下示例运行将运行在笔者的 Mac M1 和 Android 荣耀 50 上,所有代码你可以在 neon_intrinsics_exercise - github 找到。

一、通用流程

当想要对某个算法做性能优化时,首先考虑有没有更优的实现方式,例如将冒泡排序改为快排,算法复杂度从 O(N2)O(N^2)O(N2) 降到了 O(Nlog⁡N)O(N\log N)O(NlogN)。

当算法实现已经固定,实现上没有更优的方式后,这时候就考虑使用 SIMD 技术进行性能优化了。假设现在有个一个算法 A 要进行优化,那么整体流程大致为:

  1. 对 A 算法进行 profiling,将结果作为优化的基线
  2. 对 A 算法进行 SIMD 优化,得到优化后的算法 A_SIMD
  3. 将 A_SIMD 与 A 算法输出进行对比,确保 A_SIMD 结果与优化前结果保持一致
  4. 对 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_chunk{0.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;
}

其中:

  1. vld1q_f32(array + i) 从内存中加载数据到向量 chunk
  2. vaddq_f32,进行向量加法
  3. 最后用一个 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;}
}

其中:

  1. vld1q_f32 从内存中导入左右声道数据
  2. vmulq_n_f32 即向量乘上一个常数
  3. 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 4for (auto i = 0u; i < input.outputLength; i += 4) {float32x4_t yChunk{0.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 memoryvst1q_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 练习题相关推荐

  1. 用 Neon Intrinsics 优化 C 代码

    以下内容翻译自:Optimizing C Code with Neon Intrinsics 概述 本指南向您展示如何在 C 或 C++ 代码中使用 Neon intrinsics 函数,以利用 Ar ...

  2. Arm NEON intrinsics指令格式

    NEON Intrinsics 是一种更简单的编写 NEON 代码的方法,NEON Intrinsics 类似于 C 函数调用,在编译时由编译器替换为相应的汇编指令,使用时需要包含头文件arm_neo ...

  3. NEON intrinsics 函数模式介绍

    原文:https://blog.csdn.net/sunty2016/article/details/79857825 本文打算介绍下ARM的SIMD指令在C语言下intrinsics函数的使用方法, ...

  4. ARM Neon Intrinsics 学习指北:从入门、进阶到学个通透

    本文同步发表于GiantPandaCV公众号,未经作者允许严禁转载 前言 Neon是ARM平台的向量化计算指令集,通过一条指令完成多个数据的运算达到加速的目的,常用于AI.多媒体等计算密集型任务. 本 ...

  5. 【neon加速拆分/合并交叉数据】使用neon intrinsics加速合并/拆分uv的内存分布(交叉存储/分别存储)

    说明 在YUV格式的图片中,uv数据可以"UVUVUVUV"形式交叉存储叫NV12,也可以以"UUUUUVVVVV"的格式分开存储,为了将二者转换,需要类似如下 ...

  6. 大前端CPU优化技术--NEON intrinsics进阶

    前言 今天我们继续介绍NEON intrinsics的指令知识,上篇大前端CPU优化技术--NEON intrinsics开篇中已经介绍了部分指令的作用.本篇文章除了介绍指令还会附上场景示例,方便大家 ...

  7. ARM Neon 编程笔记一(ARM NEON Intrinsics, SIMD运算, 优化心得)

    1. ARM Neon Intrinsics 编程 1.入门:基本能上手写Intrinsics 1.1 Neon介绍.简明案例与编程惯例 1.2 如何检索Intrinsics 1.3 优化效果案例 1 ...

  8. Neon intrinsics

    1.介绍 在上篇中,介绍了ARM的Neon,本篇主要介绍Neon intrinsics的函数用法,也就是assembly之前的用法.NEON指令是从Armv7架构开始引入的SIMD指令,其共有16个1 ...

  9. 【genius_platform软件平台开发】第八十二讲:ARM Neon指令集一(ARM NEON Intrinsics, SIMD运算, 优化心得)

    1. ARM Neon Intrinsics 编程 1.入门:基本能上手写Intrinsics 1.1 Neon介绍.简明案例与编程惯例 1.2 如何检索Intrinsics 1.3 优化效果案例 1 ...

最新文章

  1. layui分页limit不显示_小心避坑:MySQL分页时使用 limit+order by 会出现数据重复问题...
  2. python文字识别算法_Python图像处理之图片文字识别(OCR)
  3. C# 4.0 新特性之参数
  4. 使用Azure Application Insignhts监控ASP.NET Core应用程序
  5. codeforces 939C Convenient For Everybody 简直羞耻
  6. java 常见 错误_JAVA几个常见错误简析
  7. hsqldb和mysql_HSQLDB的研究与性能测试(与Mysql对比)
  8. 常见花材的固定的方法有哪些_波峰焊喷嘴的常见故障及处理方法有哪些
  9. SDOI 2016 游戏
  10. 关于VOID *在cl与gcc的不同(无意中发现)
  11. NYOJ 972 核桃的数量(蓝桥杯)
  12. nbu备份软件异机恢复需要注意问题
  13. 模仿360加速球制作一个动态ProgressBar
  14. 2018大学计算机选择题题库,2018年9月计算机二级选择题题库(31-40)
  15. U3D游戏包il2cpp逆向解包,apk加密资源解密
  16. centos7 安装btsync
  17. 函数对称性常见公式_函数的对称性
  18. 子div在父div垂直居中的最好方法
  19. 【weblogic】WTC配置(Weblogic Tuxedo Connector)
  20. cs,ds,ss,es?即cs,ds,ss,es的区别

热门文章

  1. 计算机网络 理论复习概括
  2. spring-cloud-kubernetes的服务发现和轮询实战(含熔断)
  3. 配色神器ColorCube
  4. 计算机开机后黑屏鼠标显示桌面图标,电脑开机后黑屏只有一个鼠标箭头怎么办 电脑黑屏的解决方法...
  5. 2019 数学建模国赛 C 题思路
  6. jekyll搭建博客
  7. 【FNN回归预测】基于matlab粒子群优化前馈神经网络婚姻和离婚数据回归预测【含Matlab源码 2069期】
  8. 软件工程师的本手、妙手和俗手
  9. turtle画了一个皮卡丘
  10. 蚂蚁链API参考接口