如何编写可以自动矢量化或向量化代码

可以自动矢量化的代码


使用gcc version 4.3.4的gcc编译器,编译下面的示例代码:

int main(){

  const unsigned int ArraySize = 10000000;

  float* a = new float[ArraySize];

  float* b = new float[ArraySize];

  float* c = new float[ArraySize];

  for (unsigned int j = 0; j< 200 ; j++) // some repetitions

    for ( unsigned int i = 0; i < ArraySize; ++ i)

        c[i] = a[i] * b[i];

}

编译并运行上面的代码:

g++ vectorisable_example.cpp -o vectorisable_example -O2 time ./vectorisable_example

简单,现在我们看下如何告诉 compiler 自动对代码进行矢量化:

g++ vectorisable_example.cpp -o vectorisable_example_vect -O2 -ftree-vectorize time ./vectorisable_example_vect

第二种编译方式,产生的可执行程序比第一种产生的可执行程序运行的更快?我们深入研究下,看看为什么他就这么快?

简介


从上世纪九十年代后期开始,英特尔就已经将单指令多数据(SIMD)机器指令集成到其商品CPU产品线中。这些SIMD指令允许一次将相同的数学运算(乘,加...)应用于2,4或甚至更多的数据值上。这组数据值也被称为“vector”(不要与代数中的vector混淆)。理论上矢量计算的性能增益等于CPU可以容纳的矢量单位的数量。

尽管SIMD指令集已经集成到普通CPU中相当长一段时间了,但是这些扩展功能(SIMD指令集)只能通过在C或者C++代码中通过准汇编语言进行使用。像GNU编译器系列(GCC)这样的现代编译器现在可以将常规的C或C ++源代码转换成向量操作。这个过程被称为自动矢量化(auto-vectorization)。
这篇文章,我们将会介绍必要的软件工具和编程技术,并进一步提供利用GCC自动矢量化功能的示例源代码

硬件


要估算自动矢量化代码可以带来多大的性能提升,首先需要了解您正在使用的CPU的能力以及CPU可以处理多少个Vector。因此,运行以下命令:

$ cat /proc/cpuinfo...
flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2ss ht tm pbe syscall nx rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2ssse3cx16 xtpr pdcmsse4_1 sse4_2x2apic popcnt xsaveavxlahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid...

在CPU属性中,您将找到有关SIMD功能的信息(以粗体突出显示)。若信息中包含下表列出的字符串,则您的CPU支持(SIMD)功能:

Name Register Size Amount of floats Amount of doubles
mmx (*) 64 bit 0 0
sse2 128 bit 4 2
ssse3 128 bit 4 2
sse4_1 128 bit 4 2
sse4_2 128 bit 4 2
avx 256 bit 8 4
  • 英特尔处理器上的第一个矢量单元只能容纳两个迭代器。

所有Intel和AMD 64位CPU都至少支持sse2指令集。 AVX指令集已于2011年推出,采用Sandy Bridge架构,可在最新一代Mac Book Pro笔记本电脑上使用。
AVX指令集可以同时对四个双精度浮点值应用数学运算。因此,与未使用矢量单位的版本相比,应用程序可以实现的最大性能增益是4倍。
Vector单位大小的持续增大,反应了是计算机微体系结构演化的新趋势。十年前,计算机的时钟频率几乎每年翻一番。计算机的时钟频率已经达到了极限,制造商转而开始提供越来越大的矢量单位,并且在每个芯片中提供越来越多的核心数量。

哪些代码可以被auto-vectorized ?


为了将计算分布到CPU的矢量单元,编译器必须对源代码的依赖性和相互影响有一个很好的理解。但最重要的是,编译器必须能够检测出那些代码段可以使用SIMD指令进行优化。最简单的情况是对数组执行计算的循环:

for ( unsigned int i = 0; i < ArraySize; ++ i)
{
      c[i] = a[i] * b[i];
}

使用等于或者高于gcc461(在slc5_amd64_gcc461 scram体系结构的服务器中可用)版本的gcc编译器,编译的时候,一定要带上is-ftree-vectorize

注意:从自动矢量化中获益最多的循环是包含数学运算的循环。只是迭代对象集合的循环不会获利,在大多数情况下甚至不能自动矢量化。

