2008-05-10

Czy programy napisane w assemblerze są lepsze od tych w C?

Słyszałem wiele opinni na ten temat. Ostatnio nawet ktoś wykłócał się co jest lepsze. O tym, że assebler nie może zostać zapomniany świadczy mój prosty test. Test został przeprowadzony na małym programie, wypisującym aktualny timestamp.

Na początek, wytłumaczenie dla nie informatycznych. Timestamp to ilość sekund, które mineły od 1 stycznia 1970 00:00:00 (tzw, początek ery uniksa, EPOCH) do chwili obecnej. Na podstawie tej wartości, oblicza się daty w komputerach z systemem operacyjnym z rodziny Unix, w Windows'ach jest chyba podobnie, ale u nich, czas liczony jest chyba od innej daty. Wiąże się to z pewnym problemem, ponieważ 19 stycznia 2038 03:14:07 czasu UTC, licznik się przekręci. Dobra, przejdżmy do wyników testów.

Pierw porównajmy wielkości programów. Na sam początek pójdzie statycznie skompilowany program wypisujący timestamp o kodzie zajmującym jedyne 91 bajtów:
#include<stdio.h>
#include<time.h>

int main(){
printf("%d\n", time(NULL));
return 0;
}

Kod myśle banalny, wypisz aktualny timestamp jako liczbę i wypisz znak nowej lini. A więc ten kod zajmuje 550959 bajtów, co daje w zaokrągleniu 551KB (538KiB). Teraz raczej unika się kompilowania statycznego, więc ten sam kod, skompilowałem dynamicznie, wynik od razu rzuca się w oczy, ponieważ jest to tylko 6399, co daje w zaokrągleniu 6KB (6KiB). Powalająca różnica, prawda? Teraz przyszedł czas na assemblera. Kod w assemblerze będzie "nieco dłuższy" ponieważ zajmuje 449kb, ale z pomocą kursu Pana Bogdana Drozdowskiego powstał naprawdę szybko. Oto kod:
global _start

section .text

_start:
mov eax, 0dh
xor ebx, ebx
int 80h
;przygotowanie do itoa
mov esi, 1
mov ecx, 10
itoa:
xor edx, edx
div ecx
or dl, '0'
mov [buff+esi], dl
inc esi
test eax, eax
jnz itoa

wypisz2:
mov ecx, buff
mov eax, 4
mov ebx, 1
add ecx, esi
mov edx, 1
int 80h
dec esi
jnz wypisz2

mov eax, 4
mov ebx, 1
mov ecx, buff
mov edx, 1
int 80h

exit:
mov eax, 1
int 80h
section .data
buff times 11 db 10

Kod nie jest maksymalnie wydajny, by nie powiedzieć, że wypisywanie zostało napisane na "odwal się". Tutaj proszę o uwagę, całość zajmuje jedyne 868 bajtów, czyli poniżej 1KB! Ale to nie jedyny test jaki przeprowadziłem, pora na...

...czasy wykonania programów! Czasy będę liczył za pomocą Linuksowego narzędzia time, więc można przyjąć, że będą wiarodajne, choć jeśli ktoś chce przeprowadzić własne testy, ma kody wyżej. Na pierwszy ogień idzie timestamp wykonany w C kompilowany statycznie. Po wielu wywołaniach programu, miałem wciąż te same wyniki:
real 0m0.019s
user 0m0.000s
sys 0m0.000s
więc uważam, że można im zaufać. Pierwszy wskaźnik pokazuje ile wykonywał się kod, 0.019 sekundy to naprawdę mało. Użytkownik nic nie wprowadzał, więc jego inwencja trwała zero i system uwinął się w poniżej 0.001 sekundy. Kolejny test, program w C skompilowany dynamicznie.
real 0m0.036s
user 0m0.000s
sys 0m0.000s
Jak widać, działał o 0.017 sekundy dłużej. Można sie było tego spodziewać, teraz kolej testu timestamp'u napisanego w assemblerze:
real 0m0.001s
user 0m0.000s
sys 0m0.004s
Tutaj widzimy system pracował dłużej, a jest to spowodowane wielokrotnym wywoływaniem przerwania wypisywania, ale po jednym znaku, C pierw wszystko formatowało, wrzucało do swojego buffora, a dopiero później wypisywało, przez co jądro miało mniej roboty, to samo mogłem zrobić w assemblerze, ale nie chciało mi się pisać dłużej kodu. Wyniki są jednoznaczne, program w assemblerze wykonał się w 0.005 sekundy.

Jeśli assembler jest taki mały i szybki, to dlaczego nie pisać w nim każdego programu? Assembler jest bardzo nie przenośny, algorytm napisany na PC, trzeba będzie przepisać, by można było go odpalić na mikrofalówce. C ma tutaj wielką przewagę, bo raz dobrze napisany algorytm, będzie działał na wielu platformach. Drugim argumentem przemawiającym przeciw assemblerowi, jest fakt że źródła programu napisanego w nim, są o wiele dłuższe niż w C(w naszym przypadku o okolo 80%). Istnieje też opinia, że w assemblerze się trudniej pisze, chociaż ja się z nią nie zgadzam, według mnie, jest on łatwiejszy, ale może to być złudzenie, ponieważ przez to że kody są w nim dłuższe, mogłem po prostu lepiej nauczyć się programować w tym języku.

Osąd, który z wyżej przedstawionych języków jest lepszy, pozostawiam wam, drodzy czytelnicy.

2 komentarze:

Anonimowy pisze...

Fajnie, tylko że zmierzyłeś czas ładowania programu do pamięci + inicjalizacji; samo pobieranie czasu w obu przypadkach zajmuje tyle samo.
W dodatku printf za każdym razem musi od nowa parsować format string - te programy są całkowicie inne, jedyna wspólna cecha to wypisanie czasu.

Co do wielkości - rozmiar ma tutaj związek z budową elfa (kompilator da ci więcej sekcji + symbole do bibliotek), nie z wielkością kodu.

Jak już chcesz porównywać szybkość to porównaj coś numerycznego - np. obliczanie kolejnych punktów linii (xyz -> xyz') w C i assemblerze. Najlepsza możliwa wersja w assemblerze (na fpu) będzie tylko o ok. 3% szybsza niż ta w C... A i to wcale nie jest takie pewne.

Równie dobrze możesz porównać FFT, nawet bardziej miarodajne.

P.S C to tylko trochę wyższy assembler. Prawdziwe różnice (na niekorzyść asma) pojawią się w językach z dobrym gc - szybsza alokacja pamięci (i jej defragmentacja) jest o wiele ważniejsza dla szybkości programu niż 2 cykle mniej podczas jakichś obliczeń.
A o skalowaniu to już nie wspomnę - tutaj assembler na starcie jest na straconej pozycji.

Anonimowy pisze...

Btw - windows przechowuje czas w formie 64 bitowej, więc ten problem go nie dotyczy.