代码优化常识

32位代码优化常识
原作者:  Benny/29A
翻译改写:hume/冷雨飘心

[注意:这不是鹦鹉学舌的翻译,我尽量以我的理解传达原文的本意]

关于代码优化的文章实在太多了,遗憾的是大部分我都没有看,尽管他们就摆在我的床边(每当我要看的时候就忍不住打哈欠...嘿嘿).这篇文章较短所以翻了一下.

代码优化的含义:

代码优化的目标当然是体积小和速度快,但是在通常的情况下二者就象鱼和熊掌一样不能得兼,我们通常寻找的是这二者的折中,究竟应该偏向何方,那就得具体看我们的实际需要.

但有些常识是我们应该牢记的,下面就结合我们最常遇到的具体情况来漫谈一下:

1.寄存器清0
   
    我绝对不想再看到下面的写法:
        1)   
  mov eax, 00000000h               
    ;5 bytes
       
     
  看起来上面的写法很符合逻辑,但你应当意识到还有更加优化的写法:
        2) 
    sub eax, eax               
          ;2 bytes

3)      xor eax, eax             
            ;2 bytes
       
看看后面的字节数你就应该理解为什么要这么作了,除此之外,在速度上也没有损失,他们一样快,但你喜欢xor还是sub呢?我是比较喜欢xor,原因很简单,因为我数学不好....

不过Microsoft比较喜欢sub....我们知道windows运行的慢....(呵呵,当然是玩笑这并不是真正原因X-D!)

2.测试寄存器是否为0
        我也不希望看到下面的代码:
 
      1)      cmp eax, 00000000h     
              ;5 bytes
     
          je _label_         
                  ;2/6 bytes (short/near)

[* 注意很多指令针对eax作了优化,你要尽可能多地实用eax,比如CMP EAX,
12345678h (5 bytes)
        如果你使用其他寄存器,就是6bytes *]

让我们看看,简单的比较指令居然要用7/11
bytes,No No No,试试下面的写法:
        2)     
or eax, eax                   
      ;2 bytes
             
  je _label_                 
          ;2/6 (short/near)

3)      test eax, eax         
              ;2 bytes
     
          je _label_         
                  ;2/6 (short/near)

呵呵,只有4/8
bytes,看看我们可节省多少字节啊3/4字节...那么接下来的问题是你喜欢OR还是TEST呢,就我个人而言,比较喜欢TEST,因为test不改
变任何寄存器,并不向任何寄存器写入内容,这通常能在pentium机上取得更快的执行速度.

别高兴的太早,因为还有更值得我们高兴的事情,假如你要判断的的是eax寄存器,那么看看下面的,是不是更有启发?

4)      xchg eax, ecx   
                    ;1 byte

jecxz _label_ 
                      ;2
bytes
        在短跳转的情况下我们比2)和3)又节省了1字节.oh....___...

3.测试寄存器是否为0FFFFFFFFh
        一些API返回-1,因此如何测试这个值呢?看你可能又要这样:

1)      cmp eax, 0ffffffffh 
                ;5 bytes
   
            je _label_       
                    ;2/6 bytes

hey,不要这样,写代码的时候想一想,于是有了下面的写法:
   
    2)      inc eax         
                    ;1 byte

je _label_ 
                       
  ;2/6 bytes
               
dec eax                     
        ;1 byte

可以节省3
bytes并且执行速度会更快.

4.置寄存器为0FFFFFFFFh
        看看假如你是Api的作者,如何返回-1?这样吗?

1)      mov eax, 0ffffffffh 
                ;5 bytes

看了上面的不会再这么XXX了吧?看看下面的:
        2) 
    xor eax, eax / sub eax, eax          ;2
bytes
                dec eax 
                       
    ;1 byte
        节省一个字!还有写法:
 
      3)      stc         
                       
;1 byte
                sbb eax,
eax                     
    ;2 bytes
        这有时还可以优化掉1 byte:

jnc _label_
 
              sbb eax, eax     
                    ;2 bytes
only!
      _label_: ...

我们为什么用asm呢?这就是原因.

5.寄存器清0并移入低字数值
        1)     
xor eax, eax                   
      ;2 bytes
             
  mov ax, word ptr [esi+xx]            ;4 bytes

????--->不会吧,这可能是最多初学者的写法了,我当然原来也是,看了benny的文章之后我决定改写为:

2)      movzx eax, word ptr [esi+xx] 
        ;4 bytes
        收获2 bytes!

下面的
        3) 
    xor eax, eax               
          ;2 bytes
         
      mov al, byte ptr [esi+xx]         
  ;3 bytes

就相应改为:
   
    4)      movzx eax, byte ptr [esi+xx]   
      ;4 bytes

我们应当尽可能利用movzx

5)      xor eax, eax   
                      ;2
bytes
                mov ax, bx 
                       
  ;3 bytes

因为执行速度不慢并通常能节省字节...

6)      movzx eax, bx   
                    ;3 bytes

6.关于push,下面是着重代码体积的优化,因为寄存器操作总要比内存操作要快.

1)      mov eax, 50h           
              ;5 bytes

这样就小了1 word

2)   
  push 50h                 
            ;2 bytes
       
        pop eax             
                ;1 byte
   
   
        当操作数只有1字节时候,push只有2 bytes,否则就是5
bytes,记住!
        下一个问题,向堆栈中压入7个0

3)      push 0         
                      ;2
bytes
                push 0 
                       
      ;2 bytes
             
  push 0                   
            ;2 bytes
       
        push 0             
                  ;2 bytes
 
              push 0       
                       
;2 bytes
                push 0 
                       
      ;2 bytes
             
  push 0                   
            ;2 bytes

占用14字节,显然不能满意,优化一下
        4)      xor
eax, eax                   
      ;2 bytes
             
  push eax                 
            ;1 byte
       
        push eax           
                  ;1 byte
 
              push eax     
                       
;1 byte
                push eax 
                       
    ;1 byte
               
push eax                   
          ;1 byte
         
      push eax             
                ;1 byte
   
            push eax       
                      ;1
byte

可以更紧凑,但会慢一点的形式如下:

5)      push 7       
                       
;2 bytes
                pop ecx 
                       
    ;1 byte
      _label_:  push 0   
                       
    ;2 bytes
               
loop _label_                   
      ;2 bytes

可以节省7字节....

有时候你可能会从将一个值从一个内存地址转移到另外内存地址,并且要保存所有寄存器:

6)      push eax   
                       
  ;1 byte
                mov
eax, [ebp + xxxx]                 
;6 bytes
                mov [ebp
+ xxxx], eax                  ;6
bytes
                pop eax 
                       
      ;1 byte

试试push,pop

7)      push dword ptr [ebp
+ xxxx]            ;6 bytes
     
          pop dword ptr [ebp + xxxx]     
      ;6 bytes
7.乘法
   
   
    当eax已经放入被乘数,要乘28h,如何来写?
        1) 
    mov ecx, 28h               
          ;5 bytes
         
      mul ecx               
              ;2 bytes

好一点的写法如下:

2)     
push 28h                   
          ;2 bytes
         
      pop ecx               
              ;1 byte
     
          mul ecx           
                  ;2 bytes

哇这个更好::

3) 
    imul eax, eax, 28h             
      ;3 bytes

intel在新CPU中提供新的指令并不是摆设,需要你的使用.

8.字符串操作

你如何从内存取得一个字节呢?

速度快的方案:
        1) 
    mov al/ax/eax, [esi]             
    ;2/3/2 bytes
             
  inc esi                   
          ;1 byte

代码小的方案:
        2)      lodsb/w/d 
                       
  ;1 byte

我比较喜欢lod因为他小,虽然速度慢了点.

如何到达字符串尾呢?
 
    JQwerty‘s method:

9) 
    lea esi, [ebp + asciiz]           
  ;6 bytes
      s_check: lodsb       
                       
;1 byte
                test al,
al                     
    ;2 bytes
               
jne s_check                   
      ;2 bytes

Super‘s method:

10)    lea edi, [ebp + asciiz] 
            ;6 bytes
       
        xor al, al           
                ;2 bytes
   
  s_check: scasb                 
              ;1 byte
     
          jne s_check         
                ;2 byte

选择哪一个?Super的在386以下的更快,JQwerty的在486以及pentium上更快,体积一样,选择由你.

9.复杂一点的...

假设你有一个DWORD表,ebx指向表的开始,ecx是指针,你想给每个doword加1,看看如何作:

1)      pushad     
                       
  ;1 byte
                imul
ecx, ecx, 4                   
  ;3 bytes
                add
ebx, ecx                   
      ;2 bytes
             
  inc dword ptr [ebx]               
  ;2 bytes
                popad 
                       
      ;1 byte

