Kolejność ma znaczenie
Z artykułu Wojciecha Śmietanki wypływa ważny morał: przystosowanie algorytmu do działania na maszynie równoległej wymaga często zupełnie innego spojrzenia na dany problem. Okazuje się jednak, że nawet w przypadku architektury jednoprocesorowej optymalizacja algorytmu może wymagać od nas całkiem pomysłowych przeróbek. W tym artykule podamy dwa przykłady, w których kluczową okaże się kolejność, w jakiej wykonujemy operacje.
Za pierwszy przykład niech posłuży wspomniany już algorytm mnożenia macierzy. Można go zapisać, na przykład, tak:
Z uwagi na przemienność dodawania nie ma znaczenia, w jakiej kolejności wykonamy powyższe pętle. Ważne jest tylko to, aby ostatni wiersz został wykonany dla wszystkich trójek Okazuje się, że to, iż pętle występują w kolejności (zamiast, wydawać by się mogło, bardziej naturalnej kolejności ), ma kluczowe znaczenie. Na moim komputerze ten program dla wykonuje się 2,1 s, natomiast program „naturalny” aż 8,2 s – prawie cztery razy wolniej! Wiąże się to z tym, że w powyższym programie wszystkie macierze są przeglądane wierszami, co dobrze wpływa na wykorzystanie pamięci podręcznej procesora.
Często, aby umożliwić wykonywanie operacji w lepszej kolejności, konieczne jest głębsze przebudowanie algorytmu. Za przykład posłuży nam tu wyznaczanie liczb pierwszych za pomocą sita Eratostenesa.
Zauważmy, że dla każdej nowo znalezionej liczby pierwszej przeglądamy prawie całą tablicę by wykreślić wielokrotności To powoduje, że nieefektywnie korzystamy z pamięci podręcznej procesora. Spróbujmy zatem tak przepisać powyższy algorytm, by zwiększyć lokalność odwołań do pamięci.
Zauważmy, że liczba pierwsza zostanie użyta do wykreślania tylko, gdy Na początek wykonajmy więc powyższy kod dla początkowego kawałka tablicy przy okazji zapamiętując w tablicy napotkane liczby pierwsze, a w tablicy ich najmniejsze wielokrotności większe niż Resztę tablicy podzielmy na bloki długości i przeglądajmy te bloki kolejno, za każdym razem wykreślając (dla wszystkich ) znajdujące się w tym bloku wielokrotności liczby odpowiednio uaktualniając w najmniejszą niewykorzystaną jeszcze wielokrotność Poniższy pseudokod realizuje ten nowy algorytm:
Jeśli przyjmiemy to pierwszy kod działa na moim komputerze 4,2 s. Pamięć podręczna w moim procesorze ma rozmiar 32 kB, zatem możemy przyjąć Wtedy drugi program działa w czasie 1,3 s, co daje ponadtrzykrotne przyspieszenie, mimo iż liczba operacji wykonywanych w pseudokodzie wzrosła!
Można, oczywiście, dalej optymalizować nasz algorytm, np. wyrzucić z tablicy liczby parzyste i użyć tablicy bitowej. Można również przyjąć jako rozmiar bloku wielokrotność iloczynu małych liczb pierwszych i zauważyć, że wtedy wielokrotności tych liczb w każdym bloku są położone tak samo.