Szybkość wykonania pętli: wstawki ASM vs Pascal

0

Witam,
wgłębiając się w Pascala postanowiłem popróbować ze wstawkami ASM. Z czystej ciekawości napisałem 3 funkcje:

function GetTime:Integer;
var
  SystemTime: TSystemTime;
begin
  GetLocalTime(SystemTime);
  with SystemTime do
    Result :=(wSecond*1000)+ wMilliseconds;
end;

function AsmTime(Repeats:Integer):Integer; assembler
var
  From:Integer;
asm
  PUSH  EBX
  MOV   EBX,0
  PUSH  EAX
  CALL  GetTime
  MOV   From,EAX
  POP   EAX
  @JUMP:
    CMP   EAX,EBX
    JZ    @END
    INC   EBX
    JMP   @JUMP
  @END:
  CALL  GetTime
  SUB   EAX,From
  POP   EBX
end;

function PascalTime(Repeats:Integer):Integer;
var
  Now,I:Integer;
begin
  Now:=GetTime;
  I:=0;
  repeat
    Result:=11;
    Inc(I);
  until(I=Repeats);
  Result:=GetTime-Now;
end;

Mają one zwrócić ile czasu potrzebowały na wykonanie kodu. Spodziewałem się, że funkcja AsmTime będzie o wiele szybsza, a przynajmniej szybsza, a tu zaskoczenie. Potrzebuje ona średnio 2x więcej czasu na wykonanie kodu, niż funkcja PascalTime. Fakt, assembler w moim wypadku to początki, początków.
Dla przykładu na mojej maszynie:

const
  Too=999999999;

begin
 WriteLn('ASM:'#9#9,AsmTime(Too),' ms');
 WriteLn('Pascal:'#9#9,PascalTime(Too),' ms');

Wynosi:
ASM: 1061 ms
Pascal: 504 ms

Skąd ten zaskakujący wynik?

0

Zatrzymaj debugiera na wierszu:
Now:=GetTime;
i przełącz na widok CPU.
Zobaczysz jak to przełożone na assembler przez kompilator.
Założę się że Repeats jest ladowano do ECX na początku pętli a potem uzyte polecenie loop

0

Obstawiam, że wmieszał się optymalizator. Skoro robisz n razy Result := 11, to znaczy że można to zrobić raz, i tak też robi kod po optymalizacji. Robiłem podobne próby z przypisywaniem stringów do konkretnej zmiennej - optymalizator wykonywał ostatnie podstawienie i mówił "dziękuję" :) Dopiero gdy zaczniesz wykorzystywać gdzieś te rezultaty - test będzie bardziej miarodajny.

0

Użyj do pomiaru czasu funkcji

GetTickCount

Zegar systemowy nie nadaje się do takich zastosowań.

ps
Ja uzyskałem wyniki
ASM: 1547 ms
Pascal: 3438 ms

0

@_13th_Dragon - no więc nie do końca udało ci się trafić. Oto wycinek:
Bez tytułu.png

@zaraz_zaraz - rzeczywiście nie widać nic w CPU Window na temat przypisywania tego w pętli. Jutro się za to wezmę :)

@pelsta - pobawię się jutro i użyje owego TickCount'a. Btw, a dokonywałeś jakiś modyfikacji przed testem?

0
 Result:=11;

Nie widzę takiej linijki w asemblerze. Funkcje nie są więc równoważne, wynik jest nieważny niezależnie od jego wartości.

0

Zrób w pętli proste sumowanie - powinno nie dać się zoptymalizować do sumy ciągu arytmetycznego :)

0
zaraz_zaraz napisał(a):

Zrób w pętli proste sumowanie - powinno nie dać się zoptymalizować do sumy ciągu arytmetycznego :)

Zaraz zaraz... tylko nie zapomnij o wyświetleniu wyniku. Jeśli nigdzie go nie wykorzystujesz - to on też ulegnie "zoptymalizowaniu". Zresztą widać to pod debuggerem, nawet bez wchodzenia do okna CPU - nie da się ustawić na "zoptymalizowanych" liniach breakpointa.

0

No więc te całe:

Result:=11;

Dopisałem po pierwszych testach, chcąc obciążyć jakoś tę pętle, jednak widać, że dzięki optymalizacji nie wyszło mi to :)
Nieco poprawiłem, myślę, że bardziej się nie da, a jeśli się da to proszę o podpowiedź :)

function GetTime:Integer;
begin
  Result:=GetTickCount;