可以在这里找到可以自动矢量化的循环结构列表:http://gcc.gnu.org/projects/tree-ssa/vectorization.html
一旦你使用选项-ftree-vectorizer-verbose = 7编译你的代码,GCC将会给你一个关于你的程序中所有循环的详细报告,以及它们是否已经被自动矢量化了。以下报告是成功对循环进行向量化的结果:

autovect.cpp:66: note: vect_model_load_cost: aligned.

autovect.cpp:66: note: vect_get_data_access_cost: inside_cost = 1, outside_cost = 0.

autovect.cpp:66: note: vect_model_load_cost: aligned.

autovect.cpp:66: note: vect_get_data_access_cost: inside_cost = 2, outside_cost = 0.

autovect.cpp:66: note: vect_model_store_cost: aligned.

autovect.cpp:66: note: vect_get_data_access_cost: inside_cost = 3, outside_cost = 0.

autovect.cpp:66: note: vect_model_load_cost: aligned.

autovect.cpp:66: note: vect_model_load_cost: inside_cost = 1, outside_cost = 0 .

autovect.cpp:66: note: vect_model_load_cost: aligned.

autovect.cpp:66: note: vect_model_load_cost: inside_cost = 1, outside_cost = 0 .

autovect.cpp:66: note: vect_model_simple_cost: inside_cost = 1, outside_cost = 0 .

autovect.cpp:66: note: vect_model_simple_cost: inside_cost = 1, outside_cost = 1 .

autovect.cpp:66: note: vect_model_store_cost: aligned.

autovect.cpp:66: note: vect_model_store_cost: inside_cost = 1, outside_cost = 0 .

autovect.cpp:66: note: Cost model analysis:

Vector inside of loop cost: 5

Vector outside of loop cost: 11

Scalar iteration cost: 5

Scalar outside cost: 0

prologue iterations: 0

epilogue iterations: 2

Calculated minimum iters for profitability: 3

autovect.cpp:66: note:  Profitability threshold = 3

autovect.cpp:66: note: Profitability threshold is 3 loop iterations.

autovect.cpp:66: note: LOOP VECTORIZED.

若一个循环不能被向量化, GCC将会给出原因:

autovect.cpp:133: note: not vectorized: control flow in loop.

上面的输出的含义是:在循环中调用了tostd :: cout,引入了一个控制流,从而无法对该循环进行向量化。

让GCC自动对你的代码进行向量化


想要让GCC对自己的代码进行自动向量化,需要使用最新的编译器,建议使用至少GCC 4.6.1版本的编译器,通常来说,版本越新越好。

编译器标示


想要打开auto-vectorization,使用标示:

-ftree-vectorize

如果您使用优化级别-O3or进行编译,则隐式启用此选项。
想要得到哪些loop是已经被auto-vectorized和哪些loops没有被成功矢量化以及原因,可以使用选项:

-ftree-vectorizer-verbose=7

关于如何阅读这个输出,请看下面的Real-World代码示例部分。
某些循环结构(例如浮点数的减法)只能在允许编译器改变数学运算顺序的情况下进行矢量化。要做到这一点,需要使用选项:

-ffast-math

如果您使用优化级别-Ofast,则会隐式启用该选项。请注意,fast-math选项会修正修改浮点运算中的错误操作。详情请看http://gcc.gnu.org/onlinedocs/gcc-4.1.1/gcc/Optimize-Options.html
此外,您可以明确指出编译器在生成二进制文件时应使用哪一个SIMD指令集。在编译x86-64架构时,GCC默认使用SSE2指令集。如果要启用AVX指令集,请使用编译器标志:

-mavx

需要注意的是,要运行二进制文件的机器必须支持AVX指令集。您还可以让通过下面的选项,让编译器自己决定使用机器中的哪个指令集:

-march=native

若你想要使用 C++11 的特性,例如:lambda 表达式,一定要确认开启了新的C++ Standard:

-std=gnu++0x

启用auto-vectorization最佳实践


使用数组结构


