Problem
Buildroot, Yocto, OpenWRT – projekty, które dzięki staraniom setek (może tysięcy?) programistów znacznie ułatwiają budowanie dedykowanych dystrybucji Linuxa. Gdzie bylibyśmy bez nich, kompilując i dodając poszczególne komponenty ręcznie i w małych kawałkach?
Można napisać wiele stron o zaletach każdego z tych frameworków. Można też z całą pewnością stwierdzić, że mają sporą wadę – czas budowania.
Linux daje nam bardzo wiele rzeczy w pakiecie, m.in. ochronę pamięci przez MMU, gotowe stosy sieciowe, biblioteki do obsługi audio/video, wiele gotowych i sprawdzonych narzędzi w userspace. Niestety nic nie jest za darmo, zbudowanie tego wszystkiego i odpowiednie poukładanie wymaga sporo wiedzy i… czasu. A czas budowania dużych projektów staje się memem, co potwierdza poniższy obrazek:

Źródło: https://xkcd.com/303/
Jasne, czasem fajnie jest zrobić sobie przerwę. Jeszcze lepiej gdy mamy za to płacone. Niestety, po kilku takich wymuszonych postojach zaczyna się chyba jeden z najgorszych rzeczy w pracy programisty, czyli frustracja.
Wyobraź sobie: pracujesz już całkiem długo nad konkretnym zadaniem. Wiesz już bardzo dużo i czujesz, że jesteś blisko. Pełne skupienie, praktycznie wchodzisz w tzw. flow, czyli stan psychiczny, w którym jesteś w pełni pochłonięty, skupiony i cieszysz się procesem działania, często prowadząc tracąc poczucie czasu. Produktywność sięga zenitu.
Dochodzisz do momentu gdy w końcu trzeba przebudować projekt i koniec – czeka cię wymuszona, 45 minutowa przerwa. Wracając do zadania, po dawnym skupieniu niewiele zostanie.
Na szczęście istnieje kilka rozwiązań, które są łatwe w implementacji i mogą ZNACZNIE przyspieszyć budowanie projektu. W moim konkretnym przypadku udało się osiągnąć 5-krotną poprawę czasu budowania!
Z 46 do 9 minut.
Pokażę Ci jak to osiągnąć.
Eksperyment
Do eksperymentu używam struktury z jednego z poprzednich wpisów (KLIK). Odpalam build w następujący sposób:
$ time make -j8 clean all
$ oznacza że komenda jest wykonywana w terminalu. Przepisywanie tego symbolu nie jest potrzebne.
Polecenie $ time umożliwia w łatwy sposób zmierzenie ile czasu zajmie dana komenda – w tym przypadku budowanie systemu. Interesuje nas w zasadzie tylko pozycja oznaczona jako REAL. Oznacza to czas jaki upłynął w trakcie wykonywania komendy (tzw. „wall clock time”).
Czas przebudowania projektu bez żadnej optymalizacji:
real 46m6,486s
user 251m10,410s
sys 29m39,299s
CCACHE
CCACHE to pamięć podręczna kompilatora. Przyspiesza rekompilację poprzez buforowanie poprzednich kompilacji i wykrywanie, kiedy ta sama czynność jest wykonywana ponownie.
Działa to w następujący sposób: ccache oblicza wartość HASH plików źródłowych i opcji kompilatora. W trakcie kolejnej kompilacji, jeśli nowa wartość HASH jest różna od pierwotnej, zapisanej przez CCACHE, dany plik jest kompilowany ponownie. Jeśli jest taka sama, zamiast kompilować od nowa, używane są wcześniej wygenerowane pliki. Warto zauważyć, że zmiana opcji kompilacji (np. dodanie albo usunięcie flagi) również spowoduje ponowną kompilację plików źródłowych.
Włączenie ccache da prawdopodobniej najbardziej wymierne efekty pomiędzy rebuildami projektu.
Warto też pamiętać, że użycie ccache nie przyspieszy samej kompilacji, tylko ponowne kompilacje całego projektu. W skrócie, jeśli często używasz make clean; make
to na pewno zauważysz różnicę.
Zaznaczamy w menuconfig CCACHE i ponownie przebudowujemy obraz. Ma to na celu wypełnienie pamięci podręcznej bieżącymi wartościami (bo na razie mamy pustkę).

Stosowną opcję znajdziesz w $ make menuconfig w submenu Build options.
Jak wygląda i gdzie jest nasz cache?
$ ls $HOME/.buildroot-ccache/
0 1 2 3 4 5 6 7 8 9 a b c d e f lock
Struktura katalogów, którą widzisz (0-9 i podkatalogi a-f, plus plik lock) jest typowa dla ccache i podobnych systemów (np. sstate cache w Yocto).
Dlaczego tak dziwnie to wygląda?
Katalogi od 0 do f reprezentują wszystkie 16 cyfr szesnastkowych. Ta struktura jest używana do wydajnej organizacji plików i szybkiego wyszukiwania. Kiedy ccache przechowuje skompilowany obiekt, generuje hash plików wejściowych i parametrów kompilacji. Pierwszy znak tego skrótu określa, do którego podkatalogu trafi buforowany plik. Ponadto ta struktura pozwala na szybsze wyszukiwanie plików poprzez zawężenie do określonego podkatalogu zamiast przeszukiwania jednego dużego katalogu. Wszystkie systemy operacyjne mają limit liczby plików w danej ścieżce. Jest on bardzo duży, ale w wypadku plików cache mógłby zostać wyczerpany…
Plik „lock” to mutex, służy do zapobiegania jednoczesnej modyfikacji pamięci podręcznej przez wiele procesów ccache, zapewniając integralność danych.
Gdzie znajduje się wygenerowany cache?
Decyduje o tym opcja w konfiguracji, konkretnie BR2_CCACHE_DIR. Domyślna wartość to $HOME/.buildroot-ccache, znajduje się poza katalogiem naszego projektu, żeby w łatwy sposób móc korzystać z cache między różnimy projektami. Poza tym obniża to ryzyko przypadkowego usunięcia folderu. Jeśli chcesz się pozbyć ccache, wystarczy usunąć ten folder.
Wyniki
Po wypełnieniu pamięci podręcznej ponownie odpalamy pełne przebudowanie i patrzymy na wyniki. Oto one:
real 17m5,025s
user 45m20,255s
sys 11m10,514s
Możemy również pójść o krok dalej i przed budowaniem “dropnąć cache”, czyli zmusić system operacyjny do wykonania operacji bez korzystania z pamięci podręcznej. Czasem stosuje się taki zabieg, żeby wygenerować bardziej miarodajne wyniki.
Z dropnięciem cache:
$ sudo sh -c "sync; echo 3 > /proc/sys/vm/drop_caches"
real 18m29,186s
user 48m29,584s
sys 12m51,915s
Wyniki mówią same za siebie – włączenie CCACHE w projekcie prowadzi do bardzo widocznego spadku czasu budowania, nawet ponad 2 razy!
Pre-built toolchain
W kontekście systemów wbudowanych toolchain to zestaw narzędzi programistycznych służących do tworzenia aplikacji i systemu operacyjnego. Zazwyczaj w skład toolchaina wchodzi: kompilator, linker, assembler, debugger oraz biblioteki współdzielone (shared libs). Gdy budujemy projekt w Buildroocie, kompilujemy również domyślnego toolchaina, czyli tworzymy „własny” kompilator. Jeśli nie wprowadzamy zmian w toolchainie lub korzystamy z tego samego w różnych projektach, możemy śmiało pominąć kompilowanie go na nowo i po prostu użyć już zbudowanej wersji.
Jak skonfigurować to w buildroocie?
W głównym menu menuconfig przechodzimy do podmenu Toolchain i wybieramy opcję 'External’. Możemy wskazać ścieżkę do własnego toolchaina albo wybrać jedną z już przygotowanych opcji. Ja wybrałem tę drugą możliwość.

Przy pierwszym budowaniu toolchaina nie zaoszczędzimy czasu – w końcu najpierw musimy go stworzyć. Jednak przy kolejnych buildach, gdy już będzie skonfigurowany może zaoszczędzić naprawdę sporo. Dopóki nie będziemy nic zmieniać w samym toolchainie, utrzymamy dobre tempo budowania.
Ostatecznie, czas budowania projektu z zewnętrznym toolchainem i włączonym CCACHE:
real 9m52,353s
user 31m13,066s
sys 6m59,598s
Zyskaliśmy prawie 10 minut!
Kiedy naprawdę potrzebujemy przebudowania?
Tutaj przydaje się doświadczenie i znajomość Buildroota albo projektu, z którym pracujemy. Np. W OpenWRT również nie zawsze musimy przebudowywać cały projekt. Jeśli chcesz wiedzieć więcej na ten temat, odwiedź jeden z poprzednich wpisów: KLIK1 lub tutaj: KLIK2
Nie korzystaj z NFS
Tutaj bardzo krótko – zbieranie (po dewelopersku “fetchowanie”) plików z NFS, czyli sieciowego serwera plików, wprowadza dodatkowe opóźnienia. Możemy w ten sposób dużo zaoszczędzić na miejscu, ale odbije się to prędkością wykonywanych operacji. W zależności od konkretnego przypadku warto rozważyć zmianę konfiguracji sprzętowej. Co przenosi nas do ostatniego punktu…
Podsumowanie
Dzięki kilku łatwym do „wyklikania” opcjom w konfiguracji projektu możemy zyskać naprawdę dużo czasu. W moim małym eksperymencie udało się zejść z 46 minut do nieco ponad 9, czyli uzyskać ponad 5-krotne przyspieszenie budowania.
Dla innych projektów konfiguracja może się różnić, chociaż sporo rzeczy będzie powtarzalnych. Przebudowywanie toolchaina za każdym razem jest dość kosztowne. Włączenie cache zazwyczaj się opłaci a NFS zawsze będzie wolniejsze niż bardziej bezpośredni dostęp do plików.
Nie poruszyliśmy jeszcze jednej bardzo ważnej rzeczy, czyli konfiguracji sprzętowej. Jak bardzo pomoże dysk SSD? Czy opłaca się używać ramdysku? To zostanie omówione w kolejnym wpisie 🙂
Ostateczne podsumowanie wyników na ładnym wykresie:

Dodaj komentarz