end;

function AsmTime(Repeats:Integer):Integer; assembler
var
  From:Integer;
asm
  PUSH    EBX
  MOV     EBX,0
  PUSH    EAX
  CALL    GetTime
  MOV     From,EAX
  POP     EAX
  @JUMP:
    INC   EBX
    CMP   EAX,EBX
    JNZ   @JUMP
  CALL    GetTime
  SUB     EAX,From
  POP     EBX
end;

function PascalTime(Repeats:Integer):Integer;
var
  Now,I:Integer;
begin
  Now:=GetTime;
  I:=0;
  repeat
    Inc(I);
  until(I=Repeats);
  Result:=GetTime-Now;
end;

Btw,
Kiedy tak naprawdę warto używać wstawek? Nieco rozwinąłem ten program, wykonał 20 testów, obliczył średnią czasu obu funkcji, każdorazowo pętla była powtarzana 2.147.483.647 x 20 = 42.949.672.940 - tyle razy każda z funkcji powtórzyła operacje, a różnica między średnimi wynosiła około 6ms (oczywiście po zoptymalizowaniu), a więc niewiele.

0

Kiedy tak naprawdę warto używać wstawek?
Kiedy ma się ku temu naprawdę dobry powód ;). Kiedy język programowania nie oferuje dostępu do niektórych rzeczy, np. do segmentu FS. Są również pewne optymalizacje, z którymi kompilatory jednak wciąż nie radzą sobie najlepiej albo to ty możesz zrobić to lepiej (prefetch, czasem obliczenia równoległe). Na innych platformach jak najwięcej tych rzeczy kompilatory starają się wspierać poprzez specyfikatory (np. noalias, restrict, które podpowiadają kompilatorowi w jaki sposób będziesz odnosić się do pamięci) czy tzw. intrinsics, czyli specjalne funkcje, które są najczęściej odwzorowaniem jednej, konkretnej instrukcji procesora (np. cpuid czy wspomniany wcześniej prefetch). Niektóre kompilatory mają ich mniej, niektóre więcej (np. Visual C++ ma ich dość sporo: http://msdn.microsoft.com/en-us/library/hh977023.aspx).

0

Żeby wstawki asemblerowe miały sens, musisz mieć kod, który:

  • jest za trudny do automatycznej optymalizacji przez kompilator,
  • zabiera dużo czasu procesora w jakichś okolicznościach - szacunki są różne, ale przykładowo powiedzmy, że 90% czasu procesora jest spędzone w 10% kodu, a reszta kodu zabiera resztę czasu procesora. Tak więc trzeba zlokalizować te 10% najczęściej wykonywanego kodu i go optymalizować.
0

Lekko offtop:

Jak na razie żaden kompilator nie zrobi optymalizacji lepiej niż człowiek

Czytałem gdzieś wspomnienia programisty "siedzącego w komputerach" od czasów manualnego programowania w ASM. Też tak długi czas twierdził, ale gdzieś w połowie lat 90 musiał uznać wyższość jakiegoś kompilatora C :)

0
  • chcesz się pobawić w jakieś SSE1234 o których kompilator jeszcze 10 lat nie będzie słyszał.

W FPC można ustawić SSE4.2... Szansa że twoi użyszkodnicy będą mieli SSE5 jest baardzo niska.

Kiedy tak naprawdę warto używać wstawek?

Ja używam wstawek w następujących przypadkach:

  • Chcę się odwołać do jakieś instrukcji procesora w stylu CPUID albo RDTSC.
  • Chcę zagmatwać kod albo go po cichu wywalić
  • Wszelkiego rodzaju antidebug również najlepiej pisać w asmie.
  • No i gdy pisze się loadery (patrz: mój szyfrator relokacji) to też cały ich kod jest w asmie.

Do optymalizacji nie używam asma, bo wiem że i tak nie wyciągnę z tego dużo, lepiej napisać lepszy algorytm w Pascalu.

Czytałem gdzieś wspomnienia programisty "siedzącego w komputerach" od czasów manualnego programowania w ASM. Też tak długi czas twierdził, ale gdzieś w połowie lat 90 musiał uznać wyższość jakiegoś kompilatora C :)

Kompilatory już jakiś czas temu stały się lepsze w optymalizowaniu całego kodu, obecnie używa się jeszcze asma do optymalizowania pewnych małych fragmentów programu, ale zazwyczaj zyski i tak są niskie.

Przy okazji, niedawno popełniłem mały test wydajności FPC przy wyliczaniu MD5:

