• 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。 我们使用对齐属性:

    [plain] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. aligned (alignment)  
    2. This attribute specifies a minimum alignment for the variable or structure field, measured in bytes.  


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

    [plain] view plaincopy在CODE上查看代码片派生到我的代码片
     
    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);  



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

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

    [plain] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. sqrtps  %xmm0, %xmm0  


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

    [cpp] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. #include <emmintrin.h>  



    第一个评测

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

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

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

    [cpp] view plaincopy在CODE上查看代码片派生到我的代码片
     
    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. }  


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

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

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

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

    [plain] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. $ ./sqrt 64000000  
    2. normal: 392ms  
    3. SSE: 145ms  


    真的相当快哈!

    第二个评测

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

    [cpp] view plaincopy在CODE上查看代码片派生到我的代码片
     
    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. }  


    评测结果:

    [plain] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. $ ./add 64000000  
    2. normal: 98ms  
    3. SSE: 42ms  



    性能分析

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

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

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

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

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

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

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

    来源:http://blog.csdn.net/bendanban/article/details/42299863

              http://blog.csdn.net/gengshenghong/article/details/7008704 

  • 相关阅读:
    python学习笔记(2)--sublimeText3运行python
    python学习笔记(1)--遍历txt文件,正则匹配替换文字
    JS学习笔记(4)--js变量的生命周期
    JS学习笔记(3)--json格式数据的添加,删除及排序方法
    JS学习笔记(2)--正则表达式获取指定字符串
    JS学习笔记(1)--sort排序
    VBA学习笔记(1)----VBA对象属性方法
    能够作图的软件都有哪些
    怎么用ChemDraw连接两个结构片段
    在几何画板上画椭圆可以根据椭圆第二定义
  • 原文地址:https://www.cnblogs.com/tibetanmastiff/p/4394608.html
Copyright © 2020-2023  润新知