Jak zmusić 16-bitowy kompilator tp do 32-bitowych operacji

ŁF

Czyli jak zmusić 16-bitowy kompilator tp do 32-bitowych operacji.

Pozornie rzecz niemożliwa, a jednak...

Część tematu jest już zaprezentowana w artykule pt.: efektywne programowanie w turbo pascalu,
więc się będę nieco powtarzać. Ponadto zakładam, że znasz podstawy
asemblera, bo to dzięki niemu będziemy czarować w 32 bitach. (Będziemy
używać tylko asemblera wbudowanego w kompilator, więc jeśli używasz
asemblera zewnętrznego, to nie ma sensu żebyś czytał dalej ten artykuł.

Co nam daje działanie na 32 bitach? Przede wszystkim operacje takie są
szybsze w porównaniu do ich 16-bitowych odpowiedników, ponadto wyniki
naszych działań nie są ograniczone rozmiarem 16-bitowego rejestru a
rozmiarem rejestru 32-bitowego, czyli że bez nadmiernego kombinowania
możemy sobie działać na wielokrotnie większych liczbach - przy
zachowaniu tej samej, a nawet wyższej szybkości.

Same plusy :)

Ale jest i minus: jak? Jak zmusić kompilator tp do przełknięcia czegoś, do
czego nie był przewidziany? Popatrzmy na wbudowany asembler: nie zna
takich rejestrów, jak EAX,EBX itp, nie zna FS ani GS, w ogóle zero
zrozumienia dla naszych 32-bitowych aspiracji.

Ale zna on coś takiego, jak DB, DW i DD. I to nas urządza. :)

Jak się buduje 32-bitową instrukcję? Jeśli by za pośrednictwem debuggera
(np.: Turbo Debugger) pownikać w kod jakiegoś 32-bitowego programu, to
zobaczymy śmiesznie prostą prawidłowość: każda taka operacja jest
poprzedzona bajtem o wartości 66h (102d).

Tak! To jest naprawdę aż takie proste! Więc zaczynamy rzeźbienie...

Na pierwszy ogień najczęściej używana operacja - zerowanie rejestru.

asm
  mov ax,0              {można tak}

  xor ax,ax             {można też tak; tak jest lepiej, bo operacje
                         wykonywane na samych rejestrach, bez odwołania
                         do pamięci, są szybsze}

  db $66; mov ax,0      {tu już mamy 32 bity: mov eax,0}

  db $66; xor ax,ax     {i druga wersja 32-bitowa: xor eax,eax
                         ta wersja jest najlepsza i najszybsza}

end;

W ten sam sposób możemy sobie dzielić, mnożyć, dodawać, odejmować,
przesuwać bity, wrzucać na stos i z niego zdejmować - teoretycznie
cokolwiek!

W praktyce pojawia się kilka problemów, ale o tym za chwilę.

Może kilka przykładów:

asm
  {szybkie c=a*b, gdzie a i b są typu word, wynik
  jest też typu word, czyli w zasadzie nic nowego}
  mov ax,a
  mov bx,b
  db 66; mul bx
  mov c,ax
end;

Teraz mnożenie wordword z wynikiem dword (tzn. longint). Strach
patrzeć w debuggerze na to, co robi TP z c := longint(a)
longint(b)...

asm
  db $66; xor ax,ax;           {na wszelki wypadek eax = 0}
  mov ax,a                     {ax = a}
  mov bx,b                     {bx = b}
  db $66;  mul bx              {eax = eax * ebx}
  db $66; mov word ptr c,ax   {c = eax}
end;

Powyższa instrukcja jest wykonywana o ponad 60% szybciej.

A może by tak zaryzykować 64 bity?... Mnożenie dworddword z wynikiem
qword (2
longint)!!! Wynik zrzucany do dwóch zmiennych - poniżej
$FFFFFFFF do c, a to co wyżej - to do d.

asm
  db $66; mov ax, word ptr a
  db $66; mov bx, word ptr b
  db $66; mul bx
  db $66; mov word ptr c,ax
  db $66; mov word ptr d,dx
end;

W analogiczny sposób można dzielić dword przez dword, otrzymując w
wyniku też dwa dwordy (tzn. c = a div b; d = a mod b):

