2010-01-11 15 views
13

Oto dwa sposoby ustawienia indywidualnego bitu w C na x86-64:Skuteczność optmization GCC na operacjach bitowych

inline void SetBitC(long *array, int bit) { 
    //Pure C version 
    *array |= 1<<bit; 
} 

inline void SetBitASM(long *array, int bit) { 
    // Using inline x86 assembly 
    asm("bts %1,%0" : "+r" (*array) : "g" (bit)); 
} 

Korzystanie GCC 4.3 z -O3 -march=core2 opcji, wersja C trwa około 90% więcej czasu w przypadku używania ze stałą bit. (Obie wersje kompilacji dokładnie ten sam kod montażu, z tym, że wersja C, wykorzystuje polecenie or [1<<num],%rax zamiast instrukcji bts [num],%rax)

Przy zastosowaniu ze zmienną bit wersja C działa lepiej ale wciąż znacznie mniejsza niż inline montaż.

Resetowanie, przełączanie i sprawdzanie bitów ma podobne wyniki.

Dlaczego GCC jest tak źle zoptymalizowana w przypadku tak powszechnej operacji? Czy robię coś nie tak z wersją C?

Edytuj: Przepraszam za długie czekanie, oto kod, którego użyłem do testu porównawczego. To faktycznie rozpoczął jako prostego problemu programowania ...

int main() { 
    // Get the sum of all integers from 1 to 2^28 with bit 11 always set 
    unsigned long i,j,c=0; 
    for (i=1; i<(1<<28); i++) { 
     j = i; 
     SetBit(&j, 10); 
     c += j; 
    } 
    printf("Result: %lu\n", c); 
    return 0; 
} 

gcc -O3 -march=core2 -pg test.c 
./a.out 
gprof 
with ASM: 101.12  0.08  0.08        main 
with C: 101.12  0.16  0.16        main 

time ./a.out daje również podobne wyniki.

+1

Czy to naprawdę "wspólna operacja"? Najczęstszym czasem, jaki widzę w manipulacji bitowej, jest przedwczesna optymalizacja przez ludzi, którzy myślą, że zaoszczędzą pamięć, pakując kilka flag w jeden bajt. –

+2

Hmm ... dobry punkt. Mimo to jest to dość prosta operacja i jest powszechna w sterownikach sprzętowych. W każdym razie, chodzi o zmniejszenie wydajności o 90%. –

+1

W programowaniu sterowników jestem prawie pewien, że idiom "lewa zmiana 1 do określenia naszej maski" nie jest używany. Raczej "lub" z predefiniowaną flagą. –

Odpowiedz

0

Myślę, że zadajesz dużo swojego optymalizatora.

Możesz być w stanie pomóc go trochę wykonując `zarejestrować długi z = 1L < < nieco;.”, A następnie albo-ing, że ze swojej tablicy

jednak zakładam, że o 90% więcej czasu, masz na myśli, że wersja C zajmuje 10 cykli, a wersja asma ma 5 cykli, prawda? Jak działa porównanie z -O2 lub -O1?

+0

"rejestr" zostanie zignorowany przez kompilator. –

+0

@Laurynas Biveinis: "register" może, ale nie musi być zignorowany przez kompilator, jest to podpowiedź, w końcu – Hasturkun

+0

Pomysł polegał na tym, że wykonanie 'long | = register long' może zachęcić kompilator do optymalizacji :) – Seth

2

Czy możesz umieścić kod, którego używasz do Zrobić taktowanie? Tego rodzaju operacja może być trudna do dokładnego czasu:

Teoretycznie dwie sekwencje kodu powinny być jednakowo szybko, więc najbardziej prawdopodobnym wyjaśnieniem (moim zdaniem) jest to, że coś powoduje, że twój kod czasu daje fałszywe wyniki.

+0

Tak. Na wszystkich procesorach x86, o których wiem, podstawowa operacja ALU, taka jak "OR", jest co najmniej tak szybka jak "egzotyczne" operacje takie jak "BTS". Jedną z możliwości jest to, że test inkrementacji + w pętli czasowej używa tej samej jednostki wykonawczej CPU co 'OR', co prowadzi do rywalizacji, podczas gdy' BTS' używa innej jednostki. –

+0

Na najnowszym sprzęcie x86, 'lub' może być wysłane do więcej niż jednej jednostki wykonawczej, więc nie powinno tam być żadnych rywalizacji. –

+0

Wysłano - dziękuję za pomoc. –

13

Dlaczego GCC jest tak źle zoptymalizowana dla tak powszechnej operacji?

Prelude: Od końca 1980 roku, koncentrują się na optymalizacji kompilatora został odsunięty od microbenchmarks które koncentrują się na poszczególnych operacji i ku macrobenchmarks które koncentrują się na aplikacjach, których prędkość ludzie obchodzą. W dzisiejszych czasach większość twórców kompilacji skupia się na makrobenchmarkach, a rozwijanie dobrych zestawów benchmarków jest traktowane poważnie.

Odpowiedź: Nikt na gcc używa odniesienia gdzie różnica między or i bts sprawach do czasu realizacji prawdziwego programu. Jeśli możesz wyprodukować taki program, możesz przyciągnąć uwagę ludzi w gcc-land.

Czy robię coś nie tak z wersją C?

Nie, to jest idealnie dobry standard C. Rzeczywiście bardzo czytelny i idiomatyczny.

2

Dla takiego kodu:

#include <stdio.h> 
#include <time.h> 

int main() { 
    volatile long long i = 0; 
    time_t start = time (NULL); 
    for (long long n = 0; n < (1LL << 32); n++) { 
    i |= 1 << 10; 
    } 
    time_t end = time (NULL); 
    printf("C took %ds\n", (int)(end - start)); 
    start = time (NULL); 
    for (long long n = 0; n < (1LL << 32); n++) { 
    __asm__ ("bts %[bit], %[i]" 
        : [i] "=r"(i) 
        : "[i]"(i), [bit] "i" (10)); 
    } 
    end = time (NULL); 
    printf("ASM took %ds\n", (int)(end - start)); 
} 

wynik był:

C took 12s 
ASM took 10s 

Moja flaga była (-std=gnu99 -O2 -march=core2). Bez lotnej pętli została zoptymalizowana. gcc 4.4.2.

No różnica była z:

__asm__ ("bts %[bit], %[i]" 
       : [i] "+m"(i) 
       : [bit] "r" (10)); 

Więc prawdopodobnie odpowiedź była - nikt nie dba. W microbenchmark jedyną różnicą jest ta między tymi dwiema metodami, ale w prawdziwym życiu uważam, że taki kod nie wymaga dużo procesora.

Dodatkowo do tego kodu:

#include <stdio.h> 
#include <time.h> 

int main() { 
    volatile long long i = 0; 
    time_t start = time (NULL); 
    for (long long n = 0; n < (1L << 32); n++) { 
    i |= 1 << (n % 32); 
    } 
    time_t end = time (NULL); 
    printf("C took %ds\n", (int)(end - start)); 
    start = time (NULL); 
    for (long long n = 0; n < (1L << 32); n++) { 
    __asm__ ("bts %[bit], %[i]" 
        : [i] "+m"(i) 
        : [bit] "r" (n % 32)); 
    } 
    end = time (NULL); 
    printf("ASM took %ds\n", (int)(end - start)); 
} 

Wynik był następujący:

C took 9s 
ASM took 10s 

Oba wyniki były stabilne. Testowanie procesora "Intel (R) Core ™ 2 Duo CPU T9600 @ 2,80GHz".

+0

Sufiks 'long long' ma wartość' LL', a nie 'L' –

+1

@ LưuVĩnhPhúc good point. Naprawiono, chociaż nie powinno to wpłynąć na wynik, ponieważ Linux jest platformą LP64 (może dotyczyć systemu Windows lub innych platform LLP). –

+0

Patrząc na drugi test, nie wydaje się to być "uczciwym" porównaniem. Zmuszasz 'i' do zapisania w pamięci zamiast do rejestru, którego kod C prawdopodobnie nie robi. Jak o '__asm__ (" bts% [bit],% [i] ": [i]" + r "(i): [bit]" r "(n% 32));'? –

1

Jest to bardzo bardzo powszechna operacja na systemach wbudowanych, które na ogół są ograniczone zasobami. 10 Cycles vs 5 Cycles to nieprzyjemna kara za wydajność w takich systemach. Istnieje wiele przypadków, gdy chcemy uzyskać dostęp do portów IO lub użyć rejestrów 16- lub 32-bitowych jako flag bitowych Boolean w celu zaoszczędzenia pamięci.

Faktem jest, że if(bit_flags& 1<<12) jest dużo bardziej czytelny [i przenośny po wdrożeniu z biblioteką] niż odpowiednik zespołu. Podobnie jak w przypadku IO_PINS|= 1<<5; Są one niestety wielokrotnie wolniejsze, więc nieporęczne makra asm działają dalej.

Pod wieloma względami cele aplikacji wbudowanych i przestrzeni użytkownika są odwrotne. Reaktywność komunikacji zewnętrznej (do interfejsu użytkownika lub interfejsu maszynowego) ma niewielkie znaczenie, a zapewnienie, że pętla kontrolna (np. Do mikro-bechmark) jest realizowana w minimalnym czasie jest bezwzględnie krytyczna i może spowodować lub zepsuć wybrany procesor lub sterowanie strategia.

Oczywiście, jeśli można sobie pozwolić na procesor wielogrzeszczowy i wszystkie powiązane urządzenia peryferyjne, zestawy układów, itp. Potrzebne do obsługi tego, nie trzeba w ogóle martwić się optymalizacją niskiego poziomu. Wolniejszy mikrokontroler o sile 1000x w systemie sterowania w czasie rzeczywistym oznacza, że ​​oszczędzanie cykli zegara jest 1000 razy ważniejsze.

Powiązane problemy