可以优化一点,但是好像没人用:

2)      inc dword ptr [ebx+4*ecx] 
          ;3 bytes

一条指令就节省6字节,而且速度更快,更易读,但好像没有什么人用?...why?
        还可以有立即数:

3)      pushad     
                       
  ;1 byte
                imul
ecx, ecx, 4                   
  ;3 bytes
                add
ebx, ecx                   
      ;2 bytes
             
  add ebx, 1000h                 
      ;6 bytes
             
  inc dwor ptr [ebx]               
    ;2 bytes
               
popad                     
          ;1 byte

优化为:
        4)      inc dword ptr [ebx+4*ecx+1000h] 
    ;7 bytes

节省了8字节!

看一下lea指令能为我们干点什么呢?

lea eax, [12345678h]

eax的最后结果是什么呢?正确答案是12345678h.

假设 EBP = 1
           
    lea eax, [ebp + 12345678h]
        结果是123456789h....呵呵比较一下:

lea eax, [ebp + 12345678h] 
          ;6 bytes
         
      ==========================
       
        mov eax, 12345678h         
          ;5 bytes
         
      add eax, ebp             
            ;2 bytes

5) 看看:
                mov
eax, 12345678h                   
;5 bytes
                add eax,
ebp                     
    ;2 bytes
               
imul ecx, 4                   
      ;3 bytes
             
  add eax, ecx                 
        ;2 bytes

6)
用lea来进行一些计算我门将从体积上得到好处:

lea eax, [ebp+ecx*4+12345678h]        ;7 bytes

速度上一条lea指令更快!不影响标志位...记住下面的格式,在许多地方善用他们你可以节省时间和空间.

OPCODE <SIZE PTR>
[BASE + INDEX*SCALE + DISPLACEMENT]

10.下面是关于病毒重定位优化的,惧毒人士请绕行...

下面的代码你不应该陌生

1)      call gdelta
   
    gdelta: pop ebp
             
  sub ebp, offset gdelta

在以后的代码中我们这样使用delta来避免重定位问题

lea eax, [ebp + variable]

这样的指令在应用内存数据的时候是不可避免的,如果能优化一下,我门将会得到数倍收益,打开你的sice或者trw或者ollydbg等调试器,看看:

3)      lea eax, [ebp + 401000h] 
            ;6 bytes

假如是下面这样     
   
    4)      lea eax, [ebp + 10h]     
            ;3 bytes

也就是说如果ebp后面变量是1字节的话,总的指令就只有3字节       
   
    修改一下最初的格式变为:

5)   
  call gdelta
        gdelta: pop ebp

在某些情况下我们的指令就只有3字节了,可以节省3字节,嘿嘿,让我们看看:
   
    6)      lea eax, [ebp + variable - gdelta] 
  ;3 bytes

和上面的是等效的,但是我们可以节省3字节,看看CIH...

11.其他技巧:
      如果EAX小于80000000h,edx清0:
 
      --------------------------------------------------

1)      xor edx, edx     
                    ;2 bytes,
but faster

2)      cdq 
                       
        ;1 byte, but slower

我一直使用cdq,为什么不呢?体积更小...

下面这种情况一般不要使用esp和ebp,使用其他寄存器.

-----------------------------------------------------------

1)      mov eax, [ebp] 
                      ;3
bytes
        2)      mov eax, [esp] 
                      ;3
bytes

3)      mov eax, [ebx] 
                      ;2
bytes

交换寄存器中4个字节的顺序?用bswap
 
      ---------------------------------------------------------

mov eax, 12345678h 
                  ;5 bytes

bswap eax   
                       
;2 bytes

;eax
= 78563412h now

Wanna
save some bytes replacin‘ CALL ?
        ---------------------------------------

1)      call _label_ 
                       
;5 bytes
                ret 
                       
        ;1 byte

2) 
    jmp _label_               
          ;2/5 (SHORT/NEAR)

如果仅仅是优化,并且不需要传递参数,请尽量用jmp代替call

比较 reg/mem 时如何节省时间:
       
------------------------------------------

1)      cmp reg, [mem]           
            ;slower

2)      cmp [mem], reg         
              ;1 cycle faster

乘2除2如何节省时间和空间?
        ------------------------------------------------------------

1)      mov eax, 1000h
 
              mov ecx, 4     
                      ;5
