CPU SIMD简介

简介

之前的两篇文章,分别介绍了CPU和CPU Cache两个话题,性能是永恒的核心。我们也谈到了优化CPU性能面临的三堵墙:

  • The power wall 目前,运算速度提升30%,则需要两倍的电压和发热,并且这种设计思路无法满足移动设备,也不可能长久
  • The memory wall 内存和CPU在性能上的差距拉大。
  • The IPL wall 目前多数应用并没有很好的并行化设计(指令级别)

针对第一面墙,《The Free Lunch Is Over》文章很好的告诉我们,这种性能优化已经达到上限。针对第二面墙,CPU Cache是解决两者性能差距的缓冲区。今天,我们来看看第三面墙的解决方案。

现代CPU日益依赖并行技术来达到高性能。在多核,超线程以及支持多任务操作系统的指令等硬件技术的发展下,多线程似乎成了优化性能的一颗银弹。然而,很少有人了解CPU指令级别上的并行技术:在一个Cycle内CPU应用一组向量操作,同时对4或8个输入数据执行相同指令,产生对应4或8个结果,这称为SIMD (Single Instruction, Multiple Data)。

最早在超级计算机上应用SIMD技术,比如CDC Start-100。1996年,Intel针对X86指令集,推出了MMX扩展,这是第一次在商用硬件上支持SIMD技术,1999年,Intel在P3中推出了SSE(Streaming SIMD Extensions),基于128位寄存器,针对4个float的向量数据,提供了70个汇编指令。AVX(Advanced Vector Extensions) 是Intel的SSE延伸架构,总之,就是让CPU越来越像GPU。

实践

下面,我们来看一下,在C++中如何使用SSE。

union { __m128 result_sum4; float result_sum[4]; }; result_sum[0] = result_sum[1] = result_sum[2] = result_sum[3] = 0.0f; result_sum4 = _mm_set_ps1(0.0f);

如上一个联合体(union),__m128是一个vector,里面对应4个float值,同理,还有两个变量类型__m128i和__m128d,分别对应int和double。如上对变量result_sum赋值后,result_sum4和result_sum共享同一块内存。当然,你也可以用SSE指令_mm_set_ps1对result_sum4初始化。

数据并行

for (int i = 0; i < 1000; i++) { pResult[i] = pL[i] + pR[i]; } // SSE2.0 for (int i = 0; i < 1000; i += 4) { result_sum4 = _mm_add_ps( _mm_setr_ps(pL[i], pL[i + 1], pL[i + 2], pL[i + 3]), _mm_setr_ps(pR[i], pR[i + 1], pR[i + 2], pR[i + 3])); pResult[i] = result_sum[0]; pResult[i + 1] = result_sum[1]; pResult[i + 2] = result_sum[2]; pResult[i + 3] = result_sum[3]; }

如上,一个for循环,左右两个数组求和。在SSE中,我们通过_mm_add_ps指令,实现四个元素的同步操作。同样,SSE中也提供了_mm_sub_ps ,_mm_mul_ps,_mm_div_ps分别对应减法,乘法和除法。

条件判断

下面,我们来看一个条件语句的代码:

int fun(floatx) { if (x < 0.1f) { return 0; } elseif (x > 0.9f) { return 1; } else { return -1; } } __m128i fun4(__m128 x4) { __m128 mask = _mm_cmpge_ps(x4, _mm_set_ps1(0.1f)); __m128 fV1 = _mm_blendv_ps(_mm_set_ps1(0.0f), _mm_set_ps1(-1.0f), mask); mask=_mm_cmpge_ps(_mm_set_ps1(0.9f), x4); __m128 fV2 = _mm_blendv_ps(_mm_set_ps1(2.0f), _mm_set_ps1(0.0f), mask); __m128i result4 = _mm_cvtps_epi32(_mm_add_ps(fV1, fV2)); return result4; }

这里,_mm_cmpge_ps相当于if判断,_mm_blendv_ps对应?:语句,_mm_cvtps_epi32负责将float转为int。这样,通过SSE对应的实现4个一组的逻辑判断。

从学习的角度,SSE指令并不复杂,它提供了一组指令集,实现我们常见的数学运算和逻辑判断,初次使用可能会略有不适,但学习成本还是很低的。如果感兴趣,不妨了解一下max,min,sinpower等方法对应的SSE指令,你也可以访问如下网站,获取对应的指令说明。https://software.intel.com/sites/landingpage/IntrinsicsGuide/#techs=MMX,SSE,SSE2,SSE3,SSSE3,SSE4_1,SSE4_2,AVX,AVX2,FMA,AVX_512

Tips

看上去SSE的使用并不复杂,无非就是把C++中惯用的+ - * /,以数据并行的思路进行改造,分别用对应的SSE指令替换一下就可以了。逻辑判断上略显复杂,但也都是小技巧而已。这是我最先编写SSE代码时的想法,应该和大家会有共鸣。然而,当我将一个反向传播神经网络的算法,以SSE2.0指令改造后,发现时间只减少到70%左右,说好的四分之一呢?我感觉自己被深深的欺骗了。

于是乎,我对这些基本操作进行了简单的测试,for循环100000000次,进行最简单的四则运算和指数,得出了如下的性能对比:

结论是,加减乘除大概相当,指数会有较大提高,我查看了Release下对应的汇编,发现编译器对这类简单的for循环,会自动编译为SSE指令,因此,对这类代码,我们并不需要改造,而且我们的改造不见得比编译器写的好,反而还可能会变慢。因为exp指数并没有对应的直接的SSE指令,是我找的一个第三方的库函数,因此才会有性能的较大提升。

要点1:20%的代码会消耗掉80%的时间,找到其中计算量最大,复杂度较高的部分,这是我们改造代码的重点。如果我们不确定优化是否有效,那就只能实践出真知。

其次,采用SSE指令集,需要将4个float合并为一个__m128,这个操作太频繁了,需要调用_mm_setr_ps_mm256_loadu_pd这类的指令,在这个过程中,初始化这些变量也要浪费时间的,并且还要将结果在转回为4个float,这个成本是否可以避免。

这里,我们可以通过指针强转的方式省去这类初始化的时间消耗,代码如下:

__m128* ptrLeft = (__m128*)&pValueLeft[i];

但这里有一个要求,这里要求变量pValueLeft 16字节对齐,而一般float数组只会保证4字节对齐,因此,在声明变量的时候,我们需要显示指定16字节对齐,C++ 11中提供了alignas保证数组对齐,而指针类型的则需要通过_aligned_malloc方法确保字节对齐。

同时,因为SSE是对四个float数据的并行化处理思路,这里就有一个很讨厌的情况,如果循环长度无法被4整除,剩余的部分不做改造,还是填上空值,再做一次并行计算,都是很烦人的选择,起码代码看上去不优雅,“异常”判断太多。我的建议是,重构数据布局,强制让所有的数组都被4整除。

这是一个较大的改进,因为新增的这些“零头”可能会干扰计算过程,改变数组中元素的值。因此,最好是设计阶段就能做好这类的数据布局,重构的话,需要确保这些数组参与的计算,不会因为额外增加的元素而破坏运算的准确性。

要点2:优化数据布局,数组首地址16字节对齐,长度被四整除。AVX?

至此,SSE编码方面的要点就差不多了,但我们不一样,我们是学过Cache的程序员,上一章有一个for for循环的示例,告诉我们行优先的效率要远远高于列优先,因为前者在内存上是连续的。而SSE主要就是针对计算量较大的部分(图像,神经网络等)的数据并行,因此,我们在代码改造中,要对这类代码重点照顾。比如如下的代码:

for (inti = 0; i < NUMHIDDEN4; i++) { // get error gradient for every hidden node errorGradientsHidden[i] = GetHiddenErrorGradient(i); // for all nodes in input layer and biasneuron for (intj = 0; j < INPUTSIZE4; j++) { constintweightIdx = GetInputHiddenWeightIndex(j, i); // calculate change in weight deltaInputHidden[weightIdx] = errorGradientsHidden[i] * weightIdx; } }

这里,GetInputHiddenWeightIndex(j, i)是列优先,差评。从优化的角度,这里有很大的优化空间:

for (inti = 0; i < NUMHIDDEN4; i+= STEP_SSE) { // get error gradient for every hidden node errorGradientsHidden[i] = GetHiddenErrorGradient(i); } // for all nodes in input layer and bias neuron for (intj = 0; j < INPUTSIZE4; j++) { for (inti = 0; i < NUMHIDDEN4; i += STEP_SSE) { const int weightIdx = GetInputHiddenWeightIndex(j, i); deltaInputHidden[weightIdx] = errorGradientsHidden[i] * weightIdx; } }

这里对for循环拆分,将i和j对调,并不影响最终的计算结果,但确保访问内存是连续的,然后在对两个for分别进行数据并行的改造。

要点3:SSE优化时,时刻提醒自己,这段代码在执行中,是否内存连续,是否有改造空间。

最后,我要说的是,虽然学习SSE并不难,但在实践中还有很多综合应用,并且后续可能会有新增的指令集,不同CPU之间的兼容问题,所以,不建议自己写,而是用一些专业的第三方库。好处是不用自己来维护升级,同时在逻辑上减轻耦合度。我们的重点不是写一套自己的SSE/AVX库。

要点4:专业的人做专业的事。基于我们对SSE理解的深度,结合自身代码的需要,合理移植VCL(vector class library),更好的让他山之石可以攻玉。

总结

SIMD的介绍就到这里,理论上并不复杂,实践中却需要顾及方方面面的可能点。至此,我们讲了CPU,谈到了Cache在性能优化中的巨大价值,本章学习了SIMD技术对数据并行的改造。

简言之,CPU的核心,就是对数据计算的优化。下一章是CPU的最后一篇,DOD(Data Oriented Design),整体上理解一下现代CPU和面向对象编程之间的冲突,作为C++程序员,我们如何批判的理解面向对象的得与失,从一个更大的维度来编写我们手下的代码。

原文发布于微信公众号 - LET(LET0-0)

原文发表时间:2018-08-23

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏跟着阿笨一起玩NET

EF中Repository模式应用场景

   在DDD领域构架系统中,为了将领域模型从领域逻辑层中和数据映射层之间解耦出来,我们引用到了Repository模式,属于属于泛型编程中一个比较常用的模式,...

3513
来自专栏有趣的Python和你

Python数据分析之dataframe的groupbygroupby函数highcharts绘图

1403
来自专栏算法+

WebRTC 音频算法 附完整C代码

AEC是声学回声消除(Acoustic Echo Canceller for Mobile)

5715
来自专栏FreeBuf

Python工具分析风险数据

小安前言 随着网络安全信息数据大规模的增长,应用数据分析技术进行网络安全分析成为业界研究热点,小安在这次小讲堂中带大家用Python工具对风险数据作简单分析,主...

2759
来自专栏IT大咖说

关于 Unicode 每个程序员应该知道的 5 件事

摘要 Unicode是一个令人难以置信的有用标准,它能使全世界的计算机、智能手机和智能手表以同样的方式显示相同的信息。不幸的是,它的复杂性使它成为了欺诈分子和恶...

2897
来自专栏追不上乌龟的兔子

JupyterLab——更具生产力的Jupyter环境

Jupyter源于Ipython Notebook,是使用Python(也有R、Julia、Node等其他语言的内核)进行代码演示、数据分析、可视化、教学的很好...

12.9K4
来自专栏Java学习网

最佳编码实践:搞砸代码的10种方法

 这是一篇提供有效、实用编程方法的程序箴言,作者Susan Harkins是世界最大的技术期刊出版社的主编,具有多年的实践经验;在这篇文章里她重申“最佳编码实践...

2774
来自专栏WOLFRAM

Wolfram 语言10.2版本新函数:ISO日期

1663
来自专栏怀英的自我修炼

怀英漫谈3-百度Echarts中日期控件的使用总结

你好, 今天下午在用百度的Echarts做一个日历图的效果,其中跌跌碰碰遇到了几个问题,好在最终都解决了,今天想跟你聊聊这几个问题。 本篇偏编程,可以跳至最后看...

4039
来自专栏安恒网络空间安全讲武堂

Writeup丨国赛线上初赛解题第二波~

1554

扫码关注云+社区

领取腾讯云代金券