当前位置:首页 > > 充电吧
[导读]此文主要内容来自这篇文章,本文翻译只求能理解,不求逐句翻译。正文:我们将在本文中介绍如何在C++/C中使用SSE指令。我的目的不是用SSE写尽可能快的程序,而是试图讲明白它的使用方法。什么是SSE? 

此文主要内容来自这篇文章,本文翻译只求能理解,不求逐句翻译。

正文:

我们将在本文中介绍如何在C++/C中使用SSE指令。我的目的不是用SSE写尽可能快的程序,而是试图讲明白它的使用方法。


什么是SSE?

 SSE的全称是 Sreaming SIMD Extensions, 它是一组CPU指令,用于像信号处理、科学计算或者3D图形计算一样的应用。


SIMD 也是几个单词的首写字母组成的: Single Instruction, Multiple Data。 一个指令发出后,同一时刻被放到不同的数据上执行,

这个指令就是SIMD指令。


SSE在1999年首次出现在Pentium 3上。在过去的那段时光里,一些更加精致的功能被加入了这套指令集,

8个128-bit的寄存器被加入了CPU :xmm0到xmm7.

 


最初的时候,这些寄存器智能用来做单精度浮点数计算(float),

自从SSE2开始,这些寄存器可以被用来计算任何基本数据类型的数据了。


给定一个标准的32位机器,我们可以并行的存储和计算了:

-- 2 double

-- 2 long

-- 4 float 

-- 4 int

-- 8 short

-- 16 char

注意:整数类型可以是有符号也可以是无符号的,不过有时候你可能要用不同的指令来处理他们。

比如,你想计算两个整数数组的和,你可以一次计算四个加法。



简单的例子

开始学习SSE并不是很简单的,幸好MSDN的文档写的很好(原作的链接打不开了,新连接是我加上去的)!

如果你看一下那个算术操作的列表,一会注意到总有相应的汇编指令与其对应。

另外,一些操作是符合操作,例如那些set操作。

在C++中用SSE真真是一个low-level的操作:我们将直接通过类型

__m128(4个float)、__m128d(2个double)、__m128i(int、short、char)直接控制那些128-bit的寄存器。


不过,为了使用SSE我们不必去声明__m128类型的数组:比如,你想计算一个浮点型数组中每个元素的平方根,

有可以直接将你的数组强制类型转换成__m128*,然后使用SSE的命令操作这个数组。


不管怎样,我们还是要多做一点事情,才能用SSE。大多数SSE操作需要我们的数据是16-bytes对齐的,

这里我们将使用另一个GCC的 Variable attributes。 我们使用对齐属性:



aligned (alignment)
This attribute specifies a minimum alignment for the variable or structure field, measured in bytes.


下面是一个简单的代码,展示如何用SSE的_mm_sqrt_ps()函数一次性计算四个浮点数的平方根:



float a[] __attribute__ ((aligned (16))) = { 41982.,  81.5091, 3.14, 42.666 };
__m128* ptr = (__m128*)a;
__m128 t = _mm_sqrt_ps(*ptr);




如果用GCC编译器,在编译选项中加入-S选项,产生的汇编代码中相应的汇编语句是SQRTPS,

而且这个指令使用的寄存器就是SSE的寄存器:


sqrtps  %xmm0, %xmm0


不要忘了加上那个头文件:



#include




第一个评测

在前面的代码中,我们同时计算了4个float的平方根,但是我们没有记录结果。为了记录结果,我们使用_mm_store_ps

在下面的代码中,我们计算一个非常大的float数组的平方根。(作者使用的是他之前写的计时函数,这里我直接贴出来了)

来对程序的标准版本和SSE版计时。


class Timer
{
public:
  Timer(const std::string& name)
    : name_ (name),
      start_ (std::clock())
    {
    }
  ~Timer()
    {
      double elapsed = (double(std::clock() - start_) / double(CLOCKS_PER_SEC));
      std::cout << name_ << ": " << int(elapsed * 1000) << "ms" << std::endl;
    }
private:
  std::string name_;
  std::clock_t start_;
};
#define TIMER(name) Timer timer__(name);

void normal(float* a, int N)
{
  for (int i = 0; i < N; ++i)a[i] = sqrt(a[i]);
} 
void sse(float* a, int N)
{// We assume N % 4 == 0.
  int nb_iters = N / 4;
  __m128* ptr = (__m128*)a;
  for (int i = 0; i < nb_iters; ++i, ++ptr, a += 4)
    _mm_store_ps(a, _mm_sqrt_ps(*ptr));
}
int main(int argc, char** argv)
{
  if (argc != 2)
    return 1;
  int N = atoi(argv[1]);
  float* a;
  posix_memalign((void**)&a, 16,  N * sizeof(float));
  for (int i = 0; i < N; ++i)a[i] = 3141592.65358;
  {
    TIMER("normal");
    normal(a, N);
  }
  for (int i = 0; i < N; ++i)a[i] = 3141592.65358;
  {
    TIMER("SSE");
	sse(a, N);
  }
}