default 3339.00
default with optimization 3401.00
default opt SSE2 3541.00
default opt SSE3 3510.00
FPC271 3229.00
FPC271 SSE42 3214.00
FPC271 SSE42 AllOpt 3214.00
FPC271 SSE42 WholeOpt 3619.00
FPC271 SSE42 WholeOpt 3292.00

default- FPC 2.6.0 (budowane z Lazarusa)
opt,AllOpt - optymalizacja
WholeOpt - tzw. Whole program optimization

Może i wyniki nie są dokładne ale nie było to nic dużego, głównie porównanie FPC 2.6.0 i FPC 2.7.1 (gdzie wprowadzono m.in. -O4).

W załączniku kod. Jak chcecie to spróbujcie sił u siebie, mój CPU to Intel Dual E5200 @ 2.5Ghz .

0
-321oho napisał(a):

Przy okazji, niedawno popełniłem mały test wydajności FPC przy wyliczaniu MD5:

default 3339.00
default with optimization 3401.00
default opt SSE2 3541.00
default opt SSE3 3510.00
FPC271 3229.00
FPC271 SSE42 3214.00
FPC271 SSE42 AllOpt 3214.00
FPC271 SSE42 WholeOpt 3619.00
FPC271 SSE42 WholeOpt 3292.00

default- FPC 2.6.0 (budowane z Lazarusa)
opt,AllOpt - optymalizacja
WholeOpt - tzw. Whole program optimization

A nie wydaje się wam, że takie różnice mogą wynikać z różnego obciążenia procesora? Jak mierzyłem czasy wykonania na "zwykłym" Core Duo to ta sama procedura dawała rozstrzał rzędu 10% - na przykład pierwsze uruchomienie programu miało znacząco gorsze wyniki. Tak naprawdę należałoby zrobić serię pomiarów, w powtarzalnych warunkach, obrobić statystycznie... :)

0

A nie wydaje się wam, że takie różnice mogą wynikać z różnego obciążenia procesora?

Mogą, dlatego daje kod dla dociekliwych którzy chcą zmienić procedury na bardziej miarodajne i np. policzyć średnią ze 100 wykonań.

Tak naprawdę należałoby zrobić serię pomiarów, w powtarzalnych warunkach, obrobić statystycznie... :)

Wiem, po to daję kod źródłowy - samemu nie chce mi się tego mierzyć dokładniej :P . Jak jesteś zainteresowany to ulepsz kod, mogę go skompilować na swoim FPC 2.7.1 z maksymalną optymalizacją jeżeli nie wiesz jak to dokładnie zrobić.

0
-321oho napisał(a):

Wiem, po to daję kod źródłowy - samemu nie chce mi się tego mierzyć dokładniej :P . Jak jesteś zainteresowany to ulepsz kod, mogę go skompilować na swoim FPC 2.7.1 z maksymalną optymalizacją jeżeli nie wiesz jak to dokładnie zrobić.

Kłania się czytanie ze zrozumieniem :) Napisałem, że kiedyś już takie pomiary robiłem (w celu sprawdzenia która z funkcji z jakiejś biblioteki działa szybciej). Coś tam mi wyszło, między innymi wnioski którymi podzieliłem się w tym wątku.

FPC nie jest dla mnie - nie chodzą na nim biblioteki DevExpress, co wywraca do góry nogami całą moją wypracowaną przez lata koncepcję UI, a piszę programy dla ludzi :) Wiem, że można je zastąpić zbieraniną komponentów z netu, ale jest jeden problem - ta zbieranina nie jest jednolita ani w warstwie prezentacji, ani w warstwie logiki, i doprowadzenie jej do stanu używalności zajęłoby mi stanowczo zbyt dużo czasu.

0

Kłania się czytanie ze zrozumieniem :) Napisałem, że kiedyś już takie pomiary robiłem (w celu sprawdzenia która z funkcji z jakiejś biblioteki działa szybciej). Coś tam mi wyszło, między innymi wnioski którymi podzieliłem się w tym wątku.

I jaki związek ma to z zarzutem że nie umiem czytać? Dokładnie rozumiem, że robiłeś jakieś testy, zapewne nie takie same jak ja. Wiem również o tym, że we współczesnych procesorach prędkość wykonania zależy w mniejszym stopniu od kodu, w większym od jego otoczenia. Więc gdzie to nie umiem czytać twoim zdaniem?