想要启用自动向量化,编译器不仅需要能够分析循环迭代,还需要能够分析访问内存的模式。嵌套数组的复杂类型很难或不可能由编译器自动进行矢量化。建议使用简单的c数组orstd :: arrays(在C ++ 11中引入)来保存数据,并允许编译器以连续的方式访问数据。这也将使您的程序能够利用CPU的各种缓存。

如果你需要可变数组,建议使用std :: vectorand 获取指向第一个元素的指针来访问循环中的元素:

std::vectormyData(23);

double * pMyData = &myData.front();

...

pMyData[i] += 5;

数组结构的完整示例可以在下面的“综合代码示例”一节中找到。

限制分支


尽力限制for循环内的分支。 GCC能够将一些if语句翻译成向量化的代码,但很有限。这样做时,将评估所有可能的分支,并丢弃未采用分支的值。

尽量简化代码


如果在for循环中有复杂的计算,您希望GCC对其进行矢量化,可以考虑使用C ++ 11中的lambda表达式,将它们分解为更简单的代码。在这里可以找到C ++11新功能的介绍:http://en.wikipedia.org/wiki/Anonymous_function#C.2B.2B

这里是一个lambda函数的例子,用来计算一个double变量的平方。

auto kernel_square =

[] // capture nothing

( double const& a) // take 1 parameter by reference

->double // lambda function returns the square

{

return ( a * a );

};

在循环中调用lambda函数:

for ( unsigned int i = 0; i < size; ++ i)

{

ptr_square[i] = kernel_square( ptr_x[i] ) + kernel_square( ptr_y[i] ) + kernel_square( ptr_z[i] ) ;

}

请注意,此代码将由GCC自动矢量化。对lambda函数的调用不会像常规函数调用那样,造成特别大的开销,因为代码会被完全内联。

另一个更全面的例子是lambda函数中的for循环。这个循环也会被GCC自动矢量化:

// Defining a compute kernel to encapsulate a specific computation

auto kernel_multiply =

[ &cFixedMultiply ] // capture the constant by reference of the scope of the lambda expression

( DataArray const& a, DataArray const& b, DataArray & res ) // take 3 parameters by reference

->void // lambda function returns void

{

// simple loop vectorized

for( unsigned int i = 0; i < a.size(); ++ i)

{

res[i] = a[i] * b[i];

}

};

不要调用外部函数


调用外部函数,如exp(),log()等等,会导致循环不能被向量化。因此,您必须决定循环中的数学运算是否足够,从而将循环分解。这意味着,在第一个循环中,计算调用exp()时,需要使用的参数,并将此结果存储在临时数组中。GCC会自动矢量化这个循环。第二个循环将简单地执行对exp()的调用并存储结果。

如果要调用的函数是由您控制的,请尽量尝试将此函数指定为C ++ 11 lambda表达式。可以在这里找到该特性的介绍:http://en.wikipedia.org/wiki/Anonymous_function#C.2B.2B

在循环中使用普通的整数计数器


使用普通的整数计数器来构建for循环,而不是std :: iterator,因为这些使得GCC难以分析内存访问和循环的属性,如迭代计数。


for (vector::iterator it = y.begin(); it != y.end(); it++)

{

(*it) += 23;

}

变为:


for (unsigned int i = 0; i < y.size(); ++i)

{

y[i] += 23;

}

综合代码示例


下面的代码给出了一些关于如何用GCC编写自动矢量化C ++代码的例子。复制并粘贴源代码并使用下面命令进行编译即可:

g++ -Ofast -ftree-vectorizer-verbose=7 -march=native -std=c++11 -o autovect autovect.cpp

请保证使用的gcc版本至少是:GCC 4.7

/*

   Compile with ( use at least gcc 4.7 ):
   g++ -Ofast -ftree-vectorizer-verbose=7 -march=native -std=c++11 -o autovect autovect.cpp

*/

#include <math.h>

#include <string>
#include <iostream>
#include <array>
#include <vector>

// Sturcture-Of-Array to hold coordinates
struct Vector3
{
   std::vector<double> x;
   std::vector<double> y;
   std::vector<double> z;
   
   // final result of the distance calcualtion
   std::vector<double> distance;

