SSE(Sreaming SIMD Extensions)入门 C

转载:https://blog.csdn.net/bendanban/article/details/42299863
http://felix.abecassis.me/2011/09/cpp-getting-started-with-sse/

                    <p>此文主要内容来自<a href="http://felix.abecassis.me/2011/09/cpp-getting-started-with-sse/" target="_blank">这篇文章</a>,本文翻译只求能理解,不求逐句翻译。</p><p>正文:</p><p>我们将在本文中介绍如何在C++/C中使用SSE指令。我的目的不是用SSE写尽可能快的程序,而是试图讲明白它的使用方法。</p><p><br></p><h2><a name="t0"></a>什么是SSE?</h2><p>&nbsp;SSE的全称是 Sreaming SIMD Extensions, 它是一组CPU指令,用于像信号处理、科学计算或者3D图形计算一样的应用。</p><p><br></p><p>SIMD 也是几个单词的首写字母组成的: Single Instruction, Multiple Data。 一个指令发出后,同一时刻被放到不同的数据上执行,</p><p>这个指令就是SIMD指令。</p><p><br></p><p>SSE在1999年首次出现在Pentium 3上。在过去的那段时光里,一些更加精致的功能被加入了这套指令集,</p><p>8个128-bit的寄存器被加入了CPU :xmm0到xmm7.</p><p><span style="font-size:12px;">&nbsp;</span></p><p><span style="font-size:12px;"><img src="https://img-blog.csdn.net/20141231214404364?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYmVuZGFuYmFu/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt=""></span></p><p><span style="font-size:12px;"><br></span></p><p>最初的时候,这些寄存器智能用来做单精度浮点数计算(float),</p><p>自从SSE2开始,这些寄存器可以被用来计算任何基本数据类型的数据了。</p><p><br></p><p>给定一个标准的32位机器,我们可以并行的存储和计算了:</p><p>-- 2 double</p><p>-- 2 long</p><p>-- 4 float&nbsp;</p><p>-- 4 int</p><p>-- 8 short</p><p>-- 16 char</p><p>注意:整数类型可以是有符号也可以是无符号的,不过有时候你可能要用不同的指令来处理他们。</p><p>比如,你想计算两个整数数组的和,你可以一次计算四个加法。</p><p><br></p><p><br></p><h2><a name="t1"></a>简单的例子</h2><p>开始学习SSE并不是很简单的,幸好<a href="http://msdn.microsoft.com/en-us/library/kcwz153a(v=vs.100).aspx" target="_blank">MSDN的文档</a>写的很好(原作的链接打不开了,新连接是我加上去的)!</p><p>如果你看一下那个<a href="http://msdn.microsoft.com/en-us/library/4atda1f2(v=vs.100).aspx" target="_blank">算术操作的列表</a>,一会注意到总有相应的汇编指令与其对应。</p><p>另外,一些操作是符合操作,例如那些<a href="http://msdn.microsoft.com/en-us/library/48z274sd(v=vs.100).aspx" target="_blank">set操作</a>。</p><p>在C++中用SSE真真是一个low-level的操作:我们将直接通过类型</p><p>__m128(4个float)、__m128d(2个double)、__m128i(int、short、char)直接控制那些128-bit的寄存器。</p><p><br></p><p>不过,为了使用SSE我们不必去声明__m128类型的数组:比如,你想计算一个浮点型数组中每个元素的平方根,</p><p>有可以直接将你的数组强制类型转换成__m128*,然后使用SSE的命令操作这个数组。</p><p><br></p><p>不管怎样,我们还是要多做一点事情,才能用SSE。大多数SSE操作需要我们的数据是16-bytes对齐的,</p><p>这里我们将使用另一个GCC的 Variable attributes。 我们使用对齐属性:</p><p><br></p><p></p><div class="dp-highlighter bg_plain"><div class="bar"><div class="tools"><b>[plain]</b> <a href="#" class="ViewSource" title="view plain" onclick="dp.sh.Toolbar.Command('ViewSource',this);return false;" target="_self">view plain</a><span class="tracking-ad" data-mod="popu_168"> <a href="#" class="CopyToClipboard" title="copy" onclick="dp.sh.Toolbar.Command('CopyToClipboard',this);return false;" target="_self">copy</a><div style="position: absolute; left: 174px; top: 2696px; width: 16px; height: 16px; z-index: 99;"><embed id="ZeroClipboardMovie_1" src="https://csdnimg.cn/public/highlighter/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" name="ZeroClipboardMovie_1" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=1&amp;width=16&amp;height=16" wmode="transparent" align="middle" width="16" height="16"></div></span><span class="tracking-ad" data-mod="popu_169"> <a href="#" class="PrintSource" title="print" onclick="dp.sh.Toolbar.Command('PrintSource',this);return false;" target="_self">print</a></span><a href="#" class="About" title="?" onclick="dp.sh.Toolbar.Command('About',this);return false;" target="_self">?</a></div></div><ol start="1"><li class="alt"><span><span>aligned&nbsp;(alignment)&nbsp;&nbsp;</span></span></li><li class=""><span>This&nbsp;attribute&nbsp;specifies&nbsp;a&nbsp;minimum&nbsp;alignment&nbsp;for&nbsp;the&nbsp;variable&nbsp;or&nbsp;structure&nbsp;field,&nbsp;measured&nbsp;in&nbsp;bytes.&nbsp;&nbsp;</span></li></ol></div><pre class="plain" name="code" style="display: none;">aligned (alignment)