asm
  db $66; mov ax, word ptr a
  db $66; mov bx, word ptr b
  db $66; div bx
  db $66; mov word ptr c,ax
  db $66; mov word ptr d,dx
end;

Jak szybko zwiększyć zmienną typu longint?

asm {tak TP interpretuje inc(a): dwie operacje dodawania}
  add    word ptr a,0001
  adc    word ptr a+2,0000
end;

asm {tak my zrobimy: jedna operacja inkrementacji}
  db $66;  inc word ptr a
end;

Nie trzeba dodawać, że inc jest dużo szybsze od add?

A dodawanie? Proszę bardzo:

asm {dodawanie stałych: niech stała będzie równa np. $0F0F1010}
  db $66;  add word ptr a,$1010; DW $0F0F;
end;

{przy dodawaniu stałej, której dolna część będzie mniejsza od $100, pojawi
się problem, bo kompilator się nabierze, że to bajt; wtedy trzeba mocno czarować:}

asm
  db $66; mov ax,$0010; DW $000F;
  db $66; add word ptr a,ax
end; { co i tak generuje szybszy kod niż inc(a,$000F0010)}

Jeśli porównasz któryś z powyższych kodów z jego odpowiednikiem
generowanym w wyniku zwykłego mnożenia, dzielenia itp., to zobaczysz
jak wiele zyskujesz.

Możemy też dopalić najbardziej czasochłonną procedurę w TP: move (nota
bene - ona jest 8-bitowa!!!). Napiszemy własną wersję, która będzie
prawie cztery razy szybsza.

A w ogóle jak działa ta procedura? Szybki rzut oka:

procedure move(var src;var dest;count : word); assembler;
asm
  mov cx,count
  push ds
  lds si,src
  les di,dest

  cld
  rep movsb {!!!}

  pop ds
end;

Spójrz na linijkę z trzema wykrzyknikami: rep movsb. movsb to operacja
przenoszenia bajtu ze stosu ds:si na stos es:di. Przenosimy po bajcie!
(Jeśli nie wierzysz, to odpal program, który używa tej procedury, pod
debuggerem i sam sprawdź.)

Przecież to jest bez sensu, bo kompilator bez żadnego rzeźbienia pozwala
na napisanie procedury, która będzie przenosić po słowie, czyli w tym
samym czasie skopiuje dwa razy więcej!

Ale po co się ograniczać do słowa? Poniższa procedura będzie zrzucać od
razu cztery bajty, osiągając graniczną prędkość działania równą prędkości
zegara pamięci...

procedure move32(var src;var dest;count : word); assembler;
asm
  mov ax,count
  db $66; xor dx,dx   {xor ecx,ecx}
  mov bx,4
  div bx              {ax = count div 4; dx = count mod 4}

  push ds
  mov cx,ax
  lds si,src
  les di,dest

  cld
  db $66; rep movsw      {rep movsd}

  cmp dx,0
  jz @@1
  db $66; mov cx,dx
  rep movsb

@@1:
  pop ds
end;

Chyba już łąpiesz, o co chodzi? Wtykasz db $66; wszędzie, gdzie się da, i
już możesz być szczęśliwy. Uważaj na jedno: operacje na pamięci -
zrzucanie i pobieranie ze stosu; pamiętaj, że operujesz naraz czterema
bajtami, a nie dwoma czy jednym. Jeśli nie jesteś pewien, czy poprawnie
zrobiłeś to, co chciałeś, a masz do dyspozycji Turbo Debuggera albo jakiś
inny debugger, to nie wahaj się i sprawdzaj.

Jeżeli masz jeszcze jakieś pytania lub chcesz coś dodać - pisz do mnie.

3 komentarzy

Ta? To operuj sobie XMSem na 2MB buforze. Kopiowanie do pamięci konwencjonalnej, analiza, znowu kopiowanie.... nie wydaje się wam to.... wolne?

Więcej pamięci? Jeżeli ponad 1Mb, należy zastosować (dla MS-DOS) EMS, XMS, DOS extender, lub napisać własny OS.

Jeszce tylko pytanko - jak to zastosować do np. katr graficznych? czy to oznacza ze moge przypożadkować więcej np. pamięci?