Flagi kompilatora C i C++
Napisał: Patryk Krawaczyński
24/07/2009 w Administracja Brak komentarzy. (artykuł nr 106, ilość słów: 741)
T
ak jak w przypadku wszystkich optymalizacji związanych z systemem – zoptymalizować można działanie kompilatorów, które skompilują nam program pod dedykowaną architekturę. Oczywiście należy pamiętać o zasadzie KISS (Keep It Simple Stupid), aby nie przesadzić. Ostrożne używanie flag kompilatorów da nam stabilny i szybki system, zbyt ostre może nieźle w nim namieszać. Jeżeli mamy wątpliwości, zostawmy te flagi w spokoju. W celu dokładniejszych wyjaśnień należy zajrzeć do strony manualej i dokumentacji gcc. Najczęstszymi zmiennymi, którymi się posługuje przy ustawieniu flag kompilatorów to:
- CFLAGS – opcje kompilatora języka C
- CXXFLAGS – opcje kompilatora języka C++
Przykładem będzie optymalizacja flag dla procesora: Pentium 4 Mobile – o oznaczeniu kodowym: cpu family: 15 / model: 2 – szczegółowe informacje na temat posiadanego procesora możemy uzyskać poprzez podgląd pliku /proc/cpuinfo. Aby ustawić optymalizację dla określonego zestawu instrukcji oraz modelu planowania należy skorzystać z opcji -march=XXX gdzie XXX jest typem naszego procesora. Na przykład mając procesor Pentium 4 i ustawiając opcję: -march=pentium4 włączone zostają optymalizacje dla: MMX, SSE, SSE2, SSE3 itd. czyli: -mmmx, -msse, -msse2, -msse3… Dodatkowo użyjemy flag:
- -O2 – optymalizacji, która nie zwiększa rozmiaru kodu – jest to najczęściej obecnie wykorzystywana flaga – kompilator wprowadza optymalizację pod względem rozmiaru kodu wynikowego. Jest to można by rzec neutralna flaga, najlepiej przetestowana, przy jej użyciu nie powinny pojawiać się jakiekolwiek problemy w działaniu programu z dobrze napisanym kodem. Z tą flagą automatycznie wprowadzone zostają optymalizacje: -fthread-jumps / -fcrossjumping / -foptimize-sibling-calls / -fcse-follow-jumps / -fcse-skip-blocks / -fgcse / -fgcse-lm / -fexpensive-optimizations / -fstrength-reduce / -frerun-cse-after-loop / -frerun-loop-opt / -fcaller-saves / -fpeephole2 / -fschedule-insns / -fschedule-insns2 / -fsched-interblock -fsched-spec / -fregmove / -fstrict-aliasing / -fdelete-null-pointer-checks / -freorder-blocks / -freorder-functions / -funit-at-a-time / -falign-functions / -falign-jumps / -falign-loops / -falign-labels / -ftree-vrp / -ftree-pre.
- -pipe – użyjemy potoków zamiast plików tymczasowych w komunikacji między różnymi etapami kompilacji – nie wpłynie to na optymalizację kodu, ale spowoduje, że kompilator podczas kompilacji będzie korzystać z pamięci naszego komputera. Przyspiesza to proces kompilacji wielu programów. Jest to flaga bezpieczna i nie powinno być problemów z kompilacją jakiegokolwiek programu przy jej użyciu. Jedynym jej minusem to większe zużycie pamięci.
- -fomit-frame-pointer – flaga ta, dzięki temu, że używa pewnej modyfikacji przy wywoływaniu funkcji pozwala na zaoszczędzenie pamięci i skraca czas uruchomienia programu. Jej zastosowanie powinno mieć miejsce w przypadku kompilacji programów użytkowych ze względu na fakt, iż jej zastosowanie powoduje problemy przy debugowaniu danego programu.
Tak, więc nasz kod optymalizacyjny dla procesora Pentium 4 Mobile, będzie wyglądał następująco:
# Processor settings export CHOST="i686-pc-linux-gnu" export CFLAGS="-march=pentium4 -O2 -pipe -fomit-frame-pointer" export CXXFLAGS="${CFLAGS}"
Taką, frazę możemy dodać na końcu pliku /etc/profile, lub umieścić w katalogu /etc/profile.d. Od teraz tak ustawione flagi powinny generować lepszej jakości i lepiej zoptymalizowany kod binarny. Od teraz gcc podczas każdej kompilacji będzie ich używać. By wykazać wpływ flag kompilatora, na szybkość programów, został wykonany test za pomocą programu speed.c autorstwa Andrew Tridgell (większość osób powinna go kojarzyć z dwoma faktami: Samba i rezygnacja z BitKeepera). Pierwsza część testu została wykonana ze standardowymi flagami, a druga przy użyciu wyżej wymienionych (testowanym sprzętem jest Pentium 4 Mobile 512 MB DRR 266 MHz):
agresor@evo:~# gcc -w speed.c -o speed -lm agresor@evo:~# ./speed Floating point - sin() - 5.55957 MOPS Floating point - log() - 5.97872 MOPS Memcpy - 1kB - 1808.03 Mb/S Memcpy - 100kB - 2593.11 Mb/S Memcpy - 1MB - 400.406 Mb/S Memcpy - 10MB - 426.56 Mb/S Adding integers - 133.222 MOPS Adding floats (size 4) - 41.4978 MOPS Adding doubles (size 8) - 41.3529 MOPS
Dodajemy odpowiednie flagi do kompilatora gcc:
agresor@evo:~# gcc -w -march=pentium4 (...) speed.c -o speed -lm agresor@evo:~# ./speed Floating point - sin() - 5.57526 MOPS Floating point - log() - 6.18735 MOPS Memcpy - 1kB - 1907.19 Mb/S Memcpy - 100kB - 2595.4 Mb/S Memcpy - 1MB - 412.819 Mb/S Memcpy - 10MB - 428.808 Mb/S Adding integers - 587.055 MOPS Adding floats (size 4) - 44.7066 MOPS Adding doubles (size 8) - 44.6577 MOPS
W niektórych przypadkach różnica jest znaczna, w niektórych mało zauważalna. Zależy to głównie od faktu, z jakich funkcji dany program korzysta, lecz nawet przy tak małym programie można było zauważyć różnicę w jego rozmiarze po jego kompilacji za pomocą różnych opcji. Przedstawione powyżej opcje są kroplą w morzu możliwości dostosowywania opcji kompilacji, jednak są tym samym najbezpieczniejszym wyjściem, ponieważ używając zbyt wielu flag, możemy doprowadzić do wielu dysfunkcji kompilowanych programów. Warto wspomnieć, że GCC od wersji 4.2 wspiera funkcję -march=native, która automatycznie wykrywa typ procesora dostosowując wspierane rozszerzenia i przypisuje do niego odpowiednie ustawienia.
Więcej informacji: Safe Cflags, Cflags Matrix, CHOST, speed.c