This attribute specifies a minimum alignment for the variable or structure field, measured in bytes.
下面是一个简单的代码,展示如何用SSE的_mm_sqrt_ps()函数一次性计算四个浮点数的平方根:

  1. float a[] attribute ((aligned (16))) = { 41982.,  81.5091, 3.14, 42.666 };  
  2. m128* ptr = (__m128*)a;  
  3. __m128 t = _mm_sqrt_ps(*ptr);  
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的寄存器:

  1. sqrtps  %xmm0, %xmm0  
sqrtps  %xmm0, %xmm0

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

  1. #include <emmintrin.h>  
#include <emmintrin.h>


第一个评测

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

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

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

  1. class Timer  
  2. {  
  3. public:  
  4.   Timer(const std::string& name)  
  5.     : name (name),  
  6.       start_ (std::clock())  
  7.     {  
  8.     }  
  9.   ~Timer()  
  10.     {  
  11.       double elapsed = (double(std::clock() - start_) / double(CLOCKS_PER_SEC));  
  12.       std::cout << name_ << ”: ” << int(elapsed  1000) << “ms” << std::endl;  
  13.     }  
  14. private:  
  15.   std::string name_;  
  16.   std::clock_t start_;  
  17. };  
  18. #define TIMER(name) Timer timer(name);  
  19.   
  20. void normal(float a, int N)  
  21. {  
  22.   for (int i = 0; i < N; ++i)a[i] = sqrt(a[i]);  
  23. }   
  24. void sse(float a, int N)  
  25. {// We assume N % 4 == 0.  
  26.   int nb_iters = N / 4;  
  27.   __m128 ptr = (__m128*)a;  
  28.   for (int i = 0; i < nb_iters; ++i, ++ptr, a += 4)  
  29.     _mm_store_ps(a, _mm_sqrt_ps(ptr));  
  30. }  
  31. int main(int argc, char argv)  
  32. {  
  33.   if (argc != 2)  
  34.     return 1;  
  35.   int N = atoi(argv[1]);  
  36.   float a;  
  37.   posix_memalign((void)&a, 16,  N * sizeof(float));  
  38.   for (int i = 0; i < N; ++i)a[i] = 3141592.65358;  
  39.   {  
  40.     TIMER(”normal”);  
  41.     normal(a, N);  
  42.   }  
  43.   for (int i = 0; i < N; ++i)a[i] = 3141592.65358;  
  44.   {  
  45.     TIMER(”SSE”);  
  46.     sse(a, N);  
  47.   }  
  48. }  
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)上测试。

  1. &nbsp;./sqrt&nbsp;64000000&nbsp;&nbsp;</span></span></li><li class=""><span>normal:&nbsp;392ms&nbsp;&nbsp;</span></li><li class="alt"><span>SSE:&nbsp;145ms&nbsp;&nbsp;</span></li></ol></div><pre class="plain" name="code" style="display: none;"> &nbsp;./sqrt&nbsp;64000000&nbsp;&nbsp;</span></span></li><li class=""><span>normal:&nbsp;392ms&nbsp;&nbsp;</span></li><li class="alt"><span>SSE:&nbsp;145ms&nbsp;&nbsp;</span></li></ol></div><pre class="plain" name="code" style="display: none;"> ./sqrt 64000000
    normal: 392ms
    SSE: 145ms
    真的相当快哈!


    第二个评测


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

    1. void sse(char a, const char b, int N)                                                                                                                                                                            
    2. {                                                                                                                                                                                           
    3.   int nb_iters = N / 16;  
    4.   __m128i* l = (__m128i*)a;  
    5.   __m128i* r = (__m128i*)b;  
    6.    
    7.   for (int i = 0; i < nb_iters; ++i, ++l, ++r)  
    8.     _mm_store_si128(l, _mm_add_epi8(l, *r));  
    9. }  
    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));
    }


    评测结果:

    1. &nbsp;./add&nbsp;64000000&nbsp;&nbsp;</span></span></li><li class=""><span>normal:&nbsp;98ms&nbsp;&nbsp;</span></li><li class="alt"><span>SSE:&nbsp;42ms&nbsp;&nbsp;</span></li></ol></div><pre class="plain" name="code" style="display: none;"> &nbsp;./add&nbsp;64000000&nbsp;&nbsp;</span></span></li><li class=""><span>normal:&nbsp;98ms&nbsp;&nbsp;</span></li><li class="alt"><span>SSE:&nbsp;42ms&nbsp;&nbsp;</span></li></ol></div><pre class="plain" name="code" style="display: none;"> ./add 64000000
      normal: 98ms
      SSE: 42ms

      性能分析


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


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

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

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

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


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

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






  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值