FPC nie jest dla mnie - nie chodzą na nim biblioteki DevExpress, co wywraca do góry nogami całą moją wypracowaną przez lata koncepcję UI, a piszę programy dla ludzi :) Wiem, że można je zastąpić zbieraniną komponentów z netu, ale jest jeden problem - ta zbieranina nie jest jednolita ani w warstwie prezentacji, ani w warstwie logiki, i doprowadzenie jej do stanu używalności zajęłoby mi stanowczo zbyt dużo czasu.

No to czemu zarzucasz mi brak czytania ze zrozumieniem, skoro nawet nie na FPC sprawdzałeś a ja porównuję prędkość FPC 2.6.0 i FPC 2.7.1? Wychodzi na to, że osoba która mi zarzuca brak czytania ze zrozumieniem, sama nie umie czytać ze zrozumieniem :). Czepiasz się z marnym skutkiem. Jeżeli twoim celem jest próba prowokacji mnie do zachowania na twoim poziomie to odejdź.

0

W FPC można ustawić SSE4.2... Szansa że twoi użyszkodnicy będą mieli SSE5 jest baardzo niska.
O ile przy ogólnej optymalizacji spokojnie można zdać się na kompilator, to nie wierzyłbym w takie automaty do SSE, bo nie wiadomo kiedy i jak algorytm zostanie przekształcony na instrukcje SIMD.

Z automatyczną optymalizacją kompilatora jest tak, że może być lepszy od człowieka w 90% przypadków, a w pozostałych 10% nie ma go w ogóle, bo w danej konkretnej funkcji z jakiegoś zupełnie niejasnego powodu się nie aktywuje.

0

Primo:
Wszelkie niskopoziomowe optymalizacje to heurezy. Procesory ciągle ewoluują i heurezy zwiększające wydajność na jednym procku mogą zmniejszać wydajność na drugim. Typ obciążenia może dynamicznie się zmieniać. Dla przykładu jeśli chodzi o kolejność ifów - czasem jedna kolejność może być lepsza, czasem inna. No i bardzo ciężko przewidzieć jak zachowa się kod zrównoleglony.

Secundo:
Nie wszystkie operacje procesora są trywialne do zrozumienia i użycia. Przykład pewnej dość ekstremalnej instrukcji: http://gynvael.coldwind.pl/?id=386&lang=pl

0

<quote="896789">

I jaki związek ma to z zarzutem że nie umiem czytać? Dokładnie rozumiem, że robiłeś jakieś testy, zapewne nie takie same jak ja. Wiem również o tym, że we współczesnych procesorach prędkość wykonania zależy w mniejszym stopniu od kodu, w większym od jego otoczenia. Więc gdzie to nie umiem czytać twoim zdaniem?

Ano taki, że metodologia pomiarów jest taka sama - nie ma znaczenia czy badasz różnice w czasie wykonania poszczególnych funkcji czy różnice między kompilatorami. Czy to zrozumiałeś?

Przez chwilę wydawało mi się, że da się z Tobą rozmawiać na spokojnie, ale chyba jednak skończyły Ci się te pigułki co Ci je lekarz przepisał :)

0

Ano taki, że metodologia pomiarów jest taka sama - nie ma znaczenia czy badasz różnice w czasie wykonania poszczególnych funkcji czy różnice między kompilatorami. Czy to zrozumiałeś?

Takie samy jeżeli porównuję kompilatory i funkcje? Chyba prędkość kodu generowanego przez kompilatory...
Więc jak ma to się do tego: "Tak naprawdę należałoby zrobić serię pomiarów, w powtarzalnych warunkach, obrobić statystycznie..." Wiem, po to daję kod źródłowy - samemu nie chce mi się tego mierzyć dokładniej . Jak jesteś zainteresowany to ulepsz kod
Tak jak napisałem, doskonale wiem o tym. Moja metoda pomiaru jest mało miarodajna i właśnie z tego powodu podzieliłem się kodem, aby ktoś komu chce się w to bawić, zrobił jakieś porządne zestawienie. I cóż takiego ja tutaj nie umiem przeczytać ze zrozumieniem?

Przez chwilę wydawało mi się, że da się z Tobą rozmawiać na spokojnie, ale chyba jednak skończyły Ci się te pigułki co Ci je lekarz przepisał :)

Otóż mi też się wydawało że można z tobą normalnie rozmawiać. Ale widać ty po prostu musisz mi zarzucić coś bezsensownego, żeby następnie dziwić się mojej reakcji.

0

