Używam int
typu do przechowywania wartości. Zgodnie z semantyką programu, wartość zawsze zmienia się w bardzo małym zakresie (0 - 36) i int
(nie a char
) jest używana tylko ze względu na wydajność procesora.
Wygląda na to, że na tak małym zakresie liczb całkowitych można przeprowadzić wiele specjalnych optymalizacji arytmetycznych. Wiele wywołań funkcji na tych liczbach całkowitych może zostać zoptymalizowanych do postaci małego zestawu operacji „magicznych”, a niektóre funkcje mogą być nawet zoptymalizowane pod kątem przeszukiwania tabel.
Czy można więc powiedzieć kompilatorowi, że int
jest to zawsze w tak małym zakresie i czy kompilator może wykonać te optymalizacje?
unsigned
typów, ponieważ są one łatwiejsze do zrozumienia dla kompilatora.var value: 0..36;
.int
iunsigned int
trzeba być steru- lub zerowej wydłużony z 32 do 64-bit, też w większości systemów z 64-bitowych wskaźników. Zauważ, że na x86-64 operacje na rejestrach 32-bitowych rozszerzają się zera do 64-bitów za darmo (nie rozszerzanie znaku, ale przepełnienie znaku jest niezdefiniowanym zachowaniem, więc kompilator może po prostu użyć 64-bitowej matematyki ze znakiem, jeśli chce). Widzisz więc tylko dodatkowe instrukcje do 32-bitowych argumentów funkcji rozszerzających o zero, a nie wyniki obliczeń. Byłbyś dla węższych, niepodpisanych typów.Odpowiedzi:
Tak to mozliwe. Na przykład,
gcc
możesz użyć,__builtin_unreachable
aby powiedzieć kompilatorowi o niemożliwych warunkach, na przykład:Powyższy warunek możemy zawinąć w makro:
I używaj go w ten sposób:
Jak widać ,
gcc
przeprowadza optymalizacje na podstawie tych informacji:Produkuje:
Jest to jednak jedna wada jeśli Twój kod kiedykolwiek złamie takie założenia, otrzymujesz niezdefiniowane zachowanie .
Nie powiadamia Cię, kiedy to się stanie, nawet w kompilacjach do debugowania. Aby łatwiej debugować / testować / wychwytywać błędy z założeniami, możesz użyć hybrydowego makra zakładaj / potwierdzaj (kredyty dla @David Z), jak to:
W debug buduje (z
NDEBUG
nie określono), to działa jak zwykłyassert
, drukowania i komunikat o błędzieabort
programu „ing i uwalniania buduje to sprawia, że korzystanie z założenia, produkujących zoptymalizowany kod.Należy jednak pamiętać, że nie zastępuje zwykłego
assert
-cond
pozostaje w kompilacjach wydań, więc nie powinieneś robić czegoś takiegoassume(VeryExpensiveComputation())
.źródło
return 2
gałąź usunięta z kodu przez kompilator.__builtin_expect
jest ścisłą wskazówką.__builtin_expect(e, c)
powinno być odczytywane jako „e
najprawdopodobniej oceni doc
” i może być przydatne do optymalizacji przewidywania gałęzi, ale nie ogranicza sięe
do tegoc
, aby zawsze być , więc nie pozwala optymalizatorowi na odrzucenie innych przypadków. Zobacz, jak gałęzie są zorganizowane w zespole .__builtin_unreachable()
.assert
, na przykład określićassume
, jakassert
gdyNDEBUG
nie jest określony, jak i__builtin_unreachable()
gdyNDEBUG
jest zdefiniowana. W ten sposób uzyskujesz korzyść z założenia w kodzie produkcyjnym, ale w kompilacji do debugowania nadal masz jawne sprawdzenie. Oczywiście musisz wtedy wykonać wystarczającą liczbę testów, aby upewnić się, że założenie zostanie spełnione na wolności.Jest to standardowe wsparcie. Powinieneś zrobić to include
stdint.h
(cstdint
), a następnie użyć typeuint_fast8_t
.To mówi kompilatorowi, że używasz tylko liczb z przedziału od 0 do 255, ale można użyć większego typu, jeśli daje to szybszy kod. Podobnie kompilator może założyć, że zmienna nigdy nie będzie miała wartości powyżej 255, a następnie odpowiednio przeprowadzi optymalizacje.
źródło
uint_fast8_t
rzeczywistości jest to typ 8-bitowy (np.unsigned char
), Tak jak na x86 / ARM / MIPS / PPC ( godbolt.org/g/KNyc31 ). We wczesnej wersji DEC Alpha przed 21164A ładowanie / przechowywanie bajtów nie było obsługiwane, więc każda rozsądna implementacja byłaby używanatypedef uint32_t uint_fast8_t
. AFAIK, nie ma mechanizmu dla typu, który miałby dodatkowe ograniczenia zakresu w większości kompilatorów (takich jak gcc), więc jestem prawie pewien,uint_fast8_t
że zachowywałby się dokładnie tak samo jakunsigned int
lub cokolwiek w tym przypadku.bool
jest specjalny i jest ograniczony do zakresu do 0 lub 1, ale jest to typ wbudowany, niezdefiniowany przez pliki nagłówkowe w zakresiechar
gcc / clang. Jak powiedziałem, nie sądzę, aby większość kompilatorów miała mechanizm to by to umożliwiło.)uint_fast8_t
jest to dobra rekomendacja, ponieważ będzie używać typu 8-bitowego na platformach, na których jest to tak wydajne jakunsigned int
. (Jestem naprawdę nie wiesz, co tofast
typy mają być szybko do i czy cache ślad kompromis ma być jego częścią.). x86 ma szerokie wsparcie dla operacji bajtowych, nawet do dodawania bajtów ze źródłem pamięci, więc nie musisz nawet wykonywać oddzielnego obciążenia rozszerzającego zero (co jest również bardzo tanie). gcc tworzyuint_fast16_t
typ 64-bitowy na x86, co jest szalone w większości zastosowań (w porównaniu z 32-bitowym). godbolt.org/g/Rmq5bv .Obecna odpowiedź jest dobra w przypadku, gdy wiesz na pewno jaki jest zakres, ale jeśli nadal chcesz poprawnego zachowania, gdy wartość jest poza oczekiwanym zakresem, to nie zadziała.
W tym przypadku odkryłem, że ta technika może działać:
Pomysł polega na kompromisie między kodem a danymi: przenosisz 1 bit danych (czy
x == c
) do logiki sterowania .Wskazuje to optymalizatorowi, który
x
w rzeczywistości jest znaną stałąc
, zachęcając go do wbudowania i optymalizacji pierwszego wywołania funkcjifoo
oddzielnie od reszty, prawdopodobnie dość mocno.Upewnij się, że kod został faktycznie uwzględniony w pojedynczej procedurze
foo
- nie duplikuj kodu.Przykład:
Aby ta technika zadziałała, musisz mieć trochę szczęścia - są przypadki, w których kompilator decyduje się nie oceniać rzeczy statycznie i są one w pewnym sensie arbitralne. Ale kiedy to działa, działa dobrze:
Wystarczy użyć
-O3
i zauważ wstępnie oceniano stałe0x20
i0x30e
na wyjściu asemblera .źródło
if (x==c) foo(c) else foo(x)
? Gdyby tylko złapaćconstexpr
implementacjefoo
?constexpr
i nigdy nie zadałem sobie trudu, aby ją później "zaktualizować" (chociaż tak naprawdę nie przejmowałem się tym,constexpr
nawet później), ale powodem, dla którego nie zrobiłem tego na początku, było to, że chciałem ułatwić kompilatorowi rozłożenie ich na czynniki pierwsze jako wspólny kod i usunięcie gałęzi, jeśli zdecydował się pozostawić je jako zwykłe wywołania metod i nie optymalizować. Spodziewałem się, że jeślic
wstawię, kompilatorowi trudno będzie c (przepraszam, kiepski żart), że oba są tym samym kodem, chociaż nigdy tego nie zweryfikowałem.Chcę tylko powiedzieć, że jeśli potrzebujesz rozwiązania, które jest bardziej standardowe w C ++, możesz użyć
[[noreturn]]
atrybutu, aby napisać własneunreachable
.Dlatego ponownie wykorzystam doskonały przykład Denissa, aby zademonstrować:
Co, jak widać , daje prawie identyczny kod:
Wadą jest oczywiście to, że otrzymujesz ostrzeżenie, że
[[noreturn]]
funkcja rzeczywiście zwraca.źródło
clang
, gdy moje oryginalne rozwiązanie nie działa , tak fajna sztuczka i +1. Ale całość jest bardzo zależna od kompilatora (jak pokazał nam Peter Cordes, wicc
tym może pogorszyć wydajność), więc nadal nie ma uniwersalnego zastosowania. Ponadto drobna uwaga: aby optymalizator działał,unreachable
definicja musi być dostępna i wbudowana .