bytes
                xor edx, edx 
                       
;2 bytes
                div ecx 
                       
    ;2 bytes

2)     
shr eax, 4                   
        ;3 bytes

3) 
    mov ecx, 4               
            ;5 bytes
       
        mul ecx             
                ;2 bytes

4)      shl eax, 4       
                    ;3 bytes

loop指令

------------------------

1)      dec ecx         
                    ;1 byte

jne _label_ 
                       
;2/6 bytes (SHORT/NEAR)

2)   
  loop _label_                 
        ;2 bytes

再看:

3)      je $+5     
                       
  ;2 bytes
                dec
ecx                     
        ;1 byte
           
    jne _label_               
          ;2 bytes

4)      loopXX _label_ (XX = E, NE, Z or NZ)  ;2 bytes

loop体积小,但486以上的cpu上执行速度会慢一点...

比较:
        ---------------------------------------------------------

1)      push eax   
                       
  ;1 byte
                push
ebx                     
        ;1 byte
           
    pop eax                 
            ;1 byte
       
        pop ebx             
                ;1 byte
   
 
     
        2)   
  xchg eax, ebx                 
      ;1 byte

3)   
  xchg ecx, edx                 
      ;2 bytes
        如果仅仅是想移动数值,用mov,在pentium上会有较好的执行速度:

4)      mov ecx, edx   
                      ;2
bytes

比较:
     
  --------------------------------------------

1) 未优化:
        lbl1:  mov al, 5   
                       
;2 bytes
                stosb 
                       
      ;1 byte
             
  mov eax, [ebx]                 
      ;2 bytes
             
  stosb                   
            ;1 byte
       
        ret             
                    ;1 byte

lbl2:  mov al, 6       
                    ;2 bytes

stosb   
                       
    ;1 byte
               
mov eax, [ebx]                   
    ;2 bytes
               
stosb                     
          ;1 byte
         
      ret               
                  ;1 byte
 
                       
                       
    ---------
             
                       
                ;14 bytes
 
      2) 优化了:
        lbl1:  mov
al, 5                     
      ;2 bytes
        lbl:   
stosb                     
          ;1 byte
         
      mov eax, [ebx]             
          ;2 bytes
         
      stosb               
                ;1 byte
   
            ret         
                       
;1 byte
        lbl2:  mov al, 6     
                      ;2
bytes
                jmp lbl 
                       
    ;2 bytes
               
                       
              ---------
   
                       
                       
  ;11 bytes

读取常数变量,试试在指令中直接定义:
 
    -----------------------------

...
                mov [ebp + variable],
eax            ;6 bytes
     
          ...
         
      ...
      variable dd     
12345678h                   
;4 bytes

2) 优化为:

mov eax, 12345678h     
              ;5 bytes
     
variable = dword ptr $ - 4
             
  ...
                ...

mov [ebp + variable],
eax            ;6 bytes

呵呵,好久没看到这么有趣的代码了,前提是编译的时候支持代码段的写入属性要被设置.
     
 
        最后介绍未公开指令SALC,现在的调试器都支持...什么含义呢:就是CF位置1的话就将al置为0xff

------------------------------------------------------------------

1)      jc _lbl1   
                       
  ;2 bytes
                mov
al, 0                     
      ;2 bytes
             
  jmp _end                 
            ;2 bytes
       
  _lbl: mov al, 0ffh               
          ;2 bytes
         
_end: ...

2)      SALC 
db    0d6h                 
  ;1 byte ;)
------------------------------------------------------------------>over...

代码优化常识

时间: 2024-10-06 14:43:07

代码优化常识的相关文章

C#网络程序设计(1)网络编程常识与C#常用特性

    网络程序设计能够帮我们了解联网应用的底层通信原理!     (1)网络编程常识: 1)什么是网络编程 只有主要实现进程(线程)相互通信和基本的网络应用原理性(协议)功能的程序,才能算是真正的网络编程. 2)网络编程的层次 现实中的互联网是按照"TCP/IP分层协议栈"的体系结构构建的,因此程序员必须搞清楚自己要做的是哪个层次上的编程工作. TCP/IP协议体系的实现情况: 其中,网络接口层已经被大多数计算机生产厂家集成在了主板上,也就是经常所说的网卡(NIC).windows操

代码优化(长期更新)