Wydaje mi się że nie rozumiecie jeden drugiego.
Jeden mówi o tym że potrzebna lepsza statystyka dla sprawdzenia która metoda jest szybsza.
A drugi mówi o tym że przy jakiekolwiek statystyce lepszej czy gorszej może się okazać że dla jednego kompilatora wyjdzie jedna lepsza a przy drugim - druga.

0

@autor
Zdeasemblowałeś cały kod AsmTime? W Pascalu/Delphi pisałem bardzo dawno, ale kompilator dostawiał mnóstwo kodu defensywnego pozwalającego bezpiecznie wywoływać funkcje pascalowe z assemblera i odwrotnie (np. zachowywanie wszystkich rejestrów i odtwarzanie ich po wywołaniu). Ten narzut przy krótkim kodzie jest kosztowny. Generalnie pisząc wstawki nie wywołuj z nich żadnego kodu z zewnątrz (być może wywoływanie innych funkcji assembler będzie bez tego narzutu, ale nie dam głowy).
Ostatni raz kiedy byłem zmuszony pisać wstawki, to robiłem to dla takich prostych operacji na buforach tekstu edytora jak zmiany małych liter na duże i odwrotnie. Różnica między czasem wykonania kodu stworzonego przez kompilator, a tym ze wstawki miała się jak >10:1 na korzyść wstawki. Na tekstach 60 KB zmiana liter w Pascalu trwała nieakceptowalne 1-3 sekundy, a ze wstawki milisekundy (nieodczuwalna dla człowieka).

0

Jeden mówi o tym że potrzebna lepsza statystyka dla sprawdzenia która metoda jest szybsza.
A drugi mówi o tym że przy jakiekolwiek statystyce lepszej czy gorszej może się okazać że dla jednego kompilatora wyjdzie jedna lepsza a przy drugim - druga.

Ah, widać rzeczywiście masz umiejętności jasnowidzenia. Tak, twierdzę że mimo że wyniki nie są dokładne to i tak potwierdzają one to, że programy pisane w FPC 2.7.1 są szybsze niż w FPC 2.6.0 (ze względu na nowe opcje optymalizacji -O4). Nie potrzebuję idealnej statystyki żeby to sprawdzić, pisałem od tak, nic dokładnie nie sprawdzając, miały to być testy 'wewnętrzne' sprawdzające przede wszystkim opcję wholeopt.

Zdeasemblowałeś cały kod AsmTime? W Pascalu/Delphi pisałem bardzo dawno, ale kompilator dostawiał mnóstwo kodu defensywnego pozwalającego bezpiecznie wywoływać funkcje pascalowe z assemblera i odwrotnie (np. zachowywanie wszystkich rejestrów i odtwarzanie ich po wywołaniu). Ten narzut przy krótkim kodzie jest kosztowny. Generalnie pisząc wstawki nie wywołuj z nich żadnego kodu z zewnątrz (być może wywoływanie innych funkcji assembler będzie bez tego narzutu, ale nie dam głowy).

FPC nie generuje kodu defensywnego w przypadku gdy assembler jest w funkcji. Zakłada on, że kod assemblera będzie się trzymać danej konwencji wywołań (tutaj domyślne: nostackframe & register - ale nie jestem pewien co FPC zrobi ze zmienną From, być może jednak wymusi generowanie stack frame). W przypadku mieszania kodu assemblerowego i Pascala, FPC nie sprawdza rejestrów modyfikowanych przez assembler i generuje kod na ślepo, zazwyczaj nie używając optymalizacji na poziomie rejestrów. A to też jest rodzina 'Pascal/Delphi'.

Ostatni raz kiedy byłem zmuszony pisać wstawki, to robiłem to dla takich prostych operacji na buforach tekstu edytora jak zmiany małych liter na duże i odwrotnie. Różnica między czasem wykonania kodu stworzonego przez kompilator, a tym ze wstawki miała się jak >10:1 na korzyść wstawki. Na tekstach 60 KB zmiana liter w Pascalu trwała nieakceptowalne 1-3 sekundy, a ze wstawki milisekundy (nieodczuwalna dla człowieka).

Pascal to nie jest jeden kompilator ani jeden dialekt... Nigdy nie odczuwałem niedostatku prędkości w przypadku funkcji upcase pod FPC, ale wiadomo że wszystko można napisać szybciej (np. operując na dwordach albo quadwordach, robiąc parę iteracji pętli na raz..)

1 użytkowników online, w tym zalogowanych: 0, gości: 1