Dlaczego uint32_t miałby być preferowany zamiast uint_fast32_t?

82

Wydaje się, że uint32_tjest to znacznie bardziej rozpowszechnione niż uint_fast32_t(zdaję sobie sprawę, że to niepotwierdzone dowody). Wydaje mi się to jednak sprzeczne z intuicją.

Prawie zawsze, gdy widzę zastosowanie implementacji uint32_t, wszystko, czego naprawdę chce, to liczba całkowita, która może przechowywać wartości do 4 294 967 295 (zwykle znacznie niższa granica między 65 535 a 4 294 967 295).

Wydaje się dziwne, aby następnie wykorzystać uint32_tjako „dokładnie 32 bitów” gwarancja nie jest potrzebna, a „najszybszą dostępną> = 32 bitów” gwarancja uint_fast32_twydają się być dokładnie taki dobry pomysł. Co więcej, chociaż jest zwykle wdrażany, uint32_tnie ma gwarancji, że będzie istniał.

Dlaczego więc miałoby to uint32_tbyć preferowane? Czy jest po prostu lepiej znany, czy też są przewagi techniczne nad drugim?

Joost
źródło
24
Prosta odpowiedź, może potrzebują liczby całkowitej, która ma dokładnie 32 bity?
Stargateur
7
Po pierwsze, o którym słyszałem uint32_fast_t, która, jeśli dobrze rozumiem, ma co najmniej 32 bity (co oznacza, że ​​może być ich więcej? Brzmi to myląco). Obecnie używam uint32_ti przyjaciół w moim projekcie, ponieważ pakuję te dane i wysyłam je przez sieć i chcę, aby nadawca i odbiorca dokładnie wiedzieli, jak duże są pola. Wygląda na to, że to może nie być najsolidniejsze rozwiązanie, ponieważ platforma może nie być implementowana uint32_t, ale najwyraźniej wszystkie moje tak robią, więc nie przeszkadza mi to, co robię.
yano
5
@yano: W przypadku sieci, powinieneś również przejmować się kolejnością bajtów / endianess - uint32_tnie daje ci tego (i szkoda, że ​​nie ma uint32_t_bei uint32_t_le, co byłoby bardziej odpowiednie dla prawie każdego możliwego przypadku, w którym uint32_tjest obecnie najlepsza opcja).
Brendan
3
@Brendan - w odniesieniu do _be i _le, czy htonl () i ntohl () zapewniałyby taką samą możliwość?
mpez0
2
@Brendan to dość ciężki obiekt do ukrycia w standardowym int, z których wszystkie są typami prymitywnymi. Zgadzam się z tobą w zasadzie, że powinno to być załatwione gdzieś w standardzie, ale myślę, że to może nie być miejsce
Steve Cox

Odpowiedzi:

79

uint32_tma prawie takie same właściwości na każdej platformie, która go obsługuje. 1

uint_fast32_t ma bardzo mało gwarancji, jak zachowuje się w różnych systemach w porównaniu.

Jeśli przełączysz się na platformę, która uint_fast32_tma inny rozmiar, cały używany kod uint_fast32_tmusi zostać ponownie przetestowany i zweryfikowany. Wszystkie założenia dotyczące stabilności wyjdą na jaw. Cały system będzie działał inaczej.

Podczas pisania kodu możesz nawet nie mieć dostępu do uint_fast32_tsystemu o rozmiarze innym niż 32 bity.

uint32_t nie będzie działać inaczej (patrz przypis).

Poprawność jest ważniejsza niż szybkość. Dlatego przedwczesna poprawność jest lepszym planem niż przedwczesna optymalizacja.

W przypadku, gdy pisałem kod dla systemów, które uint_fast32_tmiały 64 lub więcej bitów, mogłem przetestować swój kod w obu przypadkach i użyć go. Poza potrzebą i szansą zrobienie tego jest złym planem.

Wreszcie, uint_fast32_tgdy przechowujesz go przez dowolny czas lub liczbę instancji, może to być wolniejsze niż uint32po prostu z powodu problemów z rozmiarem pamięci podręcznej i przepustowością pamięci. Dzisiejsze komputery są znacznie częściej związane z pamięcią niż z procesorem i uint_fast32_tmogą być szybsze w izolacji, ale nie po uwzględnieniu obciążenia pamięci.


1 Jak @chux zauważył w komentarzu, jeśli unsignedjest większe niż uint32_t, arytmetyka uint32_tprzechodzi przez zwykłe promocje w postaci liczb całkowitych, a jeśli nie, pozostaje tak uint32_t. Może to powodować błędy. Nic nigdy nie jest doskonałe.

Yakk - Adam Nevraumont
źródło
15
„uint32_t ma te same właściwości na każdej platformie, która go obsługuje.” Występuje problem narożny, gdy unsignedjest szerszy niż, uint32_ta następnie uint32_tna jednej platformie przechodzi zwykłe promocje na liczby całkowite, a na innej nie. Jednak z uint32_ttymi liczbami całkowitymi problem matematyczny jest znacznie zmniejszony.
chux - Przywróć Monikę
2
@chux przypadek narożny, który może powodować UB podczas mnożenia, ponieważ promocja preferuje wartość int ze znakiem, a przepełnienie liczby całkowitej ze znakiem to UB.
CodesInChaos
2
Chociaż ta odpowiedź jest poprawna w tym zakresie, bardzo bagatelizuje kluczowe szczegóły. W skrócie, uint32_tjest tam, gdzie dokładne szczegóły reprezentacji maszyny danego typu są ważne, podczas gdy uint_fast32_ttam, gdzie najważniejsza jest prędkość obliczeniowa, (nie) podpis i minimalny zasięg są ważne, a szczegóły reprezentacji są nieistotne. Istnieje również miejsce, uint_least32_tw którym najważniejsza jest (nie) sygnalizacja i minimalny zasięg, zwartość jest ważniejsza niż szybkość, a dokładna reprezentacja nie jest niezbędna.
John Bollinger
@JohnBollinger Wszystko w porządku, ale bez testowania na rzeczywistym sprzęcie, który implementuje więcej niż jeden wariant, typy zmiennych rozmiarów są pułapką. Powodem, dla którego ludzie używają uint32_tzamiast innych typów, jest to, że zwykle nie mają takiego sprzętu do testowania . (To samo dotyczy w int32_tmniejszym stopniu, a nawet inti short).
Yakk - Adam Nevraumont
1
Przykład przypadku narożnego: Let unsigned short== uint32_ti int== int48_t. Jeśli obliczasz coś podobnego (uint32_t)0xFFFFFFFF * (uint32_t)0xFFFFFFFF, operandy są promowane do signed inti wywołują przepełnienie ze znakiem całkowitym, co jest niezdefiniowanym zachowaniem. Zobacz to pytanie.
Nayuki
32

Dlaczego wiele osób używa uint32_tzamiast uint32_fast_t?

Uwaga: błędnie nazwane uint32_fast_tpowinno być uint_fast32_t.

uint32_tma bardziej rygorystyczną specyfikację uint_fast32_ti zapewnia bardziej spójną funkcjonalność.


uint32_t plusy:

  • Różne algorytmy określają ten typ. IMO - najlepszy powód do użycia.
  • Dokładna szerokość i znany zasięg.
  • Tablice tego typu nie generują żadnych strat.
  • matematyka liczb całkowitych bez znaku z przepełnieniem jest bardziej przewidywalna.
  • Bliższe dopasowanie w zakresie i matematyce 32-bitowych typów innych języków.
  • Nigdy nie wyściełane.

uint32_t Cons:

  • Nie zawsze dostępne (ale jest to rzadkie w 2018 r.).
    Np .: Platformy bez 8/16/32-bitowych liczb całkowitych (9/18/ 36- bit, inne ).
    Np .: Platformy używające dopełnienia innego niż 2. stary 2200

uint_fast32_t plusy:

  • Zawsze dostępne.
    To zawsze pozwolić wszystkie platformy, nowe i stare, szybko użyć / podstawowe rodzaje.
  • „Najszybszy” typ obsługujący zakres 32-bitowy.

uint_fast32_t Cons:

  • Zasięg jest znany tylko minimalnie. Na przykład może to być typ 64-bitowy.
  • Tablice tego typu mogą marnować pamięć.
  • Wszystkie odpowiedzi (moje też na początku), post i komentarze zawierały niewłaściwą nazwę uint32_fast_t. Wygląda na to, że wielu po prostu nie potrzebuje i nie używa tego typu. Nawet nie użyliśmy właściwej nazwy!
  • Możliwe wypełnienie - (rzadko).
  • W niektórych przypadkach „najszybszym” typem może być naprawdę inny typ. Więc uint_fast32_tto tylko przybliżenie pierwszego rzędu.

Ostatecznie to, co najlepsze, zależy od celu kodowania. Jeśli nie ma kodowania dla bardzo szerokiej przenośności lub jakiejś niszowej funkcji wydajności, użyj uint32_t.


Podczas korzystania z tych typów pojawia się inny problem: ich ranga w porównaniu z int/unsigned

Przypuszczalnie uint_fastN_tmogłaby to być ranga unsigned. Nie jest to określone, ale jest to pewien i sprawdzalny stan.

W związku z tym uintN_tjest bardziej prawdopodobne niż uint_fastN_twęższe rozszerzenie unsigned. Oznacza to, że kod korzystający z uintN_tmatematyki jest bardziej narażony na promowanie liczb całkowitych niż w uint_fastN_tprzypadku przenośności.

W tym względzie: przewaga przenośności uint_fastN_tprzy wybranych operacjach matematycznych.


Uwaga dodatkowa int32_traczej niż int_fast32_t: Na rzadkich komputerach INT_FAST32_MINmoże wynosić -2 147 483 647, a nie -2 147 483 648. Większy punkt: (u)intN_ttypy są ściśle określone i prowadzą do przenośnego kodu.

chux - Przywróć Monikę
źródło
2
Najszybszy typ obsługujący zakres 32-bitowy => naprawdę? To relikt czasu, gdy pamięć RAM działała z szybkością procesora, obecnie równowaga zmieniła się dramatycznie na komputerach PC, więc (1) pobieranie 32-bitowych liczb całkowitych z pamięci jest dwa razy szybsze niż pobieranie 64-bitowych i (2) wektoryzowane instrukcje na 32-bitowych liczbach całkowitych crunch jest dwukrotnie większy niż na 64-bitowych. Czy nadal jest najszybszy?
Matthieu M.
4
Najszybciej w przypadku niektórych rzeczy, wolniej w innych. Nie ma jednej odpowiedzi na pytanie „jaki jest najszybszy rozmiar liczby całkowitej”, jeśli weźmie się pod uwagę tablice, a nie konieczność rozszerzenia zerowego. W x86-64 System V ABI uint32_fast_tjest typem 64-bitowym, więc zapisuje sporadyczne rozszerzenie znaku i pozwala imul rax, [mem]zamiast oddzielnej instrukcji ładowania rozszerzającej zero, gdy jest używana z 64-bitowymi liczbami całkowitymi lub wskaźnikami. Ale to wszystko, co dostajesz za cenę podwójnego rozmiaru pamięci podręcznej i dodatkowego rozmiaru kodu (REX z prefiksem na wszystkim).
Peter Cordes,
1
Ponadto, podział 64-bitowy jest znacznie wolniejszy niż podział 32-bitowy na większości procesorów x86, a niektóre (takie jak rodzina Bulldozer, Atom i Silvermont) mają wolniejsze mnożenie 64-bitów niż 32. Rodzina Bulldozer ma również wolniejsze 64-bitowe popcnt. I pamiętaj, że używanie tego typu jest bezpieczne tylko dla wartości 32-bitowych, ponieważ jest mniejszy na innych architekturach, więc płacisz ten koszt za nic.
Peter Cordes
2
Spodziewałbym się, że jako średnia ważona wszystkich aplikacji C i C ++ robienie uint32_fast_tna x86 jest okropnym wyborem. Operacji, które są szybsze, jest niewiele, a korzyści, gdy się pojawiają, są przeważnie niewielkie: różnice w imul rax, [mem]przypadku, o którym wspomina @PeterCordes, są bardzo , bardzo małe: pojedynczy uop w domenie połączonej i zero w domenie nieużywanej. W najciekawszych scenariuszach nie doda nawet jednego cyklu. Zrównoważyć to w stosunku do dwukrotnego wykorzystania pamięci i gorszej wektoryzacji, ciężko jest zobaczyć, jak często wygrywa.
BeeOnRope
2
@PeterCordes - ciekawe, ale i straszne :). Byłoby fast_tjeszcze gorzej int: nie tylko ma różne rozmiary na różnych platformach, ale miałby różne rozmiary w zależności od decyzji optymalizacyjnych i różne rozmiary w różnych plikach! Z praktycznego punktu widzenia myślę, że nie działa nawet z optymalizacją całego programu: rozmiary w C i C ++ są ustalone, więc sizeof(uint32_fast_t)lub cokolwiek, co decyduje o tym, nawet bezpośrednio, musi zawsze zwracać tę samą wartość, więc kompilatorowi byłoby bardzo trudno dokonać takiej przemiany.
BeeOnRope
25

Dlaczego wiele osób używa uint32_tzamiast uint32_fast_t?

Głupia odpowiedź:

  • Nie ma standardowego typu uint32_fast_t, prawidłowa pisownia to uint_fast32_t.

Praktyczna odpowiedź:

  • Wiele osób faktycznie używa uint32_tlub int32_tdla swojej precyzyjnej semantyki, dokładnie 32 bitów z bez znaku zawijania arytmetycznego ( uint32_t) lub reprezentacji dopełnienia do 2 ( int32_t). Te xxx_fast32_ttypy mogą być większe, a więc nieodpowiednie do przechowywania plików binarnych, zapakowanych w użyciu tablic i struktur, lub wysłać przez sieć. Co więcej, mogą nawet nie być szybsze.

Pragmatyczna odpowiedź:

  • Wiele osób po prostu nie wie (lub po prostu nie przejmuje się) uint_fast32_t, co pokazują komentarze i odpowiedzi, i prawdopodobnie zakłada po prostu, unsigned intże ma tę samą semantykę, chociaż wiele obecnych architektur nadal ma 16-bitowe, inta niektóre rzadkie próbki muzealne mają inne dziwne rozmiary int mniejsze niż 32.

Odpowiedź UX:

  • Chociaż prawdopodobnie szybszy niż uint32_t, uint_fast32_tjest wolniejszy w użyciu: wpisywanie zajmuje więcej czasu, szczególnie biorąc pod uwagę sprawdzanie pisowni i semantyki w dokumentacji C ;-)

Elegancja ma znaczenie (oczywiście oparta na opinii):

  • uint32_twygląda na tyle źle, że wielu programistów woli zdefiniować własny u32lub uint32typ ... Z tej perspektywy uint_fast32_twygląda niezdarnie i nie da się go naprawić. Nic dziwnego, że siedzi na ławce z przyjaciółmi uint_least32_ti tak dalej.
chqrlie
źródło
+1 za UX. To lepsze niż std::reference_wrapperprzypuszczam, ale czasami zastanawiam się, czy komisja standaryzacyjna naprawdę chce, aby używane były typy, które standaryzuje ...
Matthieu M.,
7

Jednym z powodów jest to, że unsigned intjest już „najszybszy” bez potrzeby stosowania specjalnych czcionek typu lub potrzeby dołączania czegoś. Więc jeśli potrzebujesz tego szybko, po prostu użyj podstawowego intlub unsigned inttypu.
Chociaż standard nie gwarantuje wyraźnie, że jest najszybszy, robi to pośrednio , stwierdzając, że „Zwykłe wartości int mają naturalny rozmiar sugerowany przez architekturę środowiska wykonawczego” w 3.9.1. Innymi słowy, int(lub jego niepodpisany odpowiednik) jest tym, z czym procesor jest najbardziej wygodny.

Teraz oczywiście nie wiesz, jaki unsigned intmoże być rozmiar . Wiesz tylko, że jest co najmniej tak duży, jak short(i pamiętam, że shortmusi mieć co najmniej 16 bitów, chociaż nie mogę teraz znaleźć tego w standardzie!). Zwykle jest to po prostu po prostu 4 bajty, ale teoretycznie może być większy, a w skrajnych przypadkach nawet mniejszy ( chociaż osobiście nigdy nie spotkałem się z architekturą, w której tak było, nawet na komputerach 8-bitowych w latach 80. ... może jakiś mikrokontroler, który wie , że mam demencję, intmiał wtedy bardzo wyraźnie 16 bitów).

Standard C ++ nie zadaje sobie trudu, aby określić, jakie <cstdint>typy są ani co gwarantują, po prostu wspomina „to samo co w C”.

uint32_t, zgodnie ze standardem C, gwarantuje uzyskanie dokładnie 32 bitów. Nic innego, nie mniej i żadnych bitów wypełniających. Czasami jest to dokładnie to, czego potrzebujesz, a zatem jest to bardzo cenne.

uint_least32_tgwarantuje, że niezależnie od rozmiaru, nie może być mniejszy niż 32 bity (ale równie dobrze mógłby być większy). Czasami, ale znacznie rzadziej niż dokładny witdh lub „nie obchodzi mnie”, właśnie tego chcesz.

Wreszcie, uint_fast32_tmoim zdaniem , jest nieco zbędny, z wyjątkiem celów związanych z udokumentowaniem intencji. Standard C stwierdza, że „wyznacza typ liczby całkowitej, który jest zwykle najszybszy” (zwróć uwagę na słowo „zwykle”) i wyraźnie stwierdza, że ​​nie musi on być najszybszy do wszystkich celów. Innymi słowy, uint_fast32_tjest prawie taki sam, jak uint_least32_t, który zwykle jest najszybszy, z tą różnicą, że nie ma żadnej gwarancji (ale i tak żadnej gwarancji).

Ponieważ przez większość czasu albo nie dbasz o dokładny rozmiar, albo potrzebujesz dokładnie 32 (lub 64, czasem 16) bitów, a ponieważ unsigned inttyp „nie obchodzi” jest i tak najszybszy, to wyjaśnia, dlaczego uint_fast32_ttak nie jest często używany.

Damon
źródło
3
Dziwię się, że nie pamiętasz 16-bitowego intna 8-bitowych procesorach, nie pamiętam żadnego z tamtych czasów, który używał czegoś większego. Jeśli pamięć służy, kompilatory segmentowanej architektury x86 również używały 16-bitowej int.
Mark Ransom
@MarkRansom: Wow, masz rację. Byłem baaaaardzo przekonany, że intw 68000 było to 32 bity (o czym pomyślałem jako przykład). To nie był ...
Damon
intw przeszłości miał być najszybszym typem z minimalną szerokością 16 bitów (dlatego C ma regułę promowania liczb całkowitych), ale dziś w przypadku architektur 64-bitowych nie jest to już prawdą. Na przykład 8-bajtowe liczby całkowite są szybsze niż 4-bajtowe liczby całkowite na bitach x86_64, ponieważ przy 4-bajtowych liczbach całkowitych kompilator musi wstawić dodatkową instrukcję, która interpretuje wartość 4-bajtową do wartości 8-bajtowej przed porównaniem jej z innymi wartościami 8-bajtowymi.
StaceyGirl
„unsigned int” niekoniecznie jest najszybszy na x64. Dziwne rzeczy się wydarzyły.
Joshua
Innym częstym przypadkiem jest to long, że ze względów historycznych musi być 32-bitowy, a intteraz nie może być szerszy niż long, więc intmoże być konieczne zachowanie 32-bitowego, nawet jeśli 64 bity byłyby szybsze.
Davislor,
6

Nie widziałem dowodów, które uint32_tmożna by wykorzystać dla jego zakresu. Zamiast tego, przez większość czasu, który widziałem, uint32_tjest używany do przechowywania dokładnie 4 oktetów danych w różnych algorytmach, z gwarantowaną semantyką zawijania i przesunięcia!

Istnieją również inne powody, dla których warto używać uint32_tzamiast uint_fast32_t: Często jest tak, że zapewnia stabilny ABI. Dodatkowo zużycie pamięci może być dokładnie znane. To bardzo kompensuje niezależnie od przyrostu prędkości uint_fast32_t, jeśli ten typ będzie inny niż ten z uint32_t.

Dla wartości <65536 istnieje już poręczny typ, który jest wywoływany unsigned int( unsigned shortwymagane jest, aby mieć co najmniej ten zakres, ale unsigned intma on rodzimy rozmiar słowa). Dla wartości <4294967296 jest wywoływany inny unsigned long.


I wreszcie, ludzie nie używają, uint_fast32_tponieważ wpisywanie jest irytująco długie i łatwe do wpisania: D

Antti Haapala
źródło
@ikegami: zmieniłeś moje zamiary podczas shortedycji. intjest przypuszczalnie szybkim, jeśli różni się od short.
Antti Haapala
1
Twoje ostatnie zdanie jest więc całkowicie błędne. Twierdzenie, że powinieneś używać unsigned intzamiast uint16_fast_toznacza, że ​​twierdzisz, że wiesz lepiej niż kompilator.
ikegami
Przepraszam również za zmianę intencji Twojego tekstu. To nie był mój zamiar.
ikegami
unsigned longnie jest dobrym wyborem, jeśli Twoja platforma ma 64-bitowe karty longi potrzebujesz tylko liczb <2^32.
Ruslan
1
@ikegami: Typ „unsigned int” zawsze będzie zachowywał się jak typ bez znaku, nawet jeśli jest promowany. Pod tym względem jest lepszy od obu uint16_ti uint_fast16_t. Gdyby uint_fast16_tbyły bardziej luźne niż zwykłe typy liczb całkowitych, tak że jego zakres nie musi być spójny dla obiektów, których adresy nie są zajęte, mogłoby to zapewnić pewne korzyści w zakresie wydajności na platformach, które wykonują wewnętrznie 32-bitowe obliczenia arytmetyczne, ale mają 16-bitową magistralę danych . Norma nie pozwala jednak na taką elastyczność.
supercat
5

Kilka powodów.

  1. Wiele osób nie wie, że istnieją typy „szybkie”.
  2. Pisanie jest bardziej szczegółowe.
  3. Trudniej jest uzasadnić zachowanie programów, jeśli nie znasz rzeczywistego rozmiaru typu.
  4. Standard właściwie nie określa najszybciej, ani naprawdę nie może określić, jaki typ jest faktycznie najszybszy, może być bardzo zależny od kontekstu.
  5. Nie widziałem dowodów na to, aby twórcy platform zastanawiali się nad rozmiarem tych typów podczas definiowania swoich platform. Na przykład w Linuksie x86-64 wszystkie typy „szybkie” są 64-bitowe, mimo że x86-64 obsługuje sprzętowo szybkie operacje na wartościach 32-bitowych.

Podsumowując, „szybkie” typy to bezwartościowe śmieci. Jeśli naprawdę chcesz dowiedzieć się, jaki typ jest najszybszy dla danej aplikacji, musisz przetestować swój kod na swoim kompilatorze.

plugwash
źródło
W przeszłości istniały procesory, które miały 32-bitowe i / lub 64-bitowe instrukcje dostępu do pamięci, ale nie miały 8- i 16-bitowych. Więc int_fast {8,16} _t nie byłby całkiem głupi 20+ lat temu. AFAIK ostatnim takim głównym procesorem był oryginalny DEC Alpha 21064 (druga generacja 21164 została ulepszona). Prawdopodobnie nadal istnieją wbudowane procesory DSP lub cokolwiek innego, co robi tylko dostęp słowny, ale przenośność zwykle nie jest wielkim problemem w takich rzeczach, więc nie rozumiem, dlaczego miałbyś na nich szybko kult . Były też ręcznie budowane maszyny Cray "wszystko jest 64-bitowe".
user1998586
1
Kategoria 1b: Wiele osób nie obchodzi, że istnieją typy „szybkie”. To moja kategoria.
gnasher729
Kategoria 6: Wiele osób nie wierzy, że „szybkie” typy są najszybsze. Należę do tej kategorii.
Jaśniejsze
5

Z punktu widzenia poprawności i łatwości kodowania, uint32_tma wiele zalet, uint_fast32_tw szczególności ze względu na dokładniej zdefiniowany rozmiar i semantykę arytmetyczną, jak wskazało wielu użytkowników powyżej.

Co być może już zaprzepaszczona, jest to, że jeden miał korzyść z uint_fast32_t- że może być szybciej , po prostu nigdy nie zmaterializowała się w żaden znaczący sposób. Większość 64-bitowych procesorów, które dominowały w erze 64-bitowej (głównie x86-64 i Aarch64) wyewoluowała z architektur 32-bitowych i ma szybkie 32-bitowe natywne operacje nawet w trybie 64-bitowym. Tak uint_fast32_tsamo jak uint32_tna tych platformach.

Nawet jeśli niektóre z „działających również” platform, takich jak POWER, MIPS64, SPARC, oferują tylko 64-bitowe operacje ALU, zdecydowana większość interesujących operacji 32-bitowych można wykonać dobrze na rejestrach 64-bitowych: dolna wersja 32-bitowa mają pożądane rezultaty (a wszystkie popularne platformy pozwalają przynajmniej na ładowanie / przechowywanie 32-bitowych bitów). Przesunięcie w lewo jest głównym problemem, ale nawet to można w wielu przypadkach zoptymalizować przez optymalizację śledzenia wartości / zakresu w kompilatorze.

Wątpię, by sporadyczne nieco wolniejsze przesunięcie w lewo lub mnożenie 32x32 -> 64 przeważyło dwukrotnie nad zużyciem pamięci dla takich wartości we wszystkich, z wyjątkiem najbardziej niejasnych aplikacji.

Na koniec zauważę, że chociaż kompromis został w dużej mierze scharakteryzowany jako „wykorzystanie pamięci i potencjał wektoryzacji” (na korzyść uint32_t) w porównaniu z liczbą instrukcji / szybkością (na korzyść uint_fast32_t) - nawet to nie jest dla mnie jasne. Tak, na niektórych platformach będziesz potrzebować dodatkowych instrukcji dla niektórych operacji 32-bitowych, ale zapiszesz też niektóre instrukcje, ponieważ:

  • Używanie mniejszego typu często pozwala kompilatorowi sprytnie łączyć sąsiednie operacje przy użyciu jednej operacji 64-bitowej w celu wykonania dwóch operacji 32-bitowych. Przykład tego typu „wektoryzacji biedaka” nie jest rzadkością. Na przykład utworzenie stałej struct two32{ uint32_t a, b; }w raxlike two32{1, 2} może zostać zoptymalizowane do postaci pojedynczej, mov rax, 0x20001podczas gdy wersja 64-bitowa wymaga dwóch instrukcji. W zasadzie powinno to być również możliwe dla sąsiednich operacji arytmetycznych (ta sama operacja, inny operand), ale nie widziałem tego w praktyce.
  • Mniejsze „użycie pamięci” często prowadzi również do mniejszej liczby instrukcji, nawet jeśli rozmiar pamięci lub pamięci podręcznej nie stanowi problemu, ponieważ każda struktura typu lub tablice tego typu są kopiowane, zyskujesz dwa razy więcej za swoje pieniądze za skopiowany rejestr.
  • Mniejsze typy danych często wykorzystują lepsze nowoczesne konwencje wywoływania, takie jak SysV ABI, które wydajnie pakują dane struktury danych do rejestrów. Na przykład, możesz zwrócić do 16-bajtowej struktury w rejestrach rdx:rax. W przypadku funkcji zwracającej strukturę z 4 uint32_twartościami (zainicjowaną ze stałej), co przekłada się na

    ret_constant32():
        movabs  rax, 8589934593
        movabs  rdx, 17179869187
        ret
    

    Ta sama struktura z 4 64-bitowymi uint_fast32_twymaga przesunięcia rejestru i czterech zapisów w pamięci, aby zrobić to samo (a wywołujący prawdopodobnie będzie musiał odczytać wartości z pamięci po powrocie):

    ret_constant64():
        mov     rax, rdi
        mov     QWORD PTR [rdi], 1
        mov     QWORD PTR [rdi+8], 2
        mov     QWORD PTR [rdi+16], 3
        mov     QWORD PTR [rdi+24], 4
        ret
    

    Podobnie, podczas przekazywania argumentów struktury, 32-bitowe wartości są upakowane około dwa razy gęsto w rejestrach dostępnych dla parametrów, więc zmniejsza się prawdopodobieństwo, że zabraknie argumentów rejestrów i będziesz musiał przelać się na stos 1 .

  • Nawet jeśli zdecydujesz się używać uint_fast32_tw miejscach, w których „liczy się szybkość”, często będziesz mieć również miejsca, w których potrzebujesz stałego rozmiaru. Na przykład podczas przekazywania wartości do wyjścia zewnętrznego, z zewnętrznego wejścia, jako część twojego ABI, jako część struktury, która wymaga określonego układu lub ponieważ inteligentnie używasz uint32_tdo dużych agregacji wartości w celu zaoszczędzenia miejsca w pamięci. W miejscach, w których twoje uint_fast32_ttypy i `uint32_t` muszą się ze sobą łączyć, możesz znaleźć (oprócz złożoności programowania) niepotrzebne rozszerzenia znaków lub inny kod związany z niezgodnością rozmiaru. W wielu przypadkach kompilatory dobrze radzą sobie z optymalizacją tego rozwiązania, ale nadal nie jest niczym niezwykłym widzieć to w zoptymalizowanej wydajności podczas mieszania typów o różnych rozmiarach.

Możesz pobawić się niektórymi z powyższych przykładów, a więcej na Godbolt .


1 Aby było jasne, konwencja upakowania struktur ciasno w rejestry nie zawsze jest oczywistą korzyścią dla mniejszych wartości. Oznacza to, że mniejsze wartości mogą wymagać „wyodrębnienia” przed ich użyciem. Na przykład prosta funkcja, która zwraca sumę dwóch składowych struktury razem, potrzebuje trochę mov rax, rdi; shr rax, 32; add edi, eaxczasu dla wersji 64-bitowej, każdy argument otrzymuje własny rejestr i potrzebuje tylko jednego addlub lea. Jeśli jednak zaakceptujesz, że projekt „ciasno upakowanych struktur podczas przechodzenia” ma ogólnie sens, mniejsze wartości będą bardziej wykorzystywać tę funkcję.

BeeOnRope
źródło
glibc na x86-64 Linux używa 64-bitowej wersji uint_fast32_t, co jest błędem IMO. (Najwyraźniej Windows uint_fast32_tjest typem 32-bitowym w systemie Windows.) Bycie 64-bitowym na Linuksie x86-64 jest powodem, dla którego nigdy nie polecałbym nikomu go używać uint_fast32_t: jest zoptymalizowany pod kątem małej liczby instrukcji (argumenty funkcji i wartości zwracane nigdy nie wymagają rozszerzenia zerowego dla użyj jako indeksu tablicy), a nie dla ogólnej szybkości lub rozmiaru kodu na jednej z głównych ważnych platform.
Peter Cordes
2
O racja, przeczytałem twój komentarz powyżej o SysV ABI, ale jak zauważyłeś później, może to była inna grupa / dokument, która zdecydowała o tym - ale myślę, że kiedy to się stanie, jest prawie w kamieniu. Myślę, że jest nawet wątpliwe, że czysta liczba cykli / liczba instrukcji faworyzuje większe typy, nawet ignorując efekty wykorzystania pamięci i wektoryzację, nawet na platformach bez dobrej obsługi operacji 32-bitowych - ponieważ wciąż istnieją przypadki, w których kompilator może lepiej zoptymalizować mniejsze typy. Dodałem kilka przykładów powyżej. @PeterCordes
BeeOnRope
SysV pakowanie wielu członków struktury do tego samego rejestru dość często kosztuje więcej instrukcji podczas zwracania pliku pair<int,bool>lub pair<int,int>. Jeśli oba elementy członkowskie nie są stałymi czasu kompilacji, zwykle jest więcej niż tylko OR, a obiekt wywołujący musi rozpakować zwracane wartości. ( bugs.llvm.org/show_bug.cgi?id=34840 LLVM optymalizuje przekazywanie wartości zwracanej dla funkcji prywatnych i powinien traktować 32-bitowe int jako całość, raxwięc booljest oddzielne dlzamiast 64-bitowej stałej do testit.)
Peter Cordes
1
Myślę, że kompilatory generalnie nie dzielą funkcji. Wydzielenie szybkiej ścieżki jako oddzielnej funkcji jest użyteczną optymalizacją na poziomie źródła (szczególnie w nagłówku, w którym można ją wstawić). Może być bardzo dobre, jeśli 90% danych wejściowych to przypadek „nic nie rób”; robienie tego filtrowania w pętli wywołującego to duża wygrana. IIRC, Linux używa __attribute__((noinline))dokładnie, aby upewnić się, że gcc nie wbudowuje funkcji obsługi błędów i umieszcza kilka push rbx/ .../ pop rbx/ ... na szybkiej ścieżce niektórych ważnych funkcji jądra, które mają wiele wywołań i same nie są wbudowane.
Peter Cordes
1
W Javie jest to również bardzo ważne, ponieważ inlining jest kluczem do dalszych optymalizacji (zwłaszcza de-wirtualizacji, która jest wszechobecna w przeciwieństwie do C ++), więc często opłaca się wydzielić tam szybką ścieżkę, a „optymalizacja kodu bajtowego” jest faktycznie rzeczą (pomimo konwencjonalna opinia, że ​​nie ma to sensu, ponieważ JIT dokonuje ostatecznej kompilacji) tylko po to, aby uzyskać odliczanie kodu bajtowego, ponieważ decyzje inlining są oparte na rozmiarze kodu bajtowego, a nie na rozmiarze wbudowanego kodu maszynowego (a korelacja może różnić się o rzędy wielkości).
BeeOnRope
4

Ze względów praktycznych uint_fast32_tjest całkowicie bezużyteczny. Jest on nieprawidłowo zdefiniowany na najbardziej rozpowszechnionej platformie (x86_64) i nigdzie nie oferuje żadnych korzyści, chyba że masz kompilator o bardzo niskiej jakości. Koncepcyjnie nigdy nie ma sensu używać „szybkich” typów w strukturach danych / tablicach - wszelkie oszczędności, które uzyskasz dzięki temu, że typ jest bardziej efektywny w działaniu, będą ograniczone kosztem (chybienia w pamięci podręcznej itp.) Zwiększania rozmiaru Twój zestaw danych roboczych. A dla indywidualnych zmiennych lokalnych (liczniki pętli, temps, itp.) Kompilator niebędący zabawką może zwykle po prostu pracować z większym typem w wygenerowanym kodzie, jeśli jest to bardziej wydajne, i skracać do rozmiaru nominalnego tylko wtedy, gdy jest to konieczne dla poprawności (iz podpisane typy, nigdy nie jest to konieczne).

Jedynym wariantem, który jest teoretycznie przydatny, jest to uint_least32_t, że musisz mieć możliwość przechowywania dowolnej wartości 32-bitowej, ale chcesz być przenośny na maszyny, które nie mają typu 32-bitowego o dokładnym rozmiarze. Praktycznie jednak nie musisz się tym martwić.

R .. GitHub PRZESTAŃ POMÓC LODOWI
źródło
4

Według mojego zrozumienia intpoczątkowo miał to być "natywny" typ liczby całkowitej z dodatkową gwarancją, że powinien mieć rozmiar co najmniej 16 bitów - coś, co było wtedy uważane za "rozsądny" rozmiar.

Gdy platformy 32-bitowe stały się bardziej popularne, możemy powiedzieć, że „rozsądny” rozmiar zmienił się na 32 bity:

  • Nowoczesny system Windows używa wersji 32-bitowej intna wszystkich platformach.
  • POSIX gwarantuje, że intjest to co najmniej 32 bity.
  • C # Java ma typ, intktóry gwarantuje dokładnie 32 bity.

Ale kiedy platforma 64-bitowa stała się normą, nikt nie rozwinął się intdo 64-bitowej liczby całkowitej z powodu:

  • Przenośność: wiele kodu zależy od int32-bitowego rozmiaru.
  • Zużycie pamięci: podwojenie użycia pamięci dla każdego intmoże być nieracjonalne w większości przypadków, ponieważ w większości przypadków używane liczby są znacznie mniejsze niż 2 miliardy.

Teraz, dlaczego wolisz uint32_tsię uint_fast32_t? Z tego samego powodu języki, C # i Java zawsze używają liczb całkowitych o stałym rozmiarze: programista nie pisze kodu z myślą o możliwych rozmiarach różnych typów, piszą dla jednej platformy i testują kod na tej platformie. Większość kodu niejawnie zależy od określonych rozmiarów typów danych. I dlatego uint32_tw większości przypadków jest lepszym wyborem - nie pozwala na niejednoznaczność co do jego zachowania.

Co więcej, czy uint_fast32_trzeczywiście jest to najszybszy typ na platformie o rozmiarze równym lub większym niż 32 bity? Nie całkiem. Rozważmy ten kompilator kodu przez GCC dla x86_64 w systemie Windows:

extern uint64_t get(void);

uint64_t sum(uint64_t value)
{
    return value + get();
}

Wygenerowany zespół wygląda następująco:

push   %rbx
sub    $0x20,%rsp
mov    %rcx,%rbx
callq  d <sum+0xd>
add    %rbx,%rax
add    $0x20,%rsp
pop    %rbx
retq

Teraz, jeśli zmienisz get()wartość zwracaną na uint_fast32_t(czyli 4 bajty w systemie Windows x86_64), otrzymasz to:

push   %rbx
sub    $0x20,%rsp
mov    %rcx,%rbx
callq  d <sum+0xd>
mov    %eax,%eax        ; <-- additional instruction
add    %rbx,%rax
add    $0x20,%rsp
pop    %rbx
retq

Zwróć uwagę, że wygenerowany kod jest prawie taki sam, z wyjątkiem dodatkowych mov %eax,%eaxinstrukcji po wywołaniu funkcji, które mają na celu rozszerzenie wartości 32-bitowej do wartości 64-bitowej.

Nie ma takiego problemu, jeśli używasz tylko wartości 32-bitowych, ale prawdopodobnie będziesz używać tych ze size_tzmiennymi (prawdopodobnie rozmiary tablic?), A są to 64 bity na x86_64. W Linuksie uint_fast32_tjest 8 bajtów, więc sytuacja jest inna.

Wielu programistów używa go, intgdy muszą zwrócić małą wartość (powiedzmy w zakresie [-32,32]). To działałoby idealnie, gdyby intbył to natywny rozmiar całkowitych platformy, ale ponieważ nie ma go na platformach 64-bitowych, lepszym wyborem jest inny typ, który pasuje do natywnego typu platformy (chyba że jest często używany z innymi liczbami całkowitymi o mniejszym rozmiarze).

Zasadniczo, niezależnie od tego, co mówi standard, i tak uint_fast32_tjest zepsuty w niektórych implementacjach. Jeśli zależy Ci na dodatkowych instrukcjach generowanych w niektórych miejscach, powinieneś zdefiniować własny „natywny” typ liczby całkowitej. Lub możesz użyć size_tdo tego celu, ponieważ zwykle będzie pasował do nativerozmiaru (nie uwzględniam starych i niejasnych platform, takich jak 8086, tylko platformy, które mogą obsługiwać Windows, Linux itp.).


Innym znakiem, który pokazuje, że intpowinien być natywny typ liczby całkowitej, jest „reguła promocji liczby całkowitej”. Większość procesorów może wykonywać operacje tylko na rodzimym komputerze, więc 32-bitowy procesor zwykle może wykonywać tylko 32-bitowe dodawanie, odejmowanie itp. (Wyjątkiem są procesory Intel). Typy liczb całkowitych innych rozmiarów są obsługiwane tylko przez instrukcje ładowania i przechowywania. Na przykład, wartość 8-bitowa powinna zostać załadowana odpowiednią instrukcją „załaduj 8-bitowy znak ze znakiem” lub „załaduj 8-bitowy bez znaku”, a po załadowaniu wartość zostanie rozszerzona do 32 bitów. Bez reguły C promowania liczb całkowitych kompilatory musiałyby dodać trochę więcej kodu dla wyrażeń, które używają typów mniejszych niż typ natywny. Niestety, nie ma to już miejsca w przypadku architektur 64-bitowych, ponieważ kompilatory muszą teraz w niektórych przypadkach emitować dodatkowe instrukcje (jak pokazano powyżej).

StaceyGirl
źródło
2
Myśli o „nikt nie rozszerzał int do 64-bitowej liczby całkowitej, ponieważ” oraz „Niestety, to już nie dotyczy architektur 64-bitowych” to bardzo dobre punkty . Aby być uczciwym, jeśli chodzi o „najszybszy” i porównywanie kodu asemblera: w tym przypadku wydaje się, że drugi fragment kodu jest wolniejszy z dodatkowymi instrukcjami, ale długość kodu i szybkość czasami nie są tak dobrze skorelowane. Silniejsze porównanie pokazałoby czasy działania - ale nie jest to takie łatwe.
chux - Przywróć Monikę
Nie sądzę, że łatwo będzie zmierzyć powolność drugiego kodu, procesor Intel może wykonywać naprawdę dobrą robotę, ale dłuższy kod oznacza również duże zanieczyszczenie pamięci podręcznej. Pojedyncza instrukcja raz na jakiś czas prawdopodobnie nie boli, ale użyteczność uint_fast32_t staje się niejednoznaczna.
StaceyGirl
Zdecydowanie zgadzam się, że użyteczność uint_fast32_tstaje się niejednoznaczna we wszystkich, z wyjątkiem bardzo wybranych okoliczności. Podejrzewam, że głównym powodem jest uint_fastN_tw ogóle dostosowanie się do „nie używajmy wersji unsigned64-bitowej, mimo że często jest to najszybsze na nowej platformie, ponieważ zbyt dużo kodu się zepsuje”, ale „nadal chcę szybkiego, co najmniej N-bitowego typu ”. Znów bym cię naświetlał, gdybym mógł
chux - Przywróć Monikę
Większość architektur 64-bitowych może z łatwością działać na 32-bitowych liczbach całkowitych. Nawet DEC Alpha (która była nową gałęzią architektury 64-bitowej, a nie rozszerzeniem istniejącego 32-bitowego ISA, takiego jak PowerPC64 lub MIPS64) miała 32 i 64-bitowe obciążenia / magazyny. (Ale nie bajtowe lub 16-bitowe ładowanie / przechowywanie!). Większość instrukcji była tylko 64-bitowa, ale miała natywną obsługę sprzętową dla 32-bitowego dodawania / subskrybowania i mnożenia, co skraca wynik do 32 bitów. ( alasir.com/articles/alpha_history/press/alpha_intro.html ) Tak więc tworzenie wersji int64-bitowej nie powoduje prawie żadnego wzrostu szybkości , a zwykle jest to utrata szybkości wynikająca z rozmiaru pamięci podręcznej.
Peter Cordes
Ponadto, jeśli utworzyłeś intwersję 64-bitową, twoja uint32_tczcionka o stałej szerokości wymagałaby __attribute__hackowania lub innego hackowania lub jakiegoś niestandardowego typu, który jest mniejszy niż int. (Albo short, ale wtedy masz ten sam problem uint16_t.) Nikt tego nie chce. 32-bitowy jest wystarczająco szeroki dla prawie wszystkiego (w przeciwieństwie do 16-bitowego); używanie 32-bitowych liczb całkowitych, kiedy to wszystko, czego potrzebujesz, nie jest „nieefektywne” w żaden znaczący sposób na 64-bitowej maszynie.
Peter Cordes
2

W wielu przypadkach, gdy algorytm działa na tablicy danych, najlepszym sposobem poprawy wydajności jest zminimalizowanie liczby błędów pamięci podręcznej. Im mniejszy każdy element, tym więcej z nich zmieści się w pamięci podręcznej. Dlatego nadal wiele kodu jest napisanych w celu używania wskaźników 32-bitowych na maszynach 64-bitowych: nie potrzebują one niczego bliskiego 4 GiB danych, ale koszt wykonania wszystkich wskaźników i przesunięć wymaga ośmiu bajtów zamiast czterech byłby znaczny.

Istnieją również pewne ABI i protokoły, które wymagają dokładnie 32 bitów, na przykład adresy IPv4. To uint32_tnaprawdę oznacza: używaj dokładnie 32 bitów, niezależnie od tego, czy jest to wydajne dla procesora, czy nie. Kiedyś były one deklarowane jako longlub unsigned long, co powodowało wiele problemów podczas przejścia na wersję 64-bitową. Jeśli potrzebujesz tylko typu bez znaku, który zawiera liczby do co najmniej 2³²-1, taka była definicja od unsigned longczasu pojawienia się pierwszego standardu C. W praktyce jednak wystarczająco stary kod zakładał, że a longmoże zawierać dowolny wskaźnik, przesunięcie pliku lub znacznik czasu, a wystarczająco stary kod zakładał, że ma dokładnie 32 bity szerokości, że kompilatory niekoniecznie mogą zrobić longto samo, co int_fast32_tbez zrywania zbyt wielu rzeczy.

Teoretycznie byłoby bardziej przyszłościowe, aby program używał uint_least32_t, a może nawet ładował uint_least32_telementy do uint_fast32_tzmiennej w celu obliczenia. Implementacja, która nie miała żadnego uint32_ttypu, mogłaby nawet deklarować formalną zgodność ze standardem! (To po prostu nie będzie w stanie skompilować wielu istniejących programów). W praktyce nie ma już więcej architektura gdzie int, uint32_ti uint_least32_tnie są takie same, a nie zaleta, obecnie , do wykonania uint_fast32_t. Po co więc zbytnio komplikować?

Jednak spójrz na powód, dla którego wszystkie 32_ttypy musiały istnieć, kiedy już mieliśmy long, a zobaczysz, że te założenia już wcześniej wypłynęły nam na twarz. Twój kod może pewnego dnia zostać uruchomiony na komputerze, na którym obliczenia 32-bitowe o dokładnej szerokości są wolniejsze niż rodzimy rozmiar słowa, i lepiej byłoby używać go uint_least32_tdo przechowywania i uint_fast32_tobliczeń religijnie. Lub jeśli przejdziesz przez ten most, kiedy do niego dojdziesz i po prostu chcesz czegoś prostego, to jest unsigned long.

Davislor
źródło
Ale są architektury, w których intnie ma 32 bitów, na przykład ILP64. Nie żeby były powszechne.
Antti Haapala
Nie sądzę, że ILP64 istnieje w czasie teraźniejszym? Kilka stron internetowych twierdzi, że używa go „Cray”, z których wszystkie cytują tę samą stronę Unix.org z 1997 r., Ale UNICOS w połowie lat 90. faktycznie zrobił coś dziwniejszego, a dzisiejsze Crays używają sprzętu Intela. Ta sama strona twierdzi, że superkomputery ETA używały ILP64, ale wypadły z biznesu dawno temu. Wikipedia twierdzi, że port Solaris firmy HAL do SPARC64 wykorzystywał ILP64, ale przez lata również był nieczynny. CppReference mówi, że ILP64 był używany tylko w kilku wczesnych 64-bitowych Unikach. Więc ma to znaczenie tylko dla bardzo ezoterycznych retrokomputerów.
Davislor
Zauważ, że jeśli używasz dziś „interfejsu ILP64” biblioteki jądra matematycznego Intel, intbędzie on miał 32 bity szerokości. Rodzaj MKL_INTjest tym, co się zmieni.
Davislor
1

Aby udzielić bezpośredniej odpowiedzi: myślę, że prawdziwym powodem, dla którego uint32_tjest używany zbyt często uint_fast32_tlub uint_least32_tpo prostu jest to, że łatwiej jest pisać, a ponieważ jest krótszy, o wiele przyjemniejszy do czytania: jeśli tworzysz struktury z niektórymi typami, a niektóre z nich są uint_fast32_tlub podobne, wtedy często trudno jest ładnie dopasować je do intlub boollub innych typów w C, które są dość krótkie (przykład: charvs. character). Oczywiście nie mogę tego poprzeć twardymi danymi, ale inne odpowiedzi mogą tylko zgadywać, dlaczego.

Jeśli chodzi o względy techniczne uint32_t, nie sądzę, aby istniały żadne - kiedy absolutnie potrzebujesz dokładnego 32-bitowego int bez znaku, ten typ jest jedynym znormalizowanym wyborem. W prawie wszystkich innych przypadkach inne warianty są preferowane pod względem technicznym - szczególnie, uint_fast32_tjeśli obawiasz się o szybkość i uint_least32_tjeśli chodzi o przestrzeń dyskową. Używanie uint32_tw obu tych przypadkach grozi niemożnością kompilacji, ponieważ typ nie musi istnieć.

W praktyce uint32_ttypy i pokrewne istnieją na wszystkich obecnych platformach, z wyjątkiem niektórych bardzo rzadkich (obecnie) DSP lub implementacji żartów, więc rzeczywiste ryzyko związane z użyciem tego typu jest niewielkie. Podobnie, chociaż możesz napotkać kary za prędkość przy typach o stałej szerokości, nie są one już (na nowoczesnych procesorach cpus) okaleczające.

Dlatego uważam, że typ krótszy po prostu wygrywa w większości przypadków z powodu lenistwa programisty.

Pamiętaj Monikę
źródło