Czy dobrą praktyką jest stosowanie mniejszych typów danych dla zmiennych w celu oszczędzania pamięci?

32

Kiedy po raz pierwszy nauczyłem się języka C ++, dowiedziałem się, że oprócz int, float itp. W tym języku istniały mniejsze lub większe wersje tych typów danych. Na przykład mógłbym wywołać zmienną x

int x;
or 
short int x;

Główną różnicą jest to, że short int zajmuje 2 bajty pamięci, podczas gdy int zajmuje 4 bajty, a short int ma mniejszą wartość, ale możemy też to nazwać, aby był jeszcze mniejszy:

int x;
short int x;
unsigned short int x;

co jest jeszcze bardziej restrykcyjne.

Moje pytanie dotyczy tego, czy dobrą praktyką jest używanie osobnych typów danych zgodnie z wartościami, jakie zmienne przyjmują w programie. Czy dobrym pomysłem jest zawsze deklarowanie zmiennych zgodnie z tymi typami danych?

Bugster
źródło
3
czy wiesz o wzorach projektowych Flyweight ? „obiekt, który minimalizuje zużycie pamięci, współużytkując jak najwięcej danych z innymi podobnymi obiektami; jest to sposób na użycie obiektów w dużych ilościach, gdy prosta powtarzana reprezentacja wykorzystałaby niedopuszczalną ilość pamięci ...”
gnat
5
Przy standardowych ustawieniach kompilatora pakowania / wyrównania zmienne i tak zostaną wyrównane do granic 4 bajtów, więc może nie być żadnej różnicy.
nikie
36
Klasyczny przypadek przedwczesnej optymalizacji.
szalik
1
@nikie - mogą być wyrównane na granicy 4 bajtów na procesorze x86, ale ogólnie nie jest to prawdą. MSP430 umieszcza znak na dowolnym bajcie, a wszystko inne na parzystym bajcie. Myślę, że AVR-32 i ARM Cortex-M są takie same.
uɐɪ
3
Druga część twojego pytania sugeruje, że dodanie w unsignedjakiś sposób powoduje, że liczba całkowita zajmuje mniej miejsca, co oczywiście jest fałszywe. Będzie miał taką samą liczbę dyskretnie reprezentowalnych wartości (daje lub przyjmuje 1 w zależności od tego, jak znak jest reprezentowany), ale po prostu został zmieniony wyłącznie na dodatni.
underscore_d

Odpowiedzi:

41

Przez większość czasu koszt miejsca jest znikomy i nie powinieneś się tym przejmować, ale powinieneś martwić się o dodatkowe informacje, które podajesz, deklarując typ. Na przykład, jeśli:

unsigned int salary;

Przekazujesz użyteczną informację innemu deweloperowi: wynagrodzenie nie może być ujemne.

Różnica między skrótem, int, long rzadko powoduje problemy z miejscem w aplikacji. Bardziej prawdopodobne jest, że przypadkowo fałszywe założenie, że liczba zawsze będzie pasować do jakiegoś typu danych. Prawdopodobnie bezpieczniej jest zawsze używać int, chyba że masz 100% pewności, że twoje liczby będą zawsze bardzo małe. Nawet wtedy mało prawdopodobne jest, aby zaoszczędzić zauważalną ilość miejsca.

Oleksi
źródło
5
To prawda, że ​​obecnie rzadko powoduje problemy, ale jeśli projektujesz bibliotekę lub klasę, z której skorzysta inny programista, to inna sprawa. Być może będą potrzebować miejsca na milion takich obiektów, w którym to przypadku różnica jest duża - 4 MB w porównaniu do 2 MB tylko dla tego jednego pola.
dodgy_coder
30
Korzystanie unsignedw tym przypadku jest złym pomysłem: nie tylko wynagrodzenie nie może być ujemne, ale różnica między dwoma wynagrodzeniami również nie może być ujemna. (Ogólnie rzecz biorąc, używanie niepodpisanego do niczego innego niż kręcenie bitów i posiadanie określonego zachowania w przypadku przepełnienia jest złym pomysłem.)
zvrba
15
@zvrba: Różnica między dwoma wynagrodzeniami sama w sobie nie jest wynagrodzeniem, więc uzasadnione jest użycie innego podpisanego typu.
JeremyP
12
@JeremyP Tak, ale jeśli używasz C (i wygląda to tak samo w C ++), odejmowanie liczb całkowitych bez znaku skutkuje liczbą całkowitą bez znaku , która nie może być ujemna. Może zmienić się w odpowiednią wartość, jeśli rzucisz ją na podpisaną liczbę całkowitą, ale wynikiem obliczenia jest liczba całkowita bez znaku. Zobacz także tę odpowiedź, aby dowiedzieć się więcej o dziwnych obliczeniach ze znakiem / bez znaku - dlatego nigdy nie powinieneś używać zmiennych bez znaku, chyba że naprawdę kręcisz bity.
Tacroy,
5
@zvrba: Różnica to kwota pieniężna, ale nie pensja. Teraz możesz argumentować, że pensja jest również kwotą pieniężną (ograniczoną do liczb dodatnich i 0 poprzez zatwierdzenie danych wejściowych, co zrobi większość ludzi), ale różnica między dwoma pensjami sama w sobie nie jest pensją.
JeremyP
29

OP nie powiedział nic o typie systemu, dla którego piszą programy, ale zakładam, że OP myślał o typowym komputerze z pamięcią GB, skoro wspomniano o C ++. Jak mówi jeden z komentarzy, nawet przy tego rodzaju pamięci, jeśli masz kilka milionów elementów jednego typu - takich jak tablica - to wielkość zmiennej może mieć znaczenie.

Jeśli wejdziesz w świat systemów wbudowanych - co tak naprawdę nie jest poza zakresem pytania, ponieważ OP nie ogranicza go do komputerów PC - rozmiar typów danych ma bardzo duże znaczenie. Właśnie skończyłem szybki projekt na 8-bitowym mikrokontrolerze, który ma tylko 8 000 słów pamięci programu i 368 bajtów pamięci RAM. Tam oczywiście liczy się każdy bajt. Nigdy nie używa się zmiennej większej niż potrzebują (zarówno z punktu widzenia przestrzeni, jak i rozmiaru kodu - procesory 8-bitowe używają wielu instrukcji do manipulowania danymi 16 i 32-bitowymi). Po co używać procesora z tak ograniczonymi zasobami? W dużych ilościach mogą kosztować zaledwie jedną czwartą.

Obecnie pracuję nad innym projektem osadzonym z 32-bitowym mikrokontrolerem opartym na MIPS, który ma 512 000 bajtów pamięci flash i 128 000 bajtów pamięci RAM (i kosztuje około 6 USD). Podobnie jak w przypadku komputera, „naturalny” rozmiar danych wynosi 32 bity. Teraz staje się bardziej wydajne, pod względem kodu, użycie liczb całkowitych dla większości zmiennych zamiast znaków lub skrótów. Ale jeszcze raz należy rozważyć każdy typ tablicy lub struktury, czy uzasadnione są mniejsze typy danych. W przeciwieństwie do kompilatorów dla większych systemów, bardziej prawdopodobne jest, że zmienne w strukturze zostaną upakowane w systemie osadzonym. Staram się zawsze umieszczać wszystkie zmienne 32-bitowe najpierw, a następnie 16-bitowe, a następnie 8-bitowe, aby uniknąć „dziur”.

tcrosley
źródło
10
+1 za fakt, że do systemów wbudowanych mają zastosowanie inne reguły. Wspomnienie o C ++ nie oznacza, że ​​celem jest komputer. Jeden z moich ostatnich projektów został napisany w C ++ na procesorze z 32k RAM i 256K Flash.
uɐɪ
13

Odpowiedź zależy od twojego systemu. Ogólnie rzecz biorąc, oto zalety i wady korzystania z mniejszych typów:

Zalety

  • Mniejsze typy zużywają mniej pamięci w większości systemów.
  • Mniejsze typy zapewniają szybsze obliczenia w niektórych systemach. Jest to szczególnie prawdziwe w przypadku liczb zmiennoprzecinkowych i podwójnych w wielu systemach. A mniejsze typy int dają również znacznie szybszy kod na 8- lub 16-bitowych procesorach.

Niedogodności

  • Wiele procesorów ma wymagania dotyczące wyrównania. Niektóre dane wyrównane uzyskują dostęp szybciej niż niewyrównane. Niektóre muszą mieć wyrównane dane, aby mieć do nich dostęp. Większe typy liczb całkowitych są równe jednej wyrównanej jednostce, więc najprawdopodobniej nie są wyrównane. Oznacza to, że kompilator może zostać zmuszony do umieszczenia mniejszych liczb całkowitych w większych. A jeśli mniejsze typy są częścią większej struktury, możesz uzyskać różne bajty wypełniania po cichu wstawiane w dowolnym miejscu struktury przez kompilator, aby naprawić wyrównanie.
  • Niebezpieczne niejawne konwersje. C i C ++ mają kilka niejasnych, niebezpiecznych reguł, w jaki sposób zmienne są awansowane na większe, domyślnie bez typografii. Istnieją dwa zestawy niejawnych reguł konwersji splecionych ze sobą, zwane „regułami promocji liczb całkowitych” i „zwykłymi konwersjami arytmetycznymi”. Przeczytaj więcej o nich tutaj . Reguły te są jedną z najczęstszych przyczyn błędów w C i C ++. Możesz uniknąć wielu problemów, po prostu używając tego samego typu liczb całkowitych w całym programie.

Radzę polubić to:

system                             int types

small/low level embedded system    stdint.h with smaller types
32-bit embedded system             stdint.h, stick to int32_t and uint32_t.
32-bit desktop system              Only use (unsigned) int and long long.
64-bit system                      Only use (unsigned) int and long long.

Alternatywnie możesz użyć int_leastn_tlub int_fastn_tze stdint.h, gdzie n jest liczbą 8, 16, 32 lub 64. int_leastn_ttyp oznacza „Chcę, aby to było co najmniej n bajtów, ale nie obchodzi mnie, czy kompilator przydzieli go jako większy typ pasujący do wyrównania ".

int_fastn_t oznacza: „Chcę, aby miał on długość n bajtów, ale jeśli sprawi to, że mój kod będzie działał szybciej, kompilator powinien użyć typu większego niż określony”.

Ogólnie rzecz biorąc, różne typy stdint.h są znacznie lepszą praktyką niż zwykłe intitp., Ponieważ są przenośne. Chodziło o intto, aby nie nadać mu określonej szerokości wyłącznie w celu zapewnienia przenośności. Ale w rzeczywistości trudno go przenieść, ponieważ nigdy nie wiadomo, jak duży będzie w danym systemie.


źródło
Zwróć uwagę na wyrównanie. W moim obecnym projekcie nieuzasadnione użycie uint8_t na 16-bitowym MSP430 rozbiło MCU w tajemniczy sposób (najprawdopodobniej gdzieś nastąpił nierównomierny dostęp, być może z winy GCC, a może nie) - po prostu zastąpienie wszystkich uint8_t przez „niepodpisane” wyeliminowało awarie. Użycie typów 8-bitowych na> 8-bitowych łukach, jeśli nie jest krytyczne, jest co najmniej nieefektywne: kompilator generuje dodatkowe instrukcje „i reg, 0xff”. Użyj „int / unsigned” dla przenośności i uwolnij kompilator od dodatkowych ograniczeń.
alexei
11

W zależności od tego, jak działa określony system operacyjny, zazwyczaj oczekuje się, że pamięć zostanie przydzielona w sposób niezoptymalizowany, tak że gdy wywołuje się bajt, słowo lub inny niewielki typ danych do przydzielenia, wartość zajmuje cały rejestr, a wszystko to bardzo posiadać. Sposób, w jaki kompilator lub interpreter działa, aby to zinterpretować, jest jednak czymś innym, więc jeśli na przykład skompilujesz program w języku C #, wartość może fizycznie zająć rejestr dla siebie, jednak wartość zostanie sprawdzona na granicy, aby upewnić się, że nie spróbuj zapisać wartość, która przekroczy granice zamierzonego typu danych.

Jeśli chodzi o wydajność, a jeśli naprawdę jesteś pedantyczny na temat takich rzeczy, prawdopodobnie szybciej jest po prostu użyć typu danych, który najbardziej odpowiada docelowemu rozmiarowi rejestru, ale potem brakuje ci tego cudownego cukru syntaktycznego, który sprawia, że ​​praca ze zmiennymi jest tak łatwa .

Jak ci to pomaga? Cóż, tak naprawdę to Ty decydujesz, w jakiej sytuacji kodujesz. Dla prawie każdego programu, jaki kiedykolwiek napisałem, wystarczy zaufać swojemu kompilatorowi, aby zoptymalizować rzeczy i użyć typu danych, który jest dla Ciebie najbardziej użyteczny. Jeśli potrzebujesz wysokiej precyzji, użyj większych typów danych zmiennoprzecinkowych. Jeśli pracujesz tylko z dodatnimi wartościami, prawdopodobnie możesz użyć liczby całkowitej bez znaku, ale w większości przypadków wystarczy użycie typu danych int.

Jeśli jednak masz bardzo surowe wymagania dotyczące danych, takie jak napisanie protokołu komunikacyjnego lub jakiś algorytm szyfrowania, użycie typów danych ze sprawdzonym zakresem może być bardzo przydatne, szczególnie jeśli próbujesz uniknąć problemów związanych z przekroczeniami / przekroczeniami danych lub nieprawidłowe wartości danych.

Jedynym innym powodem, dla którego mogę wymyślić z góry, aby używać określonych typów danych, jest to, że próbujesz komunikować zamiary w swoim kodzie. Na przykład, używając skrótu, mówisz innym programistom, że zezwalasz na liczby dodatnie i ujemne w bardzo małym zakresie wartości.

S.Robins
źródło
6

Jak skomentował scarfridge , jest to

Klasyczny przypadek przedwczesnej optymalizacji .

Próba optymalizacji wykorzystania pamięci może mieć wpływ na inne obszary wydajności, a złotymi zasadami optymalizacji są:

Pierwsza zasada optymalizacji programu: nie rób tego .

Druga zasada optymalizacji programu (tylko dla ekspertów!): Nie rób tego jeszcze . ”

- Michael A. Jackson

Aby dowiedzieć się, czy nadszedł czas na optymalizację, należy przeprowadzić testy porównawcze i testy. Musisz wiedzieć, gdzie twój kod jest nieefektywny, abyś mógł kierować swoje optymalizacje.

Aby ustalić, czy zoptymalizowana wersja kodu jest faktycznie lepsza niż naiwna implementacja w danym momencie, musisz porównać je z tymi samymi danymi.

Pamiętaj też, że tylko dlatego, że dana implementacja jest bardziej wydajna w obecnej generacji procesorów, nie oznacza to, że zawsze tak będzie. Moja odpowiedź na pytanie Czy mikrooptymalizacja jest ważna podczas kodowania? szczegółowo przedstawia przykład z własnego doświadczenia, w którym przestarzała optymalizacja spowodowała spowolnienie rzędu wielkości.

W wielu procesorach niewyrównane dostępy do pamięci są znacznie droższe niż wyrównane dostępy do pamięci. Spakowanie kilku skrótów do struktury może po prostu oznaczać, że twój program musi wykonać operację spakowania / rozpakowania za każdym razem , gdy dotkniesz dowolnej wartości.

Z tego powodu nowoczesne kompilatory ignorują twoje sugestie. Jak komentuje Nikie :

Przy standardowych ustawieniach kompilatora pakowania / wyrównania zmienne i tak zostaną wyrównane do granic 4 bajtów, więc może nie być żadnej różnicy.

Po drugie, zgadnij, że kompilator jest na własne ryzyko.

Jest miejsce na takie optymalizacje podczas pracy z terabajtowymi zestawami danych lub wbudowanymi mikrokontrolerami, ale dla większości z nas nie jest to tak naprawdę problemem.

Mark Booth
źródło
3

Główną różnicą jest to, że short int zajmuje 2 bajty pamięci, podczas gdy int zajmuje 4 bajty, a short int ma mniejszą wartość, ale możemy też to nazwać, aby był jeszcze mniejszy:

To jest niepoprawne. Nie można zakładać, ile bajtów zawiera każdy typ, poza tym, charże jest to jeden bajt i co najmniej 8 bitów na bajt, a wielkość każdego typu jest większa lub równa poprzedniej.

Korzyści z wydajności są niezwykle małe w przypadku zmiennych stosu - prawdopodobnie i tak zostaną wyrównane / uzupełnione.

Z tego powodu shorti longobecnie nie mają praktycznie żadnego zastosowania, a prawie zawsze lepiej jest używać int.


Oczywiście jest też coś, stdint.hco idealnie nadaje się do użycia, gdy się intgo nie tnie. Jeśli kiedykolwiek przydzielasz ogromne tablice liczb całkowitych / struktur, to intX_tma sens, ponieważ możesz być wydajny i polegać na rozmiarze tego typu. Nie jest to wcale przedwczesne, ponieważ można zaoszczędzić megabajty pamięci.

Pubby
źródło
1
W rzeczywistości, wraz z pojawieniem się środowisk 64-bitowych, longmogą się różnić od int. Jeśli twoim kompilatorem jest LP64, intma 32 bity i long64 bity, a przekonasz się, że ints może być nadal wyrównany do 4 bajtów (na przykład mój kompilator ma taką opcję).
JeremyP
1
@JeremyP Tak, powiedziałem inaczej czy coś?
Pubby
Twoje ostatnie zdanie, które mówi krótko i długo, praktycznie nie ma zastosowania. Długi z pewnością ma zastosowanie, choćby jako podstawowy typint64_t
JeremyP
@JeremyP: Możesz żyć dobrze z int i długo długo.
gnasher729,
@ gnasher729: Czego używasz, jeśli potrzebujesz zmiennej, która może pomieścić wartości ponad 65 tysięcy, ale nigdy nie więcej niż miliard? int32_t, int_fast32_ti longwszystkie są dobrymi opcjami, long longjest po prostu marnotrawstwem i intnieprzenośnym.
Ben Voigt,
3

Będzie to z pewnego rodzaju punktu widzenia OOP i / lub przedsiębiorczości / aplikacji i może nie mieć zastosowania w niektórych dziedzinach / domenach, ale chciałbym raczej przywołać pojęcie prymitywnej obsesji .

Dobrym pomysłem jest używanie różnych typów danych dla różnych rodzajów informacji w aplikacji. Jednak prawdopodobnie NIE jest dobrym pomysłem stosowanie do tego wbudowanych typów, chyba że masz poważne problemy z wydajnością (które zostały zmierzone i zweryfikowane itd.).

Jeśli chcemy modelowych temperatury w stopniach Kelvina w naszej aplikacji, możemy użyć ushortlub uintlub coś podobnego do oznaczenia, że „pojęcie stopni Kelvina negatywnych jest absurdalne i błąd logiczny domeny”. Ideą tego jest dźwięk, ale nie idziesz do końca. Uświadomiliśmy sobie, że nie możemy mieć wartości ujemnych, więc jest to przydatne, jeśli możemy uzyskać kompilator, aby upewnić się, że nikt nie przypisuje wartości ujemnej do temperatury Kelvina. Jest również prawdą, że nie można wykonywać bitowych operacji na temperaturach. I nie można dodać miary masy (kg) do temperatury (K). Ale jeśli modelujesz zarówno temperaturę, jak i masę jako uints, możemy to zrobić.

Używanie wbudowanych typów do modelowania naszych jednostek DOMAIN musi doprowadzić do niechlujnego kodu oraz niektórych nieodebranych czeków i uszkodzonych niezmienników. Nawet jeśli typ przechwytuje NIEKTÓRE części bytu (nie może być ujemny), musi ominąć inne (nie może być stosowany w dowolnych wyrażeniach arytmetycznych, nie może być traktowany jako tablica bitów itp.)

Rozwiązaniem jest zdefiniowanie nowych typów, które zawierają niezmienniki. W ten sposób możesz upewnić się, że pieniądze są pieniędzmi, a odległości są odległościami, i nie możesz ich sumować, i nie możesz stworzyć ujemnej odległości, ale MOŻESZ stworzyć ujemną kwotę pieniędzy (lub długu). Oczywiście te typy będą używać wbudowanych typów wewnętrznie, ale jest to ukryte przed klientami. Odnosząc się do twojego pytania dotyczącego wydajności / zużycia pamięci, tego rodzaju rzeczy mogą pozwolić ci zmienić sposób, w jaki rzeczy są przechowywane wewnętrznie, bez zmiany interfejsu twoich funkcji, które działają na twoich domenowych domenach, jeśli dowiesz się, że to cholerne, a shortjest po prostu zbyt cholerne duży.

sara
źródło
1

Tak oczywiście. Dobrym pomysłem jest stosowanie uint_least8_tdo słowników, tablic ogromnych stałych, buforów itp. Lepiej jest używać uint_fast8_tdo celów przetwarzania.

uint8_least_t(przechowywanie) -> uint8_fast_t(przetwarzanie) -> uint8_least_t(przechowywanie).

Na przykład bierzesz 8-bitowy symbol source, 16-bitowe kody dictionariesi jakieś 32-bitowe constants. Następnie przetwarzasz z nimi 10-15 bitowe operacje i generujesz 8 bitów destination.

Wyobraźmy sobie, że musisz przetworzyć 2 gigabajty source. Liczba operacji bitowych jest ogromna. Otrzymasz świetny bonus wydajności, jeśli przełączysz się na szybkie typy podczas przetwarzania. Szybkie typy mogą być różne dla każdej rodziny procesorów. Możesz dołączyć stdint.hi wykorzystanie uint_fast8_t, uint_fast16_t, uint_fast32_t, itd.

Możesz użyć uint_least8_tzamiast uint8_tprzenośności. Ale nikt tak naprawdę nie wie, jakie nowoczesne procesory wykorzystają tę funkcję. Maszyna VAC jest dziełem muzealnym. Więc może to przesada.

puchu
źródło
1
Chociaż możesz mieć rację z wymienionymi typami danych, powinieneś wyjaśnić, dlaczego są one lepsze, niż po prostu stwierdzić, że są. Dla osób takich jak ja, które nie są zaznajomione z tymi typami danych, musiałem je wyszukiwać w Google, aby zrozumieć, o czym mówisz.
Peter M,