Assembly 提高二进制文件的效率->;8086的格雷码

Assembly 提高二进制文件的效率->;8086的格雷码,assembly,hex,x86-16,micro-optimization,gray-code,Assembly,Hex,X86 16,Micro Optimization,Gray Code,我是汇编的初学者,这是我设计用来将二进制和灰色转换成十六进制的代码,并以十六进制打印生成的位模式 mov al, a mov bl, al shr bl, 1 xor al, bl 虽然程序正在运行,但我想学习其他更简单的方法来提高效率,我尝试了许多其他方法,但它影响了输出。(这个答案是根据问题的第一个版本编写的,其中有一些尚未

我是汇编的初学者,这是我设计用来将二进制和灰色转换成十六进制的代码,并以十六进制打印生成的位模式

      mov      al, a               
      mov      bl, al
      shr      bl, 1
      xor      al, bl                         
虽然程序正在运行,但我想学习其他更简单的方法来提高效率,我尝试了许多其他方法,但它影响了输出。

(这个答案是根据问题的第一个版本编写的,其中有一些尚未优化的十六进制打印代码,它是一个.exe程序的完整源代码。问题的更新删除了唯一有空间优化的部分,除了与8086无关的ILP,所以我不打算删除这些部分。)(请回答。)

代码大小优化(与8086尤其是8088上的速度相关,请参阅):

  • bin2Gray部件:没有变化,除非您计算从mem重新加载或使
    a
    恒定。只需重新排序奔腾和更高版本上的ILP指令。或者可能是
    xlat
    的表格
  • 字节->十六进制数字:21个字节,低于32个(更少的代码提取字节)
  • 退出:从4字节(mov ah/int)减少1字节(
    ret
    )。至少适用于
    .com
    可执行文件,这些文件也更小
我可能应该以需要获取的总字节数(即执行的指令字节数)来计算代码大小,而不是静态代码大小,尽管这对于优化代码也很有用

从21字节的bin2hex部分删除循环会消耗几个字节的静态代码大小,但会将动态字节数减少约5,IIRC。并且避免在执行的分支上丢弃任何预取缓冲区,当然,
int 10h
除外


int 20h
也可以退出(无需任何AH设置),但只能从
.com
可执行文件中退出。我感兴趣的部分是使用紧凑的代码计算所需寄存器中的ASCII数字,但如果您想要一个小的整体程序,则可以使用
.com
进行退出。这也可以避免设置DS。(虽然如果将
a
和eq或
=
设为常量,则不需要设置DS。)

不尝试:利用初始寄存器值,这在某些DOS版本中显然是可靠的。如果你假装正在编写一个块,作为更大程序的一部分可能有用,这是不可行的


您的程序基本上有两个独立的部分:计算一个格雷码,以及在寄存器中计算一个1字节值的bin->hex。单独提取半字节并不能有效地向后优化格雷码计算,因此我认为我们可以将它们完全分开


有(连续值之间只有一位翻转)。好吧,
x^(x>>1)
是从二进制计算的最便宜的方法,但这不是不可能的,只要在寄存器中输入2条指令,就可以完成某些事情

同样相关:对于gray->binary指出标准的
x^(x>>1)
是GF(2k)中的乘法。因此,在最近使用Galois字段指令的CPU上,我认为您可以使用一次16字节(使用固定多项式,我认为这不是我们想要的)


8088性能主要与内存访问有关(包括代码提取) 显示指令计时,但这些计时不是代码提取。8088有一个4字节的预取缓冲区(只有一个8位数据总线),在8086上有一个6字节的16位总线。Supercat在回答中建议:

在最初的8088处理器上,估计执行速度的最简单方法通常是忽略周期计数,而是对内存访问(包括指令获取)进行计数,然后乘以4

我认为8086也差不多,只是每次访问都可以是一个完整的2字节字,所以直线代码(没有分支)一次可以获取2字节

为了简单起见,我只是用表中的指令大小和周期计数注释了asm源代码,而没有尝试对预取缓冲区的行为进行建模

(如
al=ds:[bx+al]
)只有1个字节,如果你不介意有一个256字节的表,它就值得使用。它需要11个字节来执行,但这包括它所进行的数据访问。不算代码获取,
mov-bl,al
/
shr-al,1
/
xor-al,bl
是2+2+3个周期,但代码大小的3个字将需要12个周期来获取。xlat几乎占用了很长一段时间,但当它完成预取缓冲区将有一段时间来获取以后的指令,所以我认为这更像是一场胜利

尽管如此,它还是要求该表来自某个地方,或者在加载可执行文件时来自磁盘,或者您必须对其进行预计算。并且您需要将一个指针指向BX,因此只有在循环中这样做,它才可能是一个胜利

但是如果您使用的是表格,您可以将问题的两个部分结合起来,查找给定二进制文件的格雷码的ASCII十六进制字符,例如,使用
mov-dx、[bx+si]
,表格指针为si,二进制字节为BL,BH=0。(dx设置为通过DOS调用输出DL。)当然,这需要您的表为256个字(512字节)。拥有一个小的可执行文件可能比在这里保存几个周期更有价值;屏幕或文件的实际I/O速度可能足够慢,因此不会有太大影响。但是,如果您要对多个字节执行此操作,则将ASCII字节对复制到缓冲区中可能会很好

有一个优化将有助于更现代的CPU(从奔腾开始),它可以并行运行多条指令:复制寄存器,然后移动原始寄存器,这样可以在复制的同一周期内进行

; optimized for Instruction-level Parallelism
;; input: AL    output: AL = bin_to_gray(AL)
;; clobbers: DL
   mov  dl, al         ; 2B  2 cycles (not counting code-fetch bottlenecks)
   shr  al, 1          ; 2B  2c
   xor  al, dl         ; 2B  3c
(有关现代CPU的更多信息,请参阅。另外,-mov消除在字节或字寄存器上不起作用,因为这会合并到EDX的低位。因此,即使在具有mov消除功能的CPU上,通常也是如此。)
; a equ 0ACh         ; makes the following instructions 2 bytes each
;;; otherwise, with a being static storage, loading from memory twice sucks
    mov  al, a
    shr  al, 1    ; 2B, 2 cycles
    xor  al, a    ; reg,imm: 2B, 4 cycles on 8088.    reg,mem: 3B, 13+6 cycles
;; input: byte in AL.   output: print 2 ASCII hex digits with BIOS int 10h
;; clobbers: CX, DX
hexprint_byte:
     mov   ah, 0Eh                       ; BIOS teletype call #
;     push  ax            ; 1B 15c
     mov   dx, ax        ; 2B 2c         ; save number, and AH=call number
     mov   cl, 4         ; 2B 4c
     shr   al, cl        ; 2B 8+4*4 cycles   isolate the high nibble
.loop:
     cmp   al, 10        ; 2B 4c  set CF according to digit <= 9
     sbb   al, 69h       ; 2B 4c  read CF, set CF and conditionally set AF
     das                 ; 1B 4c   magic, which happens to work
     int   10h           ; 2B        BIOS teletype output (AL), no return value
;     pop   ax            ; 1B 12c    ; would do one extra pop if you used this instead of mov/xchg, so you'd need jmp ax instead of ret.  But AND destroys AX
     xchg  ax, dx        ; 1B 3c     ; retrieve the original again (with AH=0Eh call number)
     and   al, 0Fh       ; 2B 4c     ; isolate the low nibble this time

     dec   cx            ; 1B 3c     ; PF is set from the low byte only, CH garbage isn't a problem.  
     jpe   .loop         ; 2B 4c-not-taken, 16c-taken
               ;  4-1 = 3, (0b11) which has even parity
               ; so JPE is taken the first time, falls through the 2nd 

;; size = 21 bytes
     aam   16       ; 83 cycles, 2 bytes   AH= quotient = leading digit   AL = remainder = trailing digit (low)
  ; normally never use div or aam by a power of 2, only for code-size over speed.
     cmp   al, 10        ; 2B 4c  set CF according to digit <= 9
     sbb   al, 69h       ; 2B 4c  read CF, set CF and conditionally set AF
     das                 ; 1B 4c   magic, which happens to work
     xchg  dx, ax        ; 1B 3c    stash low digit in DL

     mov   al, dh        ; 2B 2c    get leading digit
     cmp   al, 10        ; 2B 4c
     sbb   al, 69h       ; 2B 4c    most significant (high) nibble as ASCII hex
     das                 ; 1B 4c
     
     mov   ah, 0Eh       ; 2B 3c BIOS teletype output (of AL), advancing cursor
     int   10h           ; 2B ?
     mov   al, dl        ; 2B 2c  ; get low digit back from DL   xchg  ax, dx breaks AH callnum
     int   10h           ; 2B
; size=23B