一个卓有成效的汇编优化范例--使用SSE2指令优化进制转化

来源:互联网 发布:淘宝有卖烟的吗 编辑:程序博客网 时间:2024/05/16 23:02

    我的一个感兴趣的编程方向是大数计算,因此用汇编语言写了很多大数计算方面的小程序,上周突然想出一个使用SSE2指令将整数转为16进制字符串的好主意,遂付诸实现。原以为至多可提速500%,那知测试后发现,相对于最初的C语言版本,速度竟提高20倍以上,兴奋之余,遂有了这篇博客文章。

    这个程序主要示范将64bit一个整数转化为16进制字符串的功能,功能和算法都比较简单。我相信许多人都写过类似的程序,但不知有没有人尝试去你优化它。这个示范程序包括3个C语言版和1个使用SSE2指令的汇编语言版。下面我们给出代码和说明。

   先看这个函数最初的版本,UINT64_2_hexString_c1,为了性能起见,我们使用 __fastcall 函数约定,__fastcall 接口的函数使用寄存器来传递参数,免除了调用时压栈的开销,而且被调函数可以省去保存/恢复寄存器等指令。

//这是C语言普通版,直接使用64位整数逻辑指令和算术指令void __fastcall UINT64_2_hexString_c1(UINT64 *p, char *buff){  UINT64 x=*p;  int i;  for (i=15;i>=0;i--)  {    char c= (x & 0xf)+'0';    if ( c>'9')      c+=7;    buff[i]=c;    x>>=4;  }  buff[16]=0;}

    上面这个函数虽然简单,然而速度却仍不理想。我们知道,在32位运行环境,对64位整数计算的C语言语句要翻译成多条指令,故速度较慢,下面这个版本使用完全的32位整数处理,故速度快于上面的版本。

//这是C语言改进版,使用32位整数逻辑指令和算术指令void __fastcall UINT64_2_hexString_c2(UINT64 *p, char *buff){DWORD *pDW=(DWORD *)p;DWORD x;int i;for (x=pDW[1],i=7;i>=0;i--){char c= (x & 0xf)+'0';if ( c>'9')c+=7;buff[i]=c;x>>=4;}for (x=pDW[0],i=7;i>=0;i--){char c= (x & 0xf)+'0';if ( c>'9')c+=7;buff[8+i]=c;x>>=4;}buff[16]=0;}

    上面这个函数首先将64位整数地址转化32位整数的地址,然后使用32位整数运算。代码是复杂了,但速度更快了。尽管如此,程序仍有优化的空间。

     我们注意到,上面的函数包括2个循环,在每个循环中又有一个if语句,站在汇编语言视角,循环和if语句都是分支语句,可编译成比较跳转指令对。分支对CPU是个麻烦事儿,由于现代CPU普遍采用管线技术,在执行当前指令时,后面的指令已经被取到甚至译码完成。CPU遇到分支时,就需要预测那个分支最可能被执行到,从而将最可能被执行到的那个分支的代码加载到管线。当分支预测成功时,所有的指令可流畅地无停顿的执行。一旦分支预测失败,则不得不将管线中已加载测的指令全部丢弃,重新从正确的分支点取指和译码。分支预测的技术很复杂,完整的讲述需要一本书的内容。我们这里仅作简单介绍。分支预测的的实现通常是这样的,在首次遇到分支时,执行非跳转分支。在每次执行分支指令时,将实际执行情况(执行那个分支)存入历史记录。以后再遇到这个分支时,则可以根据历史记录来判断那个分支最可能被执行到。最简单的一种判断算法是,总是预测执行概率比较高的那个分支,这种分支预测方案对循环引起的分支最有效。比如一个循环次数为n的for循环,前n次总是从循环体底部跳转到头,只有最后一次循环不跳转,换言之,跳转分支执行的概率远高于非跳转分支,故CPU总是预测跳转分支。就上面的例子而言,两个循环都是固定次数的循环且循环次数很少。在这种情况下,编译器可使用循环展开的方法来消除分支,但是对于语句"if ( c>'9' ) c+=7" 这样的分支,分支预测技术很难奏效,0-15之间的随机数,大于9的概率37.5%,即使CPU总是预测<='9'的那个分支,也有37.5%的预测失败的概率,分支预测失败,CPU需要付出相当的代价,需要几个甚至10个额外的周期。

   我的下一个版本用到的技术就是分支消除技术,通过消除分支来提高函数执行速度,为了消除分支,不得不使用额外的语句,虽然代码变多了,但函数执行速度大大加快。

//这是使用消除分支技术的C语言版,在i7-4700HQ,速度是上一个版本的2.6倍void __fastcall UINT64_2_hexString_c3(UINT64 *p, char *buff){DWORD *pDW=(DWORD *)p;DWORD x;int i;for (x=pDW[1],i=7;i>=0;i--){char c= (x & 0xf)+'0';char mask= 0 -( c>'9');//the flag is 0xff when c>'9'buff[i]= c + ( mask & 7);x>>=4;}for (x=pDW[0],i=7;i>=0;i--){char c= (x & 0xf)+'0';char mask= 0 -( c>'9');//the flag is 0xff when c>'9'buff[i+8]= c + ( mask & 7);x>>=4;}buff[16]=0;}

   下面是终极版本,使用SSE2指令的汇编版,直接使用ALU指令编程的优化作用有限,甚至不如编译器。我们这里直接使用SSE2指令,SSE2指令主要使用XMM寄存器来工作,1个XMM寄存器可看成是4个DWORD,8个WORD,16个BYTE,1个UINT64位数转化为字符串后有16个字符,可全部装在一个XMM寄存器中,所以这个工作正好适合用SSE2指令来做。下面的汇编版的代码,用到几个16字节数组,要求16直接对齐,尽管在汇编中也可以定义16字节对齐,但我们这里把数组的定义放在C文件中,放在C文件中的好处是易于扩展,比如可在C文件中定义32字节对齐,我曾尝试在汇编文件中定义32字节对齐时,但汇编器总是报错。这里给出C语言中的常数数组的定义。

#include <stdio.h>#include <stdlib.h>typedef unsigned char  BYTE;#if defined(__GNUC__)    // GCC    #define INTRIN_ALIGN(n)    __attribute__((aligned(n)))#else        #define INTRIN_ALIGN(n)    __declspec(align(n))#endif    // #if defined(__GNUC__)    // GCCINTRIN_ALIGN(16) BYTE i256_num_0s[16]={'0','0','0','0','0','0','0','0','0','0','0','0','0','0','0','0',};INTRIN_ALIGN(16) BYTE i256_num_9s[16]={'9','9','9','9','9','9','9','9','9','9','9','9','9','9','9','9',};INTRIN_ALIGN(16) BYTE i256_num_full7[16]={7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,};

下面是微软汇编器ml.exe格式的汇编语言源代码。

 .686P.XMM.modelflat_DATASEGMENTEXTRN _i256_num_0s:BYTEEXTRN _i256_num_9s:BYTEEXTRN _i256_num_full7:BYTE_DATAENDSPUBLIC @UINT64_2_hexString_sse2@8_TEXTSEGMENT; The XMM register definition for function _UINT64_2_hexString_sse2XMM_R_DB_STR_0  TEXTEQU <XMM2>XMM_R_DB_STR_9  TEXTEQU <XMM3>XMM_R_DB_7TEXTEQU <XMM4>XMM_R_TMPTEXTEQU <XMM1>@UINT64_2_hexString_sse2@8 PROCmovq    XMM0, mmword ptr [ecx]PSHUFD  XMM0, XMM0, 11001101b;now XMM0 contain 2 QWORD and low 32bits in every QWORD is valid; We denote the value of xmm register with a string, "N" means a byte, "0" means a byte whose value is 0,; We use low-bytes first order,  ; Now, the value of XMM0 is (NNNN0000,NNNN0000),contain 2 QWORD and contain 32 bits in every qword; the first round transform, 2 QWORD => 4 DWORDMOVDQA  XMM_R_TMP, XMM0PSLLQ  XMM0, 48;toward high shift 48 bits; now  value of  XMM0 is (000000NN, 000000NN), now bit0-bit15 of every original QWORD is in XMM0 PSRLQ  XMM_R_TMP, 16;toward low shift 16 bits; now  value of  XMM1 is (NN000000,NN000000), bit16-bit31 of every original QWORD is in XMM1PSRLQ  XMM0, 16;toward low shift 16 bits; now  value of  XMM0 is  (0000NN00,0000NN00), now bit0-bit15 of every original QWORD is in XMM0 PORXMM0, XMM_R_TMP;merge bit0-bit15 and bit16-bit31; now  value of  XMM0 is (NN00,NN00,NN00,NN00), contain 4 DWORD, and 16 bits in every DWORD is valid ; the second round transform, 4 DWORD  => 8 WORDMOVDQA  XMM_R_TMP, XMM0PSLLD  XMM0, 24;toworad high shift 24 bits; now  value of  XMM0 is (000N,000N,000N,000N), the bit0-bit7 of every original DWORD is in XMM0 PSRLD  XMM_R_TMP, 8;toworad low shift 8 bits; now  value of  XMM0 is (N000,N000,N000,N000), the bit8-bit15 of every original DWORD is in XMM1PSRLD  XMM0, 8;toworad low shift 8 bits; now  value of  XMM0 is (00N0,00N0,00N0,00N0), the bit0-bit7 of every original DWORD is in XMM0 PORXMM0, XMM_R_TMP;merge bit0-bit7 and bit8-bit16; now  value of  XMM0 is (N0,N0,N0,N0,N0,N0,N0,N0), contain 8 WORD, and 8 bits in every WORD is valid; the third round transform, 8 WORD  => 16 BYTEMOVDQA  XMM_R_TMP, XMM0PSLLW  XMM0, 12;toworad high shift 12 bits; the bit0-bit3 of every original WORD is in XMM0 PSRLW  XMM_R_TMP, 4;toworad low shift 4 bits; now  the bit4-bit7 or every original WORD is in XMM1PSRLW  XMM0, 4; toworad low shift 4 bits; the bit0-bit3 of every original WORD is in XMM0 PORXMM0, XMM_R_TMP;merge bit0-bit3 and bit4-7; now  value of  XMM0 is (N,N,N,N,N,N,N,N,N,N,N,N,N,N,N,N,) contain 16 BYTE, the value range of every byte is 0-15; pre-load array to XMM  registerMOVDQA  XMM_R_DB_STR_0, xmmword ptr _i256_num_0sMOVDQA  XMM_R_DB_STR_9, xmmword ptr _i256_num_9sPOR     XMM0, XMM_R_DB_STR_0;now the BYTE[i] of XMM0 is '0' to '0'+15MOVDQA  XMM_R_TMP, XMM0 ;now the BYTE[i] of XMM_R_TMP is '0' to '0'+15MOVDQA  XMM_R_DB_7,xmmword ptr _i256_num_full7;now XMM_R_DB_7 is full 7PCMPGTB  XMM_R_TMP, XMM_R_DB_STR_9; BYTE[i] of XMM_R_TMP (0<=i<15) is -1 if BYTE[i]>'9'PAND     XMM_R_TMP, XMM_R_DB_7; BYTE[i] of XMM_R_TMP (0<=i<15) is 7, if BYTE[i]>'9'PADDB    XMM0, XMM_R_TMP;final result,now the BYTE[i] of XMM0 is '0'-'9' or 'A' to 'F' MOVDQU   xmmword ptr [edx], XMM0mov    byte ptr [edx+16], 0ret0@UINT64_2_hexString_sse2@8 ENDP_TEXTENDSEND
  上面我没有将英文注释翻译成中文。这是因为,对于汇编代码,高手不用讲,初学者不会看,故这里就不再给出更多的说明了。

下面给出主程序中的全部代码。

#include <stdio.h>#include <stdlib.h>#include <string.h>/*这个程序示范使用优化技术来实现将64位整数转化为16进制字符串。包括3个C语言的版本和一个使用SSE2指令的汇编版本。经测试,在我的电脑上(I7-4700HQ),汇编版本的速度是C语言普通版20倍,是分支消除技术版本的7.5倍。作者:Baocheng Liang完成日期:2015-7-30, 版权所有。*/#define ARRAY_LEN   4096#define LOOP_COUNT2048typedef unsigned long long UINT64;typedef unsigned long DWORD;typedef unsigned short WORD;typedef unsigned char BYTE;UINT64 g_nums[ARRAY_LEN];char g_buff[ARRAY_LEN*16+16];extern double currTime(); //使用高精度计时器extern void __fastcall UINT64_2_hexString_sse2(UINT64 *p, char *buff);//使用SSE2指令的汇编版本extern void __fastcall UINT64_2_hexString_c1(UINT64 *p, char *buff);//最普通的C语言版本extern void __fastcall UINT64_2_hexString_c2(UINT64 *p, char *buff);//最普通32位整数的的C语言版本extern void __fastcall UINT64_2_hexString_c3(UINT64 *p, char *buff);//使用分支消除技术的C语言版本typedef void ( __fastcall *lpfn_UInt64_2_hexString)(UINT64 *p, char *buff);//这是C语言普通版,直接使用64位整数逻辑指令和算术指令void __fastcall UINT64_2_hexString_c1(UINT64 *p, char *buff){UINT64 x=*p;int i;for (i=15;i>=0;i--){char c= (x & 0xf)+'0';if ( c>'9')c+=7;buff[i]=c;x>>=4;}buff[16]=0;}//这是C语言改进版,使用32位整数逻辑指令和算术指令void __fastcall UINT64_2_hexString_c2(UINT64 *p, char *buff){DWORD *pDW=(DWORD *)p;DWORD x;int i;for (x=pDW[1],i=7;i>=0;i--){char c= (x & 0xf)+'0';if ( c>'9')c+=7;buff[i]=c;x>>=4;}for (x=pDW[0],i=7;i>=0;i--){char c= (x & 0xf)+'0';if ( c>'9')c+=7;buff[8+i]=c;x>>=4;}buff[16]=0;}//这是使用消除分支技术的C语言版,在i7-4700HQ,速度是上一个版本的2.6倍void __fastcall UINT64_2_hexString_c3(UINT64 *p, char *buff){DWORD *pDW=(DWORD *)p;DWORD x;int i;for (x=pDW[1],i=7;i>=0;i--){char c= (x & 0xf)+'0';char mask= 0 -( c>'9');//the flag is 0xff when c>'9'buff[i]= c + ( mask & 7);x>>=4;}for (x=pDW[0],i=7;i>=0;i--){char c= (x & 0xf)+'0';char mask= 0 -( c>'9');//the flag is 0xff when c>'9'buff[i+8]= c + ( mask & 7);x>>=4;}buff[16]=0;}void test_UINT64_2_hexString(lpfn_UInt64_2_hexString fp, const char *funcName){UINT64 arr[]={0x0123456789abcdef,0x02468ACE13579BDF,0xaaaaaaaaaaaaaaaa,0xffffffffffffffff,};int i;char buff[17*4];memset(buff,0,sizeof(buff));printf("Test function %s\n",funcName);for (i=0;i<4;i++){fp(arr+i,buff+i*17);printf("%s\n", buff+i*17);}}void perf_UINT64_2_hexString(lpfn_UInt64_2_hexString fp,const char *funcName){int i,j;UINT64 x;char *p;double t;//初始化全局数组g_nums for (i=0;i<ARRAY_LEN;i++){p=(char *)(&x);for (j=0;j<8;j++)p[j]= (rand() & 0xff);//get a 64bit random numberg_nums[i]=x;}t=currTime();//计时开始g_buff[0]=0;for ( i=0;i<LOOP_COUNT;i++){p=g_buff;for (j=0;j<ARRAY_LEN;j++){fp( g_nums+j,p);p+=16;}}t=(currTime()-t)*1000000000;//转化时间到纳秒printf("It take %.2f ns to run function %s\n",t/(LOOP_COUNT*ARRAY_LEN),funcName);printf("strlen(buff) is %d\n",strlen(g_buff)); }//功能测试void test_function(){test_UINT64_2_hexString(UINT64_2_hexString_c1,  "UINT64_2_hexString_c1");test_UINT64_2_hexString(UINT64_2_hexString_c2,  "UINT64_2_hexString_c2");test_UINT64_2_hexString(UINT64_2_hexString_c3,  "UINT64_2_hexString_c3");test_UINT64_2_hexString(UINT64_2_hexString_sse2,"UINT64_2_hexString_sse2");}//性能测试void perf_function(){perf_UINT64_2_hexString(UINT64_2_hexString_c1,  "UINT64_2_hexString_c1");perf_UINT64_2_hexString(UINT64_2_hexString_c2,  "UINT64_2_hexString_c2");perf_UINT64_2_hexString(UINT64_2_hexString_c3,  "UINT64_2_hexString_c3");perf_UINT64_2_hexString(UINT64_2_hexString_sse2,"UINT64_2_hexString_sse2");}int main(int argc, char* argv[]){test_function();perf_function();return 0;}

补充:

   这里给出跨平台的计时函数currTime的代码。

#if defined(_WIN32)#include <windows.h>static LARGE_INTEGER freq;static BOOL initFreq(){BOOL ret;if ( !QueryPerformanceFrequency( &freq) ){ret=FALSE;}else{ret=TRUE;}return ret;}double currTime() //使用高精度计时器{LARGE_INTEGER performanceCount;BOOL result;double time=0.0;BOOL bRet=TRUE;if (freq.QuadPart==0){bRet=initFreq();}if (bRet){result=QueryPerformanceCounter(  &performanceCount );time= performanceCount.HighPart * 4294967296.0 + performanceCount.LowPart;time=time / (   freq.HighPart * 4294967296.0 + freq.LowPart);}return time;}#elif defined(__linux__) #include <sys/time.h>#include <stdio.h>#include <stdlib.h>double currTime(){struct timeval tv;gettimeofday(&tv, NULL);return (double)(tv.tv_sec) + (double)(tv.tv_usec)/1000000.00;}#else #error do not support this complier#endif


关于编译:本程序使用C语言和汇编语言混合编程。C语言源文件直接加到VC工程中即可。汇编语言使用VC中自带的汇编器ml.exe来编译。

方法

1. 将汇编文件加入到VC工程中。

2.选中文件,右键属性菜单,Item type选Custom Build Tool. 在Command Line一栏: 输入“ml /coff /c %(FullPath)", 在Outputs 一栏输入"%(Filename).obj;%(Outputs)"


测试结果函数c1c2c3sse2时间(纳秒)67.9552.7120.163.18相对速度100%129%335%2138%



0 0