Czy mogę wskazać optymalizatorowi, podając zakres liczby całkowitej?


173

Używam inttypu 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 intjest to zawsze w tak małym zakresie i czy kompilator może wykonać te optymalizacje?


4
Optymalizacje zakresu wartości istnieją w wielu kompilatorach, np. llvm, ale nie znam żadnej wskazówki językowej, aby to zadeklarować.
Remus Rusanu

2
Zauważ, że jeśli nigdy nie masz liczb ujemnych, możesz mieć niewielkie korzyści z używania unsignedtypów, ponieważ są one łatwiejsze do zrozumienia dla kompilatora.
user694733

4
@RemusRusanu: Pascal umożliwia definiowanie typów podzakresów , np var value: 0..36;.
Edgar Bonet

7
int (nie znak) jest używany tylko ze względu na wydajność procesora. Ta stara tradycyjna mądrość zwykle nie jest prawdziwa. Wąskie typy czasami wymagają rozszerzenia zerowego lub znaku do pełnej szerokości rejestru, zwł. gdy są używane jako indeksy tablic, ale czasami dzieje się to za darmo. Jeśli masz tablicę tego typu, zmniejszenie rozmiaru pamięci podręcznej zwykle przeważa nad wszystkim innym.
Peter Cordes

1
Zapomniałem powiedzieć: inti unsigned inttrzeba 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.
Peter Cordes

Odpowiedzi:


230

Tak to mozliwe. Na przykład, gccmożesz użyć, __builtin_unreachableaby powiedzieć kompilatorowi o niemożliwych warunkach, na przykład:

if (value < 0 || value > 36) __builtin_unreachable();

Powyższy warunek możemy zawinąć w makro:

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)

I używaj go w ten sposób:

assume(x >= 0 && x <= 10);

Jak widać ,gcc przeprowadza optymalizacje na podstawie tych informacji:

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}

Produkuje:

func(int):
    mov     eax, 17
    ret

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:

#if defined(NDEBUG)
#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)
#else
#include <cassert>
#define assume(cond) assert(cond)
#endif

W debug buduje (z NDEBUG nie określono), to działa jak zwykły assert, 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- condpozostaje w kompilacjach wydań, więc nie powinieneś robić czegoś takiego assume(VeryExpensiveComputation()).


5
@Xofo, nie rozumiem, w moim przykładzie to już się dzieje, ponieważ return 2gałąź usunięta z kodu przez kompilator.

6
Jednak wydaje się, że gcc nie może optymalizować funkcji do magicznych operacji lub przeszukiwania tabeli, zgodnie z oczekiwaniami OP.
jingyu9575

19
@ user3528438, nie __builtin_expectjest ścisłą wskazówką. __builtin_expect(e, c)powinno być odczytywane jako „ enajprawdopodobniej oceni do c” i może być przydatne do optymalizacji przewidywania gałęzi, ale nie ogranicza się edo tego c, aby zawsze być , więc nie pozwala optymalizatorowi na odrzucenie innych przypadków. Zobacz, jak gałęzie są zorganizowane w zespole .

6
W teorii każdy kod, który bezwarunkowo powoduje niezdefiniowane zachowanie, mógłby zostać użyty zamiast __builtin_unreachable().
CodesInChaos

14
Chyba że istnieje jakiś dziwactwo nie wiem o tym sprawia, że to zły pomysł, to może mieć sens, aby połączyć to z assert, na przykład określić assume, jak assertgdy NDEBUGnie jest określony, jak i __builtin_unreachable()gdy NDEBUGjest 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.
David Z

61

Jest to standardowe wsparcie. Powinieneś zrobić to include stdint.h( cstdint), a następnie użyć type uint_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.


2
Te typy nie są używane prawie tak często, jak powinny (osobiście często zapominam, że istnieją). Dają kod, który jest szybki i przenośny, całkiem genialny. I istnieją od 1999 r.
Lundin

To dobra propozycja dla ogólnego przypadku. Odpowiedź Denissa pokazuje bardziej plastyczne rozwiązanie dla określonych scenariuszy.
Wyścigi lekkości na orbicie

1
Kompilator pobiera informacje o zakresie 0-255 tylko w systemach, w których w uint_fast8_trzeczywistoś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żywana typedef 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.
Peter Cordes

( booljest specjalny i jest ograniczony do zakresu do 0 lub 1, ale jest to typ wbudowany, niezdefiniowany przez pliki nagłówkowe w zakresie chargcc / clang. Jak powiedziałem, nie sądzę, aby większość kompilatorów miała mechanizm to by to umożliwiło.)
Peter Cordes

1
W każdym razie uint_fast8_tjest to dobra rekomendacja, ponieważ będzie używać typu 8-bitowego na platformach, na których jest to tak wydajne jak unsigned int. (Jestem naprawdę nie wiesz, co to fasttypy 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 tworzy uint_fast16_ttyp 64-bitowy na x86, co jest szalone w większości zastosowań (w porównaniu z 32-bitowym). godbolt.org/g/Rmq5bv .
Peter Cordes

8

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ć:

if (x == c)  // assume c is a constant
{
    foo(x);
}
else
{
    foo(x);
}

Pomysł polega na kompromisie między kodem a danymi: przenosisz 1 bit danych (czy x == c) do logiki sterowania .
Wskazuje to optymalizatorowi, który xw 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:

#include <math.h>
#include <stdio.h>

unsigned foo(unsigned x)
{
    return x * (x + 1);
}

unsigned bar(unsigned x) { return foo(x + 1) + foo(2 * x); }

int main()
{
    unsigned x;
    scanf("%u", &x);
    unsigned r;
    if (x == 1)
    {
        r = bar(bar(x));
    }
    else if (x == 0)
    {
        r = bar(bar(x));
    }
    else
    {
        r = bar(x + 1);
    }
    printf("%#x\n", r);
}

Wystarczy użyć -O3i zauważ wstępnie oceniano stałe 0x20i 0x30ena wyjściu asemblera .


Nie chciałbyś if (x==c) foo(c) else foo(x)? Gdyby tylko złapać constexprimplementacje foo?
MSalters

@MSalters: Wiedziałem, że ktoś o to zapyta !! Wymyśliłem tę technikę wcześniej constexpri nigdy nie zadałem sobie trudu, aby ją później "zaktualizować" (chociaż tak naprawdę nie przejmowałem się tym, constexprnawet 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śli cwstawię, kompilatorowi trudno będzie c (przepraszam, kiepski żart), że oba są tym samym kodem, chociaż nigdy tego nie zweryfikowałem.
user541686

4

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łasne unreachable.

Dlatego ponownie wykorzystam doskonały przykład Denissa, aby zademonstrować:

namespace detail {
    [[noreturn]] void unreachable(){}
}

#define assume(cond) do { if (!(cond)) detail::unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}

Co, jak widać , daje prawie identyczny kod:

detail::unreachable():
        rep ret
func(int):
        movl    $17, %eax
        ret

Wadą jest oczywiście to, że otrzymujesz ostrzeżenie, że [[noreturn]]funkcja rzeczywiście zwraca.


Działa 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, w icctym 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 .
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.