Co do samego tematu: Szybkość kodu w asemblerze wynika z tego, że pisząc w nim kod wykorzystujemy wszystkie założenia dotyczące projektu - założenia są podstawą optymalizacji, a jeśli ktoś nie wykorzystuje wszystkich założeń, niech pisze w C, mniej się narobi, bo i tak nic nie zoptymalizuje - i tak:
- można niektóre warunki uprościć (założenia dotyczące zakresu zmiennych oraz wartości jakiejś operacji logicznej)
- można optymalizować główną ścieżkę wykonywania (najczęściej wykonywany kod dać w jednym ciągu, podczas gdy do przypadków szczególnych przechodzić skokami - w avr'ach jest to drobny zysk w cyklach: 1 cykl na niewykonaną instrukcję skoku, 2 na wykonaną)
- można optymalizować rozłożenie zmiennych po rejestrach (zmienne najważniejsze/często wykorzystywane trzymać w rejestrach, mniej ważne ładować z pamięci - tutaj kompilator C może działać równie dobrze, czasem lepiej, jednak nie uwzględnia on przypadku najczęstszego w jakim kod jest wykonywany, kod jest zoptymalizowany tak samo lub podobnie pod wszystkie przypadki)
- można implementować niskopoziomowe sztuczki, łącznie z wykorzystaniem instrukcji które nie są dostępne bezpośrednio z poziomu C.
- można rozwijać kluczowe dla nas pętle do dowolnego poziomu wykorzytując dodatkowe założenia na czas (im szybciej, tym więcej rozwinięta pętla), warunki pętli (jeśli liczba przebiegów jest wielokrotnościa jakiejś liczby, mozna rozwinąć o liczbę będącą dzielnikiem tej liczby), liczbę przebiegów (przy dużej liczbie przebiegów jest sens rozwijać pętle bardziej, gdyż jest większy zysk spowodowany mniejszą liczbą sprawdzania warunków pętli)
- można bezpośrednio zrealizować wszystkie usprawnienia, których kompilator nie potrafi. Wywołania końcowe - następujące po sobie [r|i]call oraz ret można zamienić na samo [r|i]jmp. Funkcje w obrębie jednego modułu można zoptymalizować znając listę wykorzystywanych rejestrów - w C wywołanie funkcji powoduje, że rejestry r0 oraz r18-r31 (bez r28 i r29) muszą - co jest związane z konwencją przekazywania parametrów i wywołań funkcji - być uznane za nie zawierające poprzedniej wartości. W kodzie asemblera lub w obrębie modułu napisanym w asemblerze można wywołać inną funkcję, o której wiadomo, że nie zmienia pewnych rejestrów. Nie trzeba wtedy ich odkładać na stos (lub przenosić do niższych rejestrów, które to muszą być odkładane na stos na początku funkcji).
Ogólnie w C ciężko wprowadzić niektóre założenia dotyczące kodu, przez co kompilator nie może zoptymalizować kodu do konkretnego przypadku (ten sam kod może być raz wydajny, raz nie, zależy to od założeń na dane). W asemblerze wszystkie założenia da się wprowadzić, gdyż jest to najniższy poziom na jakim pisze się kod.
Pomimo argumentów które przytoczyłem oraz dużego doświadczenia w pisaniu asm na avry jestem zwolennikiem łączenia C z asemblerem - aktualnie większość projektów jakie mam składa się z kodu głównego w C (często ponad 80% kodu), ale moduły odpowiedzialne za procesy krytyczne czasowo (minimalny czas reakcji) lub takie, które muszą być wykonywane skrajnie szybko (szybkość przetwarzania bloków danych) mam napisane w asemblerze. Jest to najlepszy kompromis, powiązanie szybkości pisania kodu (C) z możliwością silnego zoptymalizowania wybranych fragmentów (asm).
Przenośność jest ograniczona, ale też nie do końca, kod w asemblerze nie da się przenieść na inną rodzinę procesorów, ale też nie ma sensu, gdyż na innej może istnieje możliwość jeszcze większej optymalizacji danego fragmentu kodu.