   void add( double _x, double _y, double _z )
   {
      x.push_back( _x );
      y.push_back( _y );
      z.push_back( _z );
      distance.push_back( 0.0f );
   }

};

   
int main()
{

// Fixed Size Arrays

   typedef std::array<double, 10> DataArray;

   DataArray vect_a = { 0,1,2,3,4,5,6,7,8,9 };
   DataArray vect_b = {0.5,1,2,3,4,5,6,7,8,9 };
   DataArray vect_res_plain = { 0,0,0,0,0,0,0,0,0,0};   
   DataArray vect_res_lambda = { 0,0,0,0,0,0,0,0,0,0};

   constexpr double cFixedMultiply = 23.5f;

   // simple loop vectorized
   // -- auto-vectorized --
   for( unsigned int i = 0; i < vect_a.size(); ++ i)
   {
      vect_res_plain[i] = vect_a[i]  + vect_b[i];
   }

   // Defining a compute kernel to encapsulate a specific computation
   auto kernel_multiply = 
      [ &cFixedMultiply ] // capture the constant by reference of the scope of the lambda expression
      ( DataArray const& a, DataArray const& b, DataArray & res ) // take 3 parameters by reference
      ->void // lambda function returns void
   {
      // simple loop vectorized
      // -- auto-vectorized --
      for( unsigned int i = 0; i < a.size(); ++ i)
      {
         res[i] = a[i] * b[i] * cFixedMultiply;
      }
   };

   // call the lambda function
   // this call is autovectorized
   kernel_multiply ( vect_a, vect_b, vect_res_lambda );


   // This kernel will be called multiple times and performs the quadrature
   auto kernel_square = 
      [] // capture nothing
      ( double const& a) // take 1 parameter by reference
      ->double // lambda function returns the square
   {
      return ( a * a );
   };

   // create struct and fill with dummy values
   Vector3 v3;
   for ( unsigned int i = 0; i < 50 ; ++ i)
   {
      v3.add( i * 1.1, i * 2.2,  i* 3.3 );   
   }

   // store the size in a local variable. This is needed to GCG
   // can estimate the loop iterations
   const unsigned int size = v3.x.size();

   // -- auto-vectorized --
   for ( unsigned int i = 0; i < size; ++ i)
   {
      v3.distance[i] = sqrt( kernel_square( v3.x[i] ) + kernel_square( v3.y[i] ) + kernel_square( v3.z[i] )) ;
   }

   // output the result, so GCC won't optimize the calculations away
   std::cout << std::endl << "Computation on std::array" << std::endl;
   for( unsigned int i = 0; i < vect_a.size(); ++ i)
   {
      std::cout << vect_res_plain[i] << std::endl;
      std::cout << vect_res_lambda[i] << std::endl;
   }

   std::cout << std::endl << "Computation on Structure-of-Array with variable sized std::vectors" << std::endl;
   for( unsigned int i = 0; i < v3.x.size(); ++ i)
   {
      std::cout << "sqrt( " << v3.x[i] << "^2 + " << v3.y[i] << "^2 + " << v3.z[i] << "^2 ) = " 
                << v3.distance[i] << std::endl;
   }

   return 0;
}

自动矢量化的实际应用


在这里,您可以找到一部分利用GCC编译器的自动矢量化功能的应用列表:
The VDT mathematical library

参考: https://twiki.cern.ch/twiki/bin/view/CMSPublic/WorkBookWritingAutovectorizableCode

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容

  • 一:名词解释 Flynn分类法 Flynn于1972年提出了计算平台的Flynn分类法,主要根据指令流和数据流来分...
    金科_阅读 3,055评论 0 1
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,515评论 25 707
  • 一、温故而知新 1. 内存不够怎么办 内存简单分配策略的问题地址空间不隔离内存使用效率低程序运行的地址不确定 关于...
    SeanCST阅读 7,779评论 0 27
  • 嗯,我们的记忆感受的强力来排序,情绪不但强烈地影响着我们的行为,影响人类的学习过程,更强烈的情绪占据更优先的几,如...
    大路和小路阅读 223评论 0 0
  • 我们每天在都市丛林里来回奔波忙碌,如果算一种修炼,那最终修炼成果的是什么呢? 答案应该很多,我的答案是:判断力。 ...
    茉莉大大阅读 213评论 0 1