前言 代码优化,一个很重要的课题.可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没用,但是,吃的小虾米一多之后,鲸鱼就被喂饱了.代码优化也是一样,如果项目着眼于尽快无BUG上线,那么此时可以抓大放小,代码的细节可以不精打细磨:但是如果有足够的时间开发.维护代码,这时候就必须考虑每个可以优化的细节了,一个一个细小的优化点累积起来,对于代码的运行效率绝对是有提升的. 代码优化的目标是:

代码优化:学学Java看看Android

由于考试的原因,好长时间都没能来写博文了(什么时候出的CSDN-markdown编辑器),今天就代码优化方面来写一篇博文,主要是讲Java. 优秀代码具备的品质: 1.简练 2.可读性强 3.模块化 4.层次性 5.设计良好 花些时间设计你的程序,因为思考的代价要小于调试. 6.高效 7.清晰 清晰是优秀代码的基本. 常见的编程规范: 1.基本要求 *程序结构清晰,简单易懂,单个函数的程序行数最好不超过100行. *尽量使用标准的函数和公共函数. *不要随意的定义全局变量,尽量使用局部变量. *

跟着SEO常识和SEO手艺的泛滥

跟着SEO常识和SEO手艺的泛滥,SEO行业孔殷需要一种更有意义的交流平台或体例,SEO论坛已经快要走向恼了,像人气很火的坛现在也成了外链的海洋,还好坛推出了SEO问答,而且进行了实名制注册.SEO行业从来都离不开交流,尤其是诚信.有意义的交流,去SEO论坛和站长类论坛的人根基上都是为了发外链.这样的交流没有若干好多真正的意义,常有新人去提问题,但获得的回覆往往是“进修了,看看”之类的灌水说话.对于处所SEO行业,或者说是一个小的SEO圈子,更需要真诚的交流和分享,凭空诬捏做欠好SEO,集思广益

Linux学习笔记(七)--Linux基本常识了解

下面内容大部参阅:鸟哥的私房菜(第三版) http://vbird.dic.ksu.edu.tw/linux_basic/linux_basic.php#part2  我们下面对LINUX的基本常识做下介绍: Linux是一套开源的.性能稳定的多用户网络操作系统,支持多用户,多线程和多CPU. Linux所有的内容统称文件. 详细有: A.普通文件(regular file):就是一般存取文件,由ls -al显示出来的属性中,第一个属性为[rwx rwx rwx] B.目录文件(director

很少有人会告诉你的Android开发基本常识

原文:很少有人会告诉你的Android开发基本常识. 文章介绍了一些关于开发.测试.版本管理.工具使用等方面的知识.

简述Python中&quot;_&quot;的使用常识

0. 背景 Python里面的下划线"_"使用频率明显比其他主流语言要多的多,而且在Python中,它也有自己独到的用处.本文主要简述:下划线在python中的使用常识. 1. 单下划线-将名称封装到类中 如果想将类中的"私有"数据封装到类的实例上,但是又需要考虑到Python缺乏对属性的访问控制问题.与其依赖语言特性来封装数据,Python程序员们更期望通过特定的命名规则来表达出对数据和方法的用途. 第一个规则是任何以单下划线(_)开头的名字应该总是被认为只属于内

struts2的坑以及tomcat的一些常识

Struts2中坑 1:一个很简单的跳转,死活跳不过去,总是404,那这个时候可以把struts.xml这个配置文件用IE浏览器打开,如果能够正常打开,那么说明struts.xml文件本身没有语法错误. 我遇到的一个struts.xml中错误是<action name="toLogin" class=“com.wyl.toLoginAction” method="execute">,单靠肉眼不容易发现,实际上这个标签里的class属性的引号是中文引号,但

鑫鹏SEO告诉你网站代码优化H1、H2、H3标签的作用

网站优化不仅仅是包括:网站的标题,关键词,描述的优化:首页布局.网站导航,网站底部,头部,幻灯片,第一屏的内容布局优化,其实还包括代码优化,网站速度的优化,文章的优化,栏目.页面的优化,URL优化等等.今天鑫鹏SEO为大家讲解在代码优化里面最重要的优化是网站标签的优化,通常标签包括网站的H1.H2.H3,其实很多新手SEO并不知道这些标签有什么作用,究竟怎么设置. 一.网站代码优化H1.H2.H3标签的需要程序员写好程序的调用 说到代码的优化,肯定是少不了程序员的配合,所以在做网站的时候,在做网