在上面的SSE的函数代码中,我们用了两个指针指向的是同一个地址,但是使用的类型不同,这当然不是必须的,只是用来避免强制类型转换。

有趣的是,我们必须对__m128每次递增1(128bits),对应的,我们也必须按四递增float指针(就是相当于一次算四个float)。

另一个有趣的函数式 posix_memalign,而不是用align attribute,这个函数是在堆上申请对齐内存,而gcc attribute是在栈上申请内存。


评测环境: llvm-g++ 4.2 (flags: -O3 -msse2)  在Intel Core2 Duo P7350(2GHz)上测试。


$ ./sqrt 64000000
normal: 392ms
SSE: 145ms


真的相当快哈!


第二个评测


怎么将两个char数据加在一起呢:


void sse(char* a, const char* b, int N)                                                                                                                                                                          
{                                                                                                                                                                                         
  int nb_iters = N / 16;
  __m128i* l = (__m128i*)a;
  __m128i* r = (__m128i*)b;
 
  for (int i = 0; i < nb_iters; ++i, ++l, ++r)
    _mm_store_si128(l, _mm_add_epi8(*l, *r));
}


评测结果:


$ ./add 64000000
normal: 98ms
SSE: 42ms



性能分析


你可能会问,为什么我们没有得到四倍的加速呢?我们可是一次计算4个float数据啊,怎么我们只有2倍的加速呢??


答案是,你的编译器很聪明,它已经做了很多优化了,特别是在加入O3选项后。

实际上,如果你看下normal产生的汇编代码,里面的sqrt和add函数都已经被你的编译器给用SSE指令优化了。

编译器检测到循环模式适合SSE,就把这个代码使用SSE指令实现了。

不管怎样,直接使用SSE函数还是可以获得一些性能的。


取决于你的编译器版本,对于这种简单的循环,你发现执行时间上没有差异也是可能的。

但是,这里必须要再提一次的是,我们是介绍怎么用SSE,不是只为了性能~






本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: 驱动电源

在工业自动化蓬勃发展的当下,工业电机作为核心动力设备,其驱动电源的性能直接关系到整个系统的稳定性和可靠性。其中,反电动势抑制与过流保护是驱动电源设计中至关重要的两个环节,集成化方案的设计成为提升电机驱动性能的关键。

关键字: 工业电机 驱动电源

LED 驱动电源作为 LED 照明系统的 “心脏”,其稳定性直接决定了整个照明设备的使用寿命。然而,在实际应用中,LED 驱动电源易损坏的问题却十分常见,不仅增加了维护成本,还影响了用户体验。要解决这一问题,需从设计、生...

关键字: 驱动电源 照明系统 散热

根据LED驱动电源的公式,电感内电流波动大小和电感值成反比,输出纹波和输出电容值成反比。所以加大电感值和输出电容值可以减小纹波。

关键字: LED 设计 驱动电源

电动汽车(EV)作为新能源汽车的重要代表,正逐渐成为全球汽车产业的重要发展方向。电动汽车的核心技术之一是电机驱动控制系统,而绝缘栅双极型晶体管(IGBT)作为电机驱动系统中的关键元件,其性能直接影响到电动汽车的动力性能和...

关键字: 电动汽车 新能源 驱动电源

在现代城市建设中,街道及停车场照明作为基础设施的重要组成部分,其质量和效率直接关系到城市的公共安全、居民生活质量和能源利用效率。随着科技的进步,高亮度白光发光二极管(LED)因其独特的优势逐渐取代传统光源,成为大功率区域...

关键字: 发光二极管 驱动电源 LED

LED通用照明设计工程师会遇到许多挑战,如功率密度、功率因数校正(PFC)、空间受限和可靠性等。

关键字: LED 驱动电源 功率因数校正

在LED照明技术日益普及的今天,LED驱动电源的电磁干扰(EMI)问题成为了一个不可忽视的挑战。电磁干扰不仅会影响LED灯具的正常工作,还可能对周围电子设备造成不利影响,甚至引发系统故障。因此,采取有效的硬件措施来解决L...

关键字: LED照明技术 电磁干扰 驱动电源

开关电源具有效率高的特性,而且开关电源的变压器体积比串联稳压型电源的要小得多,电源电路比较整洁,整机重量也有所下降,所以,现在的LED驱动电源

关键字: LED 驱动电源 开关电源

LED驱动电源是把电源供应转换为特定的电压电流以驱动LED发光的电压转换器,通常情况下:LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: LED 隧道灯 驱动电源
关闭