Co jest szybsze: if (bool) czy if (int)?

94

Której wartości lepiej użyć? Wartość logiczna prawda czy liczba całkowita 1?

Powyższy temat mnie zrobić kilka eksperymentów z booli intw ifstanie. Tak więc z ciekawości napisałem ten program:

int f(int i) 
{
    if ( i ) return 99;   //if(int)
    else  return -99;
}
int g(bool b)
{
    if ( b ) return 99;   //if(bool)
    else  return -99;
}
int main(){}

g++ intbool.cpp -S generuje kod asm dla każdej funkcji w następujący sposób:

  • kod asm dla f(int)

    __Z1fi:
       LFB0:
             pushl  %ebp
       LCFI0:
              movl  %esp, %ebp
       LCFI1:
              cmpl  $0, 8(%ebp)
              je    L2
              movl  $99, %eax
              jmp   L3
       L2:
              movl  $-99, %eax
       L3:
              leave
       LCFI2:
              ret
    
  • kod asm dla g(bool)

    __Z1gb:
       LFB1:
              pushl %ebp
       LCFI3:
              movl  %esp, %ebp
       LCFI4:
              subl  $4, %esp
       LCFI5:
              movl  8(%ebp), %eax
              movb  %al, -4(%ebp)
              cmpb  $0, -4(%ebp)
              je    L5
              movl  $99, %eax
              jmp   L6
       L5:
              movl  $-99, %eax
       L6:
              leave
       LCFI6:
              ret
    

O dziwo, g(bool)generuje więcej asminstrukcji! Czy to znaczy, że if(bool)jest trochę wolniejsze niż if(int)? Kiedyś uważałem, że booljest specjalnie zaprojektowany do użycia w instrukcjach warunkowych, takich jak if, więc spodziewałem g(bool)się wygenerować mniej instrukcji asm, dzięki czemu będę g(bool)bardziej wydajny i szybki.

EDYTOWAĆ:

Na razie nie używam żadnej flagi optymalizacji. Ale nawet jego brak, dlaczego generuje więcej asm, g(bool)jest pytaniem, na które szukam rozsądnej odpowiedzi. Powinienem też powiedzieć, że -O2flaga optymalizacji generuje dokładnie to samo co m. Ale to nie jest pytanie. Pytanie brzmi, o co zadałem.


Nawaz
źródło
32
Jest to również niesprawiedliwy test, chyba że porównasz je z włączonymi rozsądnymi optymalizacjami.
Daniel Pryden,
9
@Daniel: Nie używam żadnych flag optymalizacji z żadnym z nich. Ale nawet jego brak, dlaczego generuje więcej asm, g(bool)jest pytaniem, na które szukam rozsądnej odpowiedzi.
Nawaz,
8
Dlaczego miałbyś zadawać sobie trud czytania asm, ale nie tylko uruchamiając program i mierząc wynik w czasie ? Liczba instrukcji niewiele mówi o wydajności. Musisz wziąć pod uwagę nie tylko długość instrukcji, ale także zależności i typy instrukcji (czy niektóre z nich są dekodowane przy użyciu wolniejszej ścieżki mikrokodu, jakich jednostek wykonawczych wymagają, jakie jest opóźnienie i przepustowość instrukcji, czy jest to Oddział? Dostęp do
pamięci
2
@user nieznany i @Malvolio: To jest oczywiście; Nie robię tego wszystkiego dla kodu produkcyjnego. Jak już wspomniałem na początku mojego wpisu, że "Tak więc z ciekawości napisałem ten program" . Więc tak, to czysto hipotetyczny .
Nawaz
3
To uzasadnione pytanie. Są równoważne lub szybsze. ASM został prawdopodobnie opublikowany, aby być pomocnym lub głośno myśleć, więc zamiast używać go jako sposobu na uniknięcie pytania i powiedzieć „po prostu napisz czytelny kod”, po prostu odpowiedz na pytanie lub STFU, jeśli nie wiesz lub nie mam nic pożytecznego do powiedzenia;) Mój wkład jest taki, że na pytanie można odpowiedzieć, a „po prostu napisz czytelny kod” to nic innego jak unikanie pytania.
Triynko

Odpowiedzi:

99

Dla mnie to ma sens. Twój kompilator najwyraźniej definiuje wartość a booljako wartość 8-bitową, a systemowy ABI wymaga, aby „promował” małe (<32-bitowe) argumenty liczb całkowitych do 32-bitowych, gdy są one umieszczane na stosie wywołań. Aby porównać a bool, kompilator generuje kod, aby wyodrębnić najmniej znaczący bajt z 32-bitowego argumentu, który otrzymuje g, i porównuje go z cmpb. W pierwszym przykładzie intargument używa pełnych 32 bitów, które zostały odłożone na stos, więc po prostu porównuje go z całością zcmpl .

Sherm Pendley
źródło
4
Zgadzam się. Pomaga to wyjaśnić, że wybierając typ zmienny, wybierasz go ze względu na dwa potencjalnie konkurencyjne cele, przestrzeń dyskową a wydajność obliczeniową.
Triynko
3
Czy dotyczy to również procesów 64-bitowych, czyli __int64szybszych niż int? A może procesor obsługuje osobno 32-bitowe liczby całkowite z 32-bitowymi zestawami instrukcji?
Crend King
1
@CrendKing może warto rzucić kolejne pytanie?
Nazwa wyświetlana
81

Kompilowanie z -03 daje mi:

fa:

    pushl   %ebp
    movl    %esp, %ebp
    cmpl    $1, 8(%ebp)
    popl    %ebp
    sbbl    %eax, %eax
    andb    $58, %al
    addl    $99, %eax
    ret

sol:

    pushl   %ebp
    movl    %esp, %ebp
    cmpb    $1, 8(%ebp)
    popl    %ebp
    sbbl    %eax, %eax
    andb    $58, %al
    addl    $99, %eax
    ret

.. tak zestawia się zasadniczo ten sam kod, za wyjątkiem cmplvs cmpb. Oznacza to, że różnica, jeśli istnieje, nie ma znaczenia. Ocenianie po niezoptymalizowanym kodzie jest niesprawiedliwe.


Edytuj, aby wyjaśnić mój punkt widzenia. Niezoptymalizowany kod służy do prostego debugowania, a nie do szybkości. Porównywanie szybkości niezoptymalizowanego kodu jest bezsensowne.

Alexander Gessler
źródło
8
O ile zgadzam się z twoim wnioskiem, myślę, że pomijasz interesującą część. Dlaczego jest używany cmpldo jednego i cmpbdo drugiego?
jalf
22
@jalf: Ponieważ a boolto jeden bajt, a an intto cztery. Nie sądzę, żeby było coś bardziej wyjątkowego.
CB Bailey
7
Myślę, że inne odpowiedzi zwracały większą uwagę na przyczyny: to dlatego, że omawiana platforma traktuje booljako typ 8-bitowy.
Alexander Gessler,
9
@Nathan: Nie. C ++ nie ma bitowych typów danych. Najmniejszy typ to char, który z definicji jest bajtem i jest najmniejszą adresowalną jednostką. boolRozmiar jest zdefiniowany w implementacji i może wynosić 1, 4 lub 8 lub cokolwiek. Jednak kompilatory mają tendencję do tego.
GManNickG,
6
@Nathan: Cóż, w Javie też jest to trudne. Java twierdzi, że dane reprezentowane przez wartość logiczną mają wartość jednego bitu, ale sposób przechowywania tego bitu jest nadal zdefiniowany w implementacji. Komputery pragmatyczne po prostu nie adresują bitów.
GManNickG,
26

Kiedy kompiluję to z rozsądnym zestawem opcji (konkretnie -O3), oto co otrzymuję:

Dla f():

        .type   _Z1fi, @function
_Z1fi:
.LFB0:
        .cfi_startproc
        .cfi_personality 0x3,__gxx_personality_v0
        cmpl    $1, %edi
        sbbl    %eax, %eax
        andb    $58, %al
        addl    $99, %eax
        ret
        .cfi_endproc

Dla g():

        .type   _Z1gb, @function
_Z1gb:
.LFB1:
        .cfi_startproc
        .cfi_personality 0x3,__gxx_personality_v0
        cmpb    $1, %dil
        sbbl    %eax, %eax
        andb    $58, %al
        addl    $99, %eax
        ret
        .cfi_endproc

Nadal używają innych instrukcji do porównania ( cmpbdla wartości logicznych vs. cmpldla int), ale poza tym ciała są identyczne. Szybkie spojrzenie na podręczniki Intela mówi mi: ... niewiele. Nie ma czegoś takiego jak cmpblub cmplw podręcznikach Intela. To wszystko cmpi nie mogę w tej chwili znaleźć tabel czasowych. Domyślam się jednak, że nie ma różnicy w zegarze między porównaniem bajtu natychmiastowego i długiego natychmiastowego, więc ze wszystkich praktycznych powodów kod jest identyczny.


edytowane, aby dodać następujące na podstawie dodanego tekstu

Powodem, dla którego kod jest inny w niezoptymalizowanym przypadku, jest to, że jest niezoptymalizowany. (Tak, to jest cykliczne, wiem.) Kiedy kompilator przechodzi przez AST i bezpośrednio generuje kod, nie „wie” niczego poza tym, co jest w bezpośrednim punkcie AST, w którym się znajduje. W tym momencie brakuje mu wszystkich potrzebnych informacji kontekstowych aby wiedzieć, że w tym konkretnym momencie może traktować zadeklarowany typ booljakoint . Wartość logiczna jest oczywiście domyślnie traktowana jako bajt i podczas manipulowania bajtami w świecie Intela musisz zrobić takie rzeczy, jak rozszerzenie znaku, aby uzyskać określone szerokości, aby umieścić je na stosie itp. (Nie możesz przesunąć bajtu .)

Kiedy jednak optymalizator ogląda AST i wykonuje swoją magię, patrzy na otaczający kontekst i „wie”, kiedy może zastąpić kod czymś bardziej wydajnym bez zmiany semantyki. Więc „wie”, że może użyć liczby całkowitej w parametrze i tym samym stracić niepotrzebne konwersje i poszerzenie.

TYLKO MOJA poprawna OPINIA
źródło
1
Haha, podoba mi się, jak kompilator po prostu zwrócił 99, czyli 99 + 58 = 157 = -99 (przepełnienie 8 bitów ze znakiem) ... ciekawe.
zwolnił
@Daniel: Nawet mi się to podobało. Na początku powiedziałem „gdzie jest -99” i od razu zdałem sobie sprawę, że robi coś bardzo perwersyjnego.
Nawaz,
7
li bsą sufiksami używanymi tylko w składni AT&T. Po prostu odnoszą się do wersji cmpużywających odpowiednio 4-bajtowych (long) i 1-bajtowych (bajtowych) operandów. W przypadku, gdy istnieje jakiekolwiek niejasności składni Intel, Tradycyjnie argumentu pamięci jest oznaczony BYTE PTR, WORD PTRlub DWORD PTRzamiast zawieszania sufiks kodu maszynowego.
CB Bailey,
Tabele czasu: agner.org/optimize Oba rozmiary operandów cmpmają taką samą wydajność i nie ma kar za częściowy rejestr do odczytu %dil . (Ale to nie przeszkadza w zabawnym tworzeniu przeciągnięcia częściowego rejestru przy użyciu rozmiaru bajtów andna AL jako części bez rozgałęzienia przełączania wielkości liter między 99 a -99.)
Peter Cordes
13

GCC 4.5 na Linux i Windows co najmniej, sizeof(bool) == 1. Na x86 i x86_64 nie możesz przekazać funkcji mniejszej niż wartość rejestru ogólnego przeznaczenia (czy to przez stos, czy przez rejestr w zależności od konwencji wywoływania itp.).

Więc kod bool, gdy nie jest zoptymalizowany, w rzeczywistości idzie do pewnej długości, aby wyodrębnić tę wartość bool ze stosu argumentów (używając innego miejsca stosu do zapisania tego bajtu). Jest to bardziej skomplikowane niż zwykłe pobieranie natywnej zmiennej wielkości rejestru.

Mata
źródło
Ze standardu C ++ 03, §5.3.3 / 1: „ sizeof(bool)i sizeof(wchar_t)są zdefiniowane w ramach implementacji. To powiedzenie sizeof(bool) == 1nie jest do końca poprawne, chyba że mówisz o konkretnej wersji konkretnego kompilatora.
ildjarn,
9

Na poziomie maszyny nie ma czegoś takiego jak bool

Bardzo niewiele architektur zestawów instrukcji definiuje dowolny rodzaj argumentu logicznego, chociaż często istnieją instrukcje, które wyzwalają akcję na wartościach niezerowych. Dla procesora zwykle wszystko jest jednym z typów skalarnych lub ich ciągiem.

Dany kompilator i dany ABI będą musieli wybrać określone rozmiary dla inti boolkiedy, tak jak w Twoim przypadku, są to różne rozmiary, mogą generować nieco inny kod, a na niektórych poziomach optymalizacji jeden może być nieco szybszy.

Dlaczego w wielu systemach wartość logiczna to jeden bajt?

Bezpieczniej jest wybrać chartyp bool, ponieważ ktoś mógłby stworzyć naprawdę dużą ich tablicę.

Aktualizacja: przez „bezpieczniejsze” rozumiem: dla kompilatorów i implementatorów bibliotek. Nie mówię, że ludzie muszą ponownie zaimplementować typ systemu.

DigitalRoss
źródło
2
+1 Wyobraź sobie, że narzut na x86 byłby boolreprezentowany przez bity; więc bajt będzie dobrym kompromisem dla szybkości / zwartości danych w wielu implementacjach.
hardmath
1
@Billy: Myślę, że nie mówił „użyj charzamiast bool”, ale zamiast tego użył „ chartyp”, aby oznaczać „1 bajt”, odnosząc się do rozmiaru wybranego przez kompilator dla boolobiektów.
Dennis Zickefoose
Och, jasne, nie miałem na myśli tego, że każdy program powinien wybierać, po prostu przedstawiłem uzasadnienie, dlaczego typ bool systemu ma 1 bajt.
DigitalRoss
@Dennis: Ach, to ma sens.
Billy ONeal
7

Tak, dyskusja jest fajna. Ale po prostu to przetestuj:

Kod testowy:

#include <stdio.h>
#include <string.h>

int testi(int);
int testb(bool);
int main (int argc, char* argv[]){
  bool valb;
  int  vali;
  int loops;
  if( argc < 2 ){
    return 2;
  }
  valb = (0 != (strcmp(argv[1], "0")));
  vali = strcmp(argv[1], "0");
  printf("Arg1: %s\n", argv[1]);
  printf("BArg1: %i\n", valb ? 1 : 0);
  printf("IArg1: %i\n", vali);
  for(loops=30000000; loops>0; loops--){
    //printf("%i: %i\n", loops, testb(valb=!valb));
    printf("%i: %i\n", loops, testi(vali=!vali));
  }
  return valb;
}

int testi(int val){
  if( val ){
    return 1;
  }
  return 0;
}
int testb(bool val){
  if( val ){
    return 1;
  }
  return 0;
}

Skompilowany na 64-bitowym laptopie Ubuntu 10.10 z: g ++ -O3 -o / tmp / test_i /tmp/test_i.cpp

Porównanie na podstawie liczb całkowitych:

sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m8.203s
user    0m8.170s
sys 0m0.010s
sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m8.056s
user    0m8.020s
sys 0m0.000s
sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m8.116s
user    0m8.100s
sys 0m0.000s

Boolean test / print bez komentarzy (i liczba całkowita z komentarzem):

sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m8.254s
user    0m8.240s
sys 0m0.000s
sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m8.028s
user    0m8.000s
sys 0m0.010s
sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m7.981s
user    0m7.900s
sys 0m0.050s

Są takie same z 1 przypisaniem i 2 porównaniami w każdej pętli ponad 30 milionów pętli. Znajdź coś innego do optymalizacji. Na przykład nie używaj niepotrzebnie strcmp. ;)

dannysauer
źródło
0

Podchodząc do pytania na dwa różne sposoby:

Jeśli mówisz konkretnie o C ++ lub jakimkolwiek języku programowania, który będzie generował kod asemblera w tym zakresie, jesteśmy zobowiązani do tego, jaki kod wygeneruje kompilator w ASM. Jesteśmy również zobowiązani do reprezentacji prawdy i fałszu w języku c ++. Liczba całkowita będzie musiała być zapisana w 32 bitach, a ja mógłbym po prostu użyć bajtu do przechowywania wyrażenia logicznego. Fragmenty ASM dla instrukcji warunkowych:

Dla liczby całkowitej:

  mov eax,dword ptr[esp]    ;Store integer
  cmp eax,0                 ;Compare to 0
  je  false                 ;If int is 0, its false
  ;Do what has to be done when true
false:
  ;Do what has to be done when false

Dla bool:

  mov  al,1     ;Anything that is not 0 is true
  test al,1     ;See if first bit is fliped
  jz   false    ;Not fliped, so it's false
  ;Do what has to be done when true
false:
  ;Do what has to be done when false

Dlatego właśnie porównanie szybkości jest tak zależne od kompilacji. W powyższym przypadku wartość bool byłaby nieco szybka, ponieważ cmpwymagałaby odejmowania przy ustawianiu flag. Jest to również sprzeczne z tym, co wygenerował Twój kompilator.

Innym podejściem, o wiele prostszym, jest przyjrzenie się logice samego wyrażenia i próba nie martwienia się o to, jak kompilator przetłumaczy twój kod. Myślę, że jest to znacznie zdrowszy sposób myślenia. Ostatecznie nadal wierzę, że kod generowany przez kompilator w rzeczywistości próbuje dać prawdziwe rozwiązanie. Chodzi mi o to, że być może, jeśli zwiększysz liczbę przypadków testowych w instrukcji if i będziesz trzymać się wartości logicznej po jednej stronie i liczby całkowitej po drugiej, kompilator sprawi, że wygenerowany kod będzie wykonywał się szybciej z wyrażeniami boolowskimi na poziomie maszyny.

Uważam, że jest to pytanie koncepcyjne, więc podam koncepcyjną odpowiedź. Ta dyskusja przypomina mi dyskusje, które często prowadzę na temat tego, czy wydajność kodu przekłada się na mniej wierszy kodu w asemblerze. Wydaje się, że ta koncepcja jest powszechnie akceptowana jako prawdziwa. Biorąc pod uwagę, że śledzenie, jak szybko jednostka ALU będzie obsługiwać każdą instrukcję, nie jest wykonalne, drugą opcją byłoby skupienie się na skokach i porównaniach w asemblerze. W takim przypadku rozróżnienie między instrukcjami logicznymi lub liczbami całkowitymi w przedstawionym kodzie staje się raczej reprezentatywne. Wynik wyrażenia w C ++ zwróci wartość, która następnie otrzyma reprezentację. Z drugiej strony podczas montażu skoki i porównania będą oparte na wartościach liczbowych, niezależnie od tego, jaki typ wyrażenia był oceniany z powrotem w instrukcji C ++ if. W tych pytaniach ważne jest, aby pamiętać, że czysto logiczne stwierdzenia, takie jak te, kończą się ogromnym narzutem obliczeniowym, nawet jeśli jeden bit byłby zdolny do tego samego.

Artie
źródło