找回密码
 欢迎注册
查看: 13140|回复: 7

[原创] 小代码的优化

[复制链接]
发表于 2011-11-12 19:12:40 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?欢迎注册

×
用汇编写代码不一定高效,呵呵,是的,汇编爱好者可能爱上了它的灵活性。
写代码嘛,首先是解决实现问题,如果都不能实现,不谈优化,接着再优化优化你的代码,当然,如果你说你牛,就一步到位^_^。

精华
有时候,我们可能没有很好的设计逻辑流程,举个例子,

有人在某群里发了个问题,要帮忙解决,问题如下:
求一个 int64 有效数据至少有多少 byte 长?如果是255 则有效字节长为1;若是256,则有效字节长为2 ...
最大有效字节为8,最小为0。

(假定是32位平台)面对这个问题,我想到的是bsr指令(或许有更好的思路^_^)
BSR—Bit Scan Reverse

BSR r32,r/m32
该指令的操作描述如下(可查阅Intel指令手册):
  1. IF SRC = 0
  2.    THEN
  3.         ZF ← 1;
  4.         DEST is undefined;
  5.    ELSE
  6.         ZF ← 0;
  7.         temp ← OperandSize – 1;
  8.    WHILE Bit(SRC, temp) = 0
  9.    DO
  10.         temp ← temp - 1;
  11.         DEST ← temp;
  12.    OD;
  13. FI;
复制代码
对于BSR DEST,SRC,如果SRC为0,则将flag标志的ZF置1,DEST的值不确定。
否则,将ZF置0,再从SRC的高位开始扫描Bit位,当找到Bit 1则退出,将
此Bit 1的位置赋给DEST (范围:0~31)。
例如:
  1. mov eax,4       ;00000000 00000000 00000000 00000100
  2. bsr edx,eax     ;edx=29  ZF=0

  3. xor eax,eax
  4. bsr edx,eax     ;edx=?   ZF=0
复制代码
;===================================================================
嗯,讲了这么多没用的,呵呵。开始上面的那个解决问题^_^
代码一:
  1.             bsr     eax,dword ptr [buf+4]  ;0~31
  2.             jz      A00
  3.             add     eax,32+8
  4.             jmp     A02
  5.         A00:
  6.             bsr     eax,dword ptr [buf]    ;0~31
  7.             jz      A01
  8.             add     eax,8
  9.             jmp     A02
  10.         A01:
  11.             xor     eax,eax
  12.         A02:
  13.             shr     eax,3
复制代码
以上代码,先找高32位,如果不为0,则说明有效字节数>=4,可直接跳到A02.
否则,高位均为0,开始考虑低32位,如果也均为0,则跳转到A01,清0结束。
呵呵,上面的代码跳的有点晕,改进一下:
代码二:
  1.             bsr     eax,dword ptr [buf+4]   ;0~31
  2.             jz      A00
  3.             add     eax,32
  4.             jmp     A01
  5.         A00:
  6.             bsr     eax,dword ptr [buf]     ;0~31
  7.             jnz     A01
  8.             mov     eax,-8
  9.         A01:
  10.             add     eax,8
  11.             shr     eax,3
复制代码
呵呵,同样的功能,却省下了一个jmp A02 ,当均为0时,将执行mov eax,-8,
这样与下面的add eax,8相消,达到清0效果。
再改进一下:
代码三:
  1.             bsr     eax, dword ptr [buf+4] ;0~31
  2.             jnz     A00
  3.             bsr     eax, dword ptr [buf]   ;0~31
  4.             jnz     A01
  5.             mov     eax,-40
  6.         A00:
  7.             add     eax,32
  8.         A01:
  9.             add     eax,8
  10.             shr     eax,3
复制代码
经过一系列的逻辑流程调整,是不是清爽了许多咧。
毋因群疑而阻独见  毋任己意而废人言
毋私小惠而伤大体  毋借公论以快私情
 楼主| 发表于 2011-11-12 19:55:39 | 显示全部楼层
2. FNV hash 有名的hash函数。最近需要对以0结尾的字符串进行hash,找了下,觉得FNV合适。
参考:http://isthe.com/chongo/tech/comp/fnv/
FNV-1a片段如下:
  1. typedef unsigned int Fnv32_t;
  2. Fnv32_t __stdcall fnv_32a_str(char *str)
  3. {
  4.     unsigned char *s = (unsigned char *)str;
  5.     Fnv32_t hval= 2166136261;  
  6.     while (*s) {
  7.         hval ^= (Fnv32_t)*s++;
  8.         hval *= 0x01000193;
  9.     }
  10.     return hval;
  11. }
复制代码
考虑到while循环没有do while循环的良好流程形式,所以修改如下(这样将结尾的0也hash,没什么不当^_^ ,从上面提供的网址可以查看
到,他们提供的asm代码如fast_fnv32a,均如此)
  1. typedef unsigned int Fnv32_t;
  2. Fnv32_t __stdcall fnv_32a_str_c(char *str)
  3. {
  4.     unsigned char *s = (unsigned char *)str;
  5.     Fnv32_t hval= 2166136261;   
  6.     do {
  7.         hval ^= (Fnv32_t)*s;
  8.         hval *= 16777619;
  9.     }while(*s++);
  10.     return hval;
  11. }
复制代码
首先我参考fast_fnv32a写了一个:
代码一:
  1.            push    esi
  2.            push    edi
  3.            mov     esi,[esp + 12]      ;str
  4.            mov     eax,2166136261      ;FNV1_32A_INIT
  5.            mov     edi,16777619        ;fnv_32_prime
  6.         ;---------------------------
  7.         A00:
  8.            movzx   ecx,byte ptr [esi]
  9.            xor     eax,ecx
  10.            mul     edi
  11.            inc     esi
  12.            test    ecx,ecx
  13.            jnz     A00
  14.         ;---------------------------
  15.            pop     edi
  16.            pop     esi
  17.            retn    4
复制代码
以上代码实现了上面的c功能,然后vc下测试,启用/O2,发现不如fnv_32a_str_c的c代码高效。
查看c的反汇编如下:
代码二:
  1.    mov     edx,[esp + 4]       ;str
  2.    push    esi
  3.    
  4.    mov     eax,2166136261      ;FNV1_32A_INIT
  5. ;---------------------------
  6. A00:
  7.    mov     cl,byte ptr [edx]
  8.    mov     esi,ecx
  9.    and     esi,255
  10.    xor     esi,eax
  11.    
  12.    imul    esi,16777619       ;fnv_32_prime
  13.    inc     edx
  14.    mov     eax,esi
  15.    test    cl,cl
  16.    jnz     A00
  17. ;---------------------------
  18.    pop     esi
  19.    retn    4
复制代码
发现其巧用imul而省下了寄存器edx,少了一对push pop,你可能想到了用movzx,又可省下不少。
   mov     cl ,byte ptr [edx]
   mov     esi,ecx
   and     esi,255      ;占6字节
替换为:
   movzx   ecx,byte ptr [edx]
   mov     esi,ecx
一下省了5字节,效率上mov与movzx相当。
细心一点又会发现esi不需要。
代码三:
  1.    mov     edx,[esp + 4]       ;str
  2.    mov     eax,2166136261      ;FNV1_32A_INIT
  3. ;---------------------------
  4. A00:
  5.    movzx   ecx,byte ptr [edx]
  6.    xor     eax,ecx
  7.    imul    eax,16777619        ;fnv_32_prime
  8.    inc     edx
  9.    test    ecx,ecx
  10.    jnz     A00
  11. ;---------------------------
  12.    retn    4
复制代码
呵呵,减小到28字节,效率也有提升。
接着,我将inc edx调整一下顺序,在我的赛扬处理器上获得了更佳的性能^_^。
代码四:
  1.    mov     edx,[esp + 4]       ;str
  2.    mov     eax,2166136261      ;FNV1_32A_INIT
  3. ;---------------------------
  4. A00:
  5.    movzx   ecx,byte ptr [edx]
  6.    inc     edx
  7.    xor     eax,ecx
  8.    imul    eax,16777619        ;fnv_32_prime
  9.    test    ecx,ecx
  10.    jnz     A00
  11. ;---------------------------
  12.    retn    4
复制代码
毋因群疑而阻独见  毋任己意而废人言
毋私小惠而伤大体  毋借公论以快私情
 楼主| 发表于 2011-11-13 10:37:24 | 显示全部楼层
本帖最后由 G-Spider 于 2011-11-13 10:38 编辑

对于上面的代码四,由于包含imul 乘法运算,难免有点。于是想到用逻辑运算代替乘法。
fnv_32_prime这个数有点特点:16777619=0x01000193
hval =hval * (0x01000000  +256+128+16+2+1)
hval + = (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24);
首先我用Intel c++优化编译,得到如下结果,性能优于代码四:
代码五:
  1.     mov     eax,2166136261      ;FNV1_32A_INIT
  2.     mov     edx,[esp + 4]       ;str
  3.     push    ebx
  4.     push    esi
  5.     push    edi
  6. ;---------------------------   
  7. A00:
  8.     movzx   ecx, BYTE ptr [edx]
  9.     xor     eax, ecx
  10.     mov     edi, eax
  11.     mov     esi, eax
  12.     shl     edi, 4
  13.     inc     edx            
  14.     shl     esi, 7                    
  15.     add     edi, esi                       
  16.     mov     esi, eax               
  17.     shl     esi, 8                     
  18.     lea     ebx, DWORD ptr [eax+eax*2]         
  19.     shl     eax, 24                     
  20.     add     ebx, edi                     
  21.     add     esi, eax                     
  22.     test    ecx, ecx                 
  23.     lea     eax, DWORD ptr [ebx+esi]   
  24.     jne     A00
  25. ;---------------------------   
  26.     pop     edi
  27.     pop     esi
  28.     pop     ebx
  29. ;---------------------------   
  30.     retn    4
复制代码
个人自己写了个版本,性能优于代码五,且比代码五的总字节数更小:
代码六:
  1.     mov   eax,2166136261      //;FNV1_32A_INIT
  2.     mov   edx,[esp + 4]       //;str
  3.     push  esi
  4.     push  edi
  5. //;---------------------------   
  6. //;hval ^= (Fnv32_t)*s;
  7. //;hval *= 0x01000193;
  8. //;hval =hval * (0x01000000  +256+128+16+2+1)
  9. //;hval + = (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24);
  10. A00:
  11.     movzx ecx,byte ptr [edx]
  12.     xor   eax,ecx               //;hval ^= (Fnv32_t)*s;
  13.     lea   esi,[eax+eax*2]       //;(hval)+(hval<<1)
  14.     shl   eax,4                 //;(hval<<4)
  15.     inc   edx
  16.     lea   edi,[eax+eax*8]       //;(hval<<4) + (hval<<7)
  17.     shl   eax,4
  18.     add   esi,eax               //;(hval)+(hval<<1) +(hval<<8)
  19.     shl   eax,16                //;(hval<<24)
  20.     add   edi,eax               //;(hval<<24) +(hval<<4) + (hval<<7)
  21.     test  ecx,ecx
  22.     lea   eax,[esi+edi]
  23.     jnz   A00
  24. ;---------------------------   
  25.     pop     edi
  26.     pop     esi
  27. ;---------------------------   
  28.     retn    4
复制代码
在我的赛扬机上的测试比较:
1.PNG
毋因群疑而阻独见  毋任己意而废人言
毋私小惠而伤大体  毋借公论以快私情
 楼主| 发表于 2011-11-13 10:53:03 | 显示全部楼层
基于楼上的思路,也可以对另一款不错的串hash进行优化:
BKDR_C
  1. unsigned int __stdcall BKDRHash0(unsigned char *str)
  2. {
  3.     unsigned int seed = 131; // 31 131 1313 13131 131313 etc..
  4.     unsigned int hash = 0;
  5.     do{
  6.         hash = hash * seed + (*str);
  7.     }while(*str++);
  8.     return hash;
  9. }
复制代码
启用优化后,vc会对131分解为128+4+1
BKDR_asm0 使用乘法指令:
  1. __declspec(naked)
  2. unsigned int __stdcall BKDRHash1(unsigned char *str)
  3. {
  4. __asm{
  5.         xor   eax, eax
  6.         mov   edx, [4+esp]
  7. A00:
  8.         imul  eax,131
  9.         movzx ecx, BYTE ptr [edx]
  10.         inc   edx
  11.         add   eax, ecx
  12.         test  ecx, ecx
  13.         jne   A00

  14.         retn       4
  15.       }
  16. }
复制代码
BKDR_asm1 对分解的优化:
  1. __declspec(naked)
  2. unsigned int __stdcall BKDRHash2(unsigned char *str)
  3. {
  4. __asm{
  5.         xor   eax,eax
  6.         mov   edx,[esp+4]
  7.     A00:  
  8.         mov   ecx,eax
  9.         shl   ecx,6
  10.         add   ecx,eax
  11.         inc   edx
  12.         lea   eax,[eax+ecx*2]
  13.         movzx ecx,byte ptr [edx]
  14.         add   eax,ecx
  15.         test  ecx,ecx
  16.         jnz   A00
  17. ;---------------------------
  18.         retn  4
  19.       }
  20. }
复制代码
在我的赛扬机下的测试结果:
2.PNG
毋因群疑而阻独见  毋任己意而废人言
毋私小惠而伤大体  毋借公论以快私情
发表于 2011-11-14 10:26:50 | 显示全部楼层
mark, 有时间好好学习一下
毋因群疑而阻独见  毋任己意而废人言
毋私小惠而伤大体  毋借公论以快私情
发表于 2011-11-14 10:56:22 | 显示全部楼层
膜拜一下
毋因群疑而阻独见  毋任己意而废人言
毋私小惠而伤大体  毋借公论以快私情
 楼主| 发表于 2011-11-14 14:28:38 | 显示全部楼层
接四楼,有点小问题修正一下,131=128+2+1   , BKDR_asm1中的inc edx 应该放在 movzx之后,这样三款hash值一致,当然不影响执行效率。
是不是很不错了呢?
对imul 与乘进行逻辑分解有个权衡,这与cpu有关。之前在赛扬上测试,imul 不如分解之后的快,今个在Intel Core i5上测试,BKDR_asm0 胜过BKDR_asm1.
可见imul 的执行效率已经大大改善。
可以设想,如果通过分解后与imul的执行效率相当,那么在稍差的机器上,分解版将完胜imul版,^_^。
出于这个想法,有必要再对BKDR_asm1做进一步优化,因为BKDR_asm1在i5上输给了BKDR_asm0.

还有可能优化吗?能否再减少A00循环体的指令数呢?
BKDRHash3:
  1. __declspec(naked)
  2. unsigned int __stdcall BKDRHash3(unsigned char *str)

  3. { //131=128+2+1
  4. //hash = hash * (128+2+1) + (*str);   
  5. __asm{
  6.         xor   eax,eax
  7.         mov   edx,[esp+4]
  8.     A00:  
  9.         lea   ecx,[eax+eax*2]
  10.         shl   eax,7
  11.         add   eax,ecx
  12.         movzx ecx,byte ptr [edx]
  13.         inc   edx   
  14.         add   eax,ecx
  15.         test  ecx,ecx
  16.         jnz   A00
  17.     ;---------------------------
  18.         retn  4
  19.       }
  20. }
复制代码
小结:
整个BKDR hash函数的优化过程,为了减少push pop 将movzx 置后,使得ecx 可以在movzx之前使用^_^,通过分解将乘转化为简单的逻辑运算,使用强大的指令lea 完成加乘赋值一体化。
好的组合分解又能减少不必要的操作^_^。
这次在Intel Core i5 CPU上测试,发现imul 与分解后的执行效率相当,这说明在赛扬必完胜imul ,见下图的BKDRHash1(imul版)和BDKRHash4(分解优化版):
11.jpg
测试代码:
  1. #include <stdio.h>
  2. #include <time.h>
  3. #define N0 10000000

  4. //string test
  5. unsigned char buf[]="InitializeCriticalSectionAndSpinCount";

  6. //=====================================================
  7. unsigned int __stdcall BKDRHash0(unsigned char *str)
  8. {
  9.     unsigned int seed = 131; // 31 131 1313 13131 131313 etc..
  10.     unsigned int hash = 0;
  11.     do{
  12.         hash = hash * seed + (*str);
  13.     }while(*str++);
  14.     return hash;
  15. }
  16. //=====================================================
  17. __declspec(naked)
  18. unsigned int __stdcall BKDRHash1(unsigned char *str)

  19. {
  20. __asm{
  21.         xor   eax, eax
  22.         mov   edx, [4+esp]
  23. A00:
  24.         imul  eax, 131
  25.         movzx ecx, BYTE ptr [edx]
  26.         inc   edx
  27.         add   eax, ecx
  28.         test  ecx, ecx
  29.         jne   A00
  30.         retn  4
  31.       }
  32. }
  33. //=====================================================
  34. __declspec(naked)
  35. unsigned int __stdcall BKDRHash2(unsigned char *str)
  36. {//131=128+2+1
  37. //hash =(hash * 64+hash)*2+hash + (*str);
  38. __asm{
  39.         xor   eax,eax
  40.         mov   edx,[esp+4]
  41.     A00:  
  42.         mov   ecx,eax
  43.         shl   ecx,6
  44.         add   ecx,eax
  45.         lea   eax,[eax+ecx*2]
  46.         movzx ecx,byte ptr [edx]
  47.         add   eax,ecx
  48.         inc   edx
  49.         test  ecx,ecx
  50.    
  51.         jnz   A00
  52. ;---------------------------
  53.         retn  4
  54.       }
  55. }

  56. //=====================================================
  57. __declspec(naked)
  58. unsigned int __stdcall BKDRHash3(unsigned char *str)

  59. { //131=128+2+1
  60. //hash = hash * (128+2+1) + (*str);   
  61. __asm{
  62.         xor   eax,eax
  63.         mov   edx,[esp+4]
  64.     A00:  
  65.         lea   ecx,[eax+eax*2]
  66.         shl   eax,7
  67.         add   eax,ecx
  68.         movzx ecx,byte ptr [edx]
  69.         inc   edx   
  70.         add   eax,ecx
  71.         test  ecx,ecx
  72.         jnz   A00
  73.     ;---------------------------
  74.         retn  4
  75.       }
  76. }

  77. //=====================================================
  78. int main()
  79. {
  80.     double t0,t1,t2,t3;
  81.     int    i,j;
  82.     printf("Hash   Value:\n");
  83.     printf("%x         "  ,BKDRHash0(buf));
  84.     printf("%x         "  ,BKDRHash1(buf));
  85.     printf("%x         "  ,BKDRHash2(buf));
  86.     printf("%x         "  ,BKDRHash3(buf));
  87.    

  88.     printf("\nElapsed time: \nBKDRHash0        BKDRHash1        BKDRHash2\
  89.         BKDRHash4(seconds)\n");

  90.     for(j=0;j<20;j++)
  91.     {
  92.             //=======================================================
  93.             t0=clock();
  94.    
  95.             for(i=0;i<=N0;i++)
  96.             {
  97.                 BKDRHash0(buf);

  98.             }
  99.             printf("%.8f       ",(clock()-t0)/CLOCKS_PER_SEC);
  100.             //=======================================================

  101.             t1=clock();

  102.             for(i=0;i<=N0;i++)
  103.             {
  104.                 BKDRHash1(buf);

  105.             }
  106.             printf("%.8f       ",(clock()-t1)/CLOCKS_PER_SEC);
  107.             //=======================================================
  108.             t2=clock();

  109.             for(i=0;i<=N0;i++)
  110.             {
  111.                 BKDRHash2(buf);
  112.             }

  113.             printf("%.8f       ",(clock()-t2)/CLOCKS_PER_SEC);
  114.             //=======================================================
  115.             t3=clock();

  116.             for(i=0;i<=N0;i++)
  117.             {
  118.                 BKDRHash3(buf);
  119.             }
  120.             printf("%.8f\n",(clock()-t3)/CLOCKS_PER_SEC);
  121.             //=======================================================
  122.     }
  123.     system("PAUSE");
  124.     return 0;

  125. }
复制代码
毋因群疑而阻独见  毋任己意而废人言
毋私小惠而伤大体  毋借公论以快私情
 楼主| 发表于 2011-11-14 14:38:44 | 显示全部楼层
5# liangbch
相互交流,共同进步^_^。

6# wayne
不敢当,我等荣幸。
毋因群疑而阻独见  毋任己意而废人言
毋私小惠而伤大体  毋借公论以快私情
您需要登录后才可以回帖 登录 | 欢迎注册

本版积分规则

小黑屋|手机版|数学研发网 ( 苏ICP备07505100号 )

GMT+8, 2024-5-2 05:54 , Processed in 0.049794 second(s), 19 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表