Dlaczego typy zawsze mają określony rozmiar, niezależnie od ich wartości?


149

Implementacje mogą się różnić w zależności od rzeczywistych rozmiarów typów, ale w większości typów, takich jak unsigned int i float, mają zawsze 4 bajty. Ale dlaczego typ zawsze zajmuje określoną ilość pamięci, bez względu na jego wartość? Na przykład, jeśli utworzę następującą liczbę całkowitą o wartości 255

int myInt = 255;

Wtedy myIntzajmowałby 4 bajty z moim kompilatorem. Jednak rzeczywistą wartość 255można przedstawić za pomocą tylko 1 bajtu, więc dlaczego myIntnie miałby zajmować po prostu 1 bajtu pamięci? Lub bardziej uogólniony sposób pytania: dlaczego typ ma tylko jeden rozmiar skojarzony, skoro miejsce wymagane do przedstawienia wartości może być mniejsze niż ten rozmiar?


15
1) „ Jednak rzeczywista wartość 256 może być reprezentowana tylko za pomocą 1 bajtu ” Zła, największa unsingedwartość, którą można przedstawić za pomocą 1 bajtu to 255. 2) Weź pod uwagę narzuty obliczania optymalnego rozmiaru pamięci i zmniejszania / powiększania obszaru przechowywania zmiennej, gdy zmienia się wartość.
Algirdas Preidžius

99
Cóż, kiedy nadejdzie czas na odczytanie wartości z pamięci, w jaki sposób proponujesz, aby maszyna określiła, ile bajtów ma odczytać? Skąd maszyna będzie wiedzieć, gdzie zatrzymać odczytywanie wartości? Będzie to wymagało dodatkowych udogodnień. I w ogólnym przypadku narzut pamięci i wydajności dla tych dodatkowych udogodnień będzie znacznie wyższy niż w przypadku zwykłego użycia ustalonych 4 bajtów na unsigned intwartość.
AnT

74
Naprawdę podoba mi się to pytanie. Chociaż odpowiedź na to pytanie może wydawać się prosta, myślę, że podanie dokładnego wyjaśnienia wymaga dobrego zrozumienia, jak w rzeczywistości działają komputery i architektury komputerów. Większość ludzi prawdopodobnie uzna to za rzecz oczywistą, bez wyczerpującego wyjaśnienia.
andreee

37
Zastanów się, co by się stało, gdybyś dodał 1 do wartości zmiennej, co daje 256, więc musiałaby się rozwinąć. Gdzie się rozszerza? Czy przenosisz resztę pamięci, aby zrobić miejsce? Czy sama zmienna się porusza? Jeśli tak, gdzie się przenosi i jak znajdujesz wskaźniki, które musisz zaktualizować?
molbdnilo

13
@someidiot nie, mylisz się. std::vector<X>zawsze ma ten sam rozmiar, tj. sizeof(std::vector<X>)jest stałą czasu kompilacji.
SergeyA

Odpowiedzi:


131

Kompilator powinien produkować asembler (i ostatecznie kod maszynowy) dla jakiejś maszyny i ogólnie C ++ stara się być sympatyczny dla tej maszyny.

Bycie sympatycznym dla maszyny bazowej oznacza z grubsza: ułatwianie pisania kodu w C ++, który będzie efektywnie mapowany na operacje, które maszyna może wykonać szybko. Dlatego chcemy zapewnić dostęp do typów danych i operacji, które są szybkie i „naturalne” na naszej platformie sprzętowej.

Konkretnie, rozważ konkretną architekturę maszyny. Weźmy aktualną rodzinę Intel x86.

Podręcznik programisty oprogramowania architektury Intel® 64 i IA-32, tom 1 ( łącze ), sekcja 3.4.1 mówi:

32-bitowe rejestry ogólnego przeznaczenia EAX, EBX, ECX, EDX, ESI, EDI, EBP i ESP służą do przechowywania następujących elementów:

• Operandy operacji logicznych i arytmetycznych

• Operandy do obliczania adresu

• Wskaźniki pamięci

Dlatego chcemy, aby kompilator używał tych rejestrów EAX, EBX itp., Kiedy kompiluje prostą arytmetykę liczb całkowitych w C ++. Oznacza to, że kiedy deklaruję int, to powinno być coś kompatybilnego z tymi rejestrami, abym mógł z nich efektywnie korzystać.

Rejestry mają zawsze ten sam rozmiar (tutaj 32 bity), więc mój int zmienne również będą miały zawsze 32 bity. Użyję tego samego układu (little-endian), aby nie musieć wykonywać konwersji za każdym razem, gdy ładuję wartość zmiennej do rejestru lub przechowuję rejestr z powrotem w zmiennej.

Używając godbolt , możemy dokładnie zobaczyć, co kompilator robi dla jakiegoś trywialnego kodu:

int square(int num) {
    return num * num;
}

kompiluje się (z GCC 8.1 i -fomit-frame-pointer -O3dla uproszczenia) do:

square(int):
  imul edi, edi
  mov eax, edi
  ret

to znaczy:

  1. int numparametr został przekazany w rejestrze EDI, co oznacza, że jest dokładnie wielkość i układ Intel spodziewać na rodzimym rejestru. Funkcja nie musi niczego konwertować
  2. mnożenie to pojedyncza instrukcja ( imul), która jest bardzo szybka
  3. zwrócenie wyniku jest po prostu kwestią skopiowania go do innego rejestru (dzwoniący oczekuje, że wynik zostanie umieszczony w EAX)

Edycja: możemy dodać odpowiednie porównanie, aby pokazać różnicę przy użyciu innego niż natywnego układu. Najprostszym przypadkiem jest przechowywanie wartości w innej szerokości niż natywna.

Używając ponownie godbolta , możemy porównać proste mnożenie natywne

unsigned mult (unsigned x, unsigned y)
{
    return x*y;
}

mult(unsigned int, unsigned int):
  mov eax, edi
  imul eax, esi
  ret

z równoważnym kodem dla niestandardowej szerokości

struct pair {
    unsigned x : 31;
    unsigned y : 31;
};

unsigned mult (pair p)
{
    return p.x*p.y;
}

mult(pair):
  mov eax, edi
  shr rdi, 32
  and eax, 2147483647
  and edi, 2147483647
  imul eax, edi
  ret

Wszystkie dodatkowe instrukcje dotyczą konwersji formatu wejściowego (dwie 31-bitowe liczby całkowite bez znaku) na format obsługiwany przez procesor. Gdybyśmy chcieli zapisać wynik z powrotem w wartości 31-bitowej, byłaby jeszcze jedna lub dwie instrukcje, aby to zrobić.

Ta dodatkowa złożoność oznacza, że ​​będziesz się tym przejmować tylko wtedy, gdy oszczędność miejsca jest bardzo ważna. W tym przypadku oszczędzamy tylko dwa bity w porównaniu do używania natywnego unsignedlub uint32_ttypu, który wygenerowałby znacznie prostszy kod.


Uwaga dotycząca rozmiarów dynamicznych:

Powyższy przykład to nadal wartości o stałej szerokości, a nie o zmiennej szerokości, ale szerokość (i wyrównanie) nie są już zgodne z rejestrami natywnymi.

Platforma x86 ma kilka natywnych rozmiarów, w tym 8-bitowe i 16-bitowe oprócz głównego 32-bitowego (dla uproszczenia zajmuję się trybem 64-bitowym i różnymi innymi rzeczami).

Te typy (char, int8_t, uint8_t, int16_t itp.) Są również bezpośrednio obsługiwane przez architekturę - częściowo dla wstecznej kompatybilności ze starszymi 8086/286/386 / itp. itp. zestawy instrukcji.

Z pewnością jest tak, że wybierając najmniejszy naturalny stały rozmiar , który wystarczy, może być dobrą praktyką - nadal są to szybkie, pojedyncze instrukcje ładują się i zapisują, nadal otrzymujesz arytmetykę natywną o pełnej prędkości, a nawet możesz poprawić wydajność, zmniejszenie błędów pamięci podręcznej.

To bardzo różni się od kodowania o zmiennej długości - pracowałem z niektórymi z nich i są okropne. Każde ładowanie staje się pętlą zamiast pojedynczej instrukcji. Każdy sklep to także pętla. Każda struktura ma zmienną długość, więc nie można w naturalny sposób używać tablic.


Kolejna uwaga na temat wydajności

W kolejnych komentarzach używałeś słowa „wydajne”, o ile mogę powiedzieć, w odniesieniu do rozmiaru pamięci. Czasami decydujemy się na zminimalizowanie rozmiaru magazynu - może to być ważne, gdy zapisujemy bardzo dużą liczbę wartości w plikach lub wysyłamy je przez sieć. Kompromis polega na tym, że musimy załadować te wartości do rejestrów, aby cokolwiek z nimi zrobić , a wykonanie konwersji nie jest darmowe.

Kiedy rozmawiamy o wydajności, musimy wiedzieć, co optymalizujemy i jakie są kompromisy. Korzystanie z nienatywnych typów pamięci masowej jest jednym ze sposobów zamiany szybkości przetwarzania na miejsce i czasami ma sens. Korzystanie z pamięci o zmiennej długości (przynajmniej dla typów arytmetycznych) zapewnia większą szybkość przetwarzania (oraz złożoność kodu i czas programisty), co często minimalizuje dalsze oszczędności miejsca.

Kara za szybkość, za którą płacisz, oznacza, że ​​opłaca się to tylko wtedy, gdy musisz absolutnie zminimalizować przepustowość lub długoterminową pamięć, aw takich przypadkach zwykle łatwiej jest użyć prostego i naturalnego formatu - a następnie po prostu skompresować go za pomocą systemu ogólnego przeznaczenia (jak zip, gzip, bzip2, xy lub cokolwiek).


tl; dr

Każda platforma ma jedną architekturę, ale możesz wymyślić zasadniczo nieograniczoną liczbę różnych sposobów reprezentowania danych. Nie jest rozsądne, aby jakikolwiek język zapewniał nieograniczoną liczbę wbudowanych typów danych. Tak więc C ++ zapewnia niejawny dostęp do natywnego, naturalnego zestawu typów danych platformy i umożliwia samodzielne kodowanie dowolnej innej (innej niż natywna) reprezentacji.


Patrzę na wszystkie fajne odpowiedzi, próbując zrozumieć je wszystkie ... Więc jeśli chodzi o twoją odpowiedź, czy rozmiar dynamiczny nie byłby mniejszy niż 32 bity dla liczby całkowitej, a nie tylko pozwalał na więcej zmiennych w rejestrze ? Jeśli endianess jest taki sam, dlaczego nie byłoby to optymalne?
Nichlas Uden

7
@asd, ale ile rejestrów użyjesz w kodzie, który obliczy, ile zmiennych jest obecnie przechowywanych w rejestrze?
user253751

1
FWIW często pakuje się wiele wartości w najmniejszą dostępną przestrzeń, w której decydujesz, że oszczędność miejsca jest ważniejsza niż szybkość pakowania i rozpakowywania. Po prostu nie można generalnie operować na nich w sposób naturalny w ich spakowanej formie, ponieważ procesor nie wie, jak poprawnie wykonywać obliczenia arytmetyczne na czymkolwiek innym niż wbudowane rejestry. Wyszukaj BCD pod kątem częściowego wyjątku z obsługą procesorów
Bezużyteczne

3
Jeśli faktycznie nie trzeba wszystkie 32 bity dla pewnej wartości, nadal trzeba gdzieś przechowywać długość, więc teraz muszę więcej niż 32 bity w niektórych przypadkach.
Bezużyteczne

1
+1. Uwaga dotycząca „prostego i naturalnego formatu, a następnie kompresji” jest zazwyczaj lepsza: Jest to z pewnością ogólnie prawda , ale : dla niektórych danych VLQ-każda-wartość-następnie-skompresuj-całość działa znacznie lepiej niż zwykła kompresja -całkowicie, aw przypadku niektórych aplikacji dane nie mogą być skompresowane razem , ponieważ są albo rozbieżne (jak w gitmetadanych), albo faktycznie przechowujesz je w pamięci i czasami trzeba losowo uzyskać dostęp lub zmodyfikować kilka, ale nie większość wartości (jak w silnikach renderujących HTML + CSS), a zatem można je przetransferować tylko przy użyciu czegoś takiego jak VLQ w miejscu.
mtraceur

139

Ponieważ typy zasadniczo reprezentują pamięć i są definiowane w kategoriach maksymalnej wartości, jaką mogą przechowywać, a nie bieżącej wartości.

Bardzo prostą analogią byłby dom - dom ma stałą wielkość, niezależnie od tego, ile osób w nim mieszka, a ponadto istnieje kodeks budowlany, który określa maksymalną liczbę osób, które mogą mieszkać w domu o określonej wielkości.

Jednak nawet jeśli jedna osoba mieszka w domu, który może pomieścić 10 osób, aktualna liczba osób nie będzie miała wpływu na wielkość domu.


31
Podoba mi się analogia. Gdybyśmy go trochę rozszerzyli, moglibyśmy sobie wyobrazić użycie języka programowania, który nie używa stałych rozmiarów pamięci dla typów, co byłoby podobne do burzenia pomieszczeń w naszym domu, gdy nie były używane, i przebudowywania ich, gdy to konieczne (tj. mnóstwo kosztów ogólnych, kiedy moglibyśmy po prostu zbudować kilka domów i zostawić je na czas, gdy potrzebujemy).
ahouse101

5
„Ponieważ typy zasadniczo reprezentują pamięć”, nie dotyczy to wszystkich języków (na przykład maszynopisu)
corvus_192

56
Tagi @ corvus_192 mają znaczenie. To pytanie jest oznaczone tagiem C ++, a nie „maszynopis”
SergeyA

4
@ ahouse101 Rzeczywiście, istnieje wiele języków, które mają liczby całkowite o nieograniczonej precyzji, a ich liczba rośnie w miarę potrzeb. Te języki nie wymagają przydzielania stałej pamięci dla zmiennych, są wewnętrznie implementowane jako odwołania do obiektów. Przykłady: Lisp, Python.
Barmar,

2
@jamesqf Prawdopodobnie nie jest przypadkiem, że arytmetyka MP została po raz pierwszy zastosowana w Lispie, który również zajmował się automatycznym zarządzaniem pamięcią. Projektanci uznali, że wpływ na wydajność jest drugorzędny w stosunku do łatwości programowania. Opracowano również techniki optymalizacji, aby zminimalizować wpływ.
Barmar

44

To optymalizacja i uproszczenie.

Możesz mieć obiekty o stałych rozmiarach. W ten sposób przechowując wartość.
Lub możesz mieć obiekty o zmiennej wielkości. Ale przechowywanie wartości i rozmiaru.

obiekty o stałych rozmiarach

Kod, który manipuluje liczbą, nie musi martwić się o rozmiar. Zakładasz, że zawsze używasz 4 bajtów i sprawiasz, że kod jest bardzo prosty.

Obiekty o dynamicznych rozmiarach

Kod, który manipuluje liczbą, musi rozumieć podczas odczytywania zmiennej, że musi odczytywać wartość i rozmiar. Użyj rozmiaru, aby upewnić się, że wszystkie wysokie bity są zerowe w rejestrze.

Kiedy umieszczasz wartość z powrotem w pamięci, jeśli wartość nie przekroczyła swojego obecnego rozmiaru, po prostu umieść wartość z powrotem w pamięci. Ale jeśli wartość zmniejszyła się lub wzrosła, musisz przenieść miejsce przechowywania obiektu do innej lokalizacji w pamięci, aby upewnić się, że się nie przepełni. Teraz musisz śledzić położenie tego numeru (ponieważ może się on przesunąć, jeśli wzrośnie zbyt duży w stosunku do jego rozmiaru). Musisz także śledzić wszystkie nieużywane zmienne lokalizacje, aby potencjalnie można było je ponownie wykorzystać.

Podsumowanie

Kod wygenerowany dla obiektów o stałym rozmiarze jest dużo prostszy.

Uwaga

Kompresja wykorzystuje fakt, że 255 zmieści się w jednym bajcie. Istnieją schematy kompresji do przechowywania dużych zestawów danych, które będą aktywnie wykorzystywać różne wartości rozmiaru dla różnych liczb. Ale ponieważ nie są to dane na żywo, nie masz złożoności opisanych powyżej. Używasz mniej miejsca do przechowywania danych kosztem kompresji / dekompresji danych do przechowywania.


4
To dla mnie najlepsza odpowiedź: jak sprawdzasz rozmiar? Z większą pamięcią?
online Thomas

@ThomasMoors Tak, dokładnie: z większą pamięcią. Jeśli na przykład masz tablicę dynamiczną, to niektóre z nich intbędą przechowywać liczbę elementów w tej tablicy. Że intsam będzie musiał ponownie ustaloną wielkość.
Alfe

1
@ThomasMoors są dwie powszechnie używane opcje, z których obie wymagają dodatkowej pamięci - albo masz pole (o stałym rozmiarze) informujące o ilości danych (np. Int określający rozmiar tablicy lub ciągi „w stylu pascala”, gdzie pierwszy zawiera liczbę znaków) lub alternatywnie możesz mieć łańcuch (lub bardziej złożoną strukturę), w którym każdy element w jakiś sposób odnotowuje, czy jest ostatni - np. ciągi zakończone zerem lub większość form połączonych list.
Peteris,

27

Ponieważ w języku takim jak C ++ celem projektu jest kompilacja prostych operacji do prostych instrukcji maszynowych.

Wszystkie główne zestawy instrukcji procesora działają z typami o stałej szerokości , a jeśli chcesz wykonywać typy o zmiennej szerokości , musisz wykonać wiele instrukcji maszynowych, aby je obsłużyć.

Co do tego, dlaczego podstawowy sprzęt komputerowy jest taki: jest tak, ponieważ jest prostszy i bardziej wydajny w wielu przypadkach (ale nie we wszystkich).

Wyobraź sobie komputer jako kawałek taśmy:

| xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | ...

Jeśli po prostu powiesz komputerowi, aby spojrzał na pierwszy bajt na taśmie, xxskąd ma on wiedzieć, czy typ na tym się kończy, czy też przechodzi do następnego bajtu? Jeśli masz liczbę taką jak 255(szesnastkowa FF) lub taką jak 65535(szesnastkowa FFFF), pierwszy bajt jest zawsze FF.

Więc skąd wiesz? Musisz dodać dodatkową logikę i „przeładować” znaczenie co najmniej jednej wartości bitowej lub bajtowej, aby wskazać, że wartość jest kontynuowana do następnego bajtu. Ta logika nigdy nie jest „darmowa”, albo emulujesz ją w oprogramowaniu, albo dodajesz kilka dodatkowych tranzystorów do procesora, aby to zrobić.

Odzwierciedlają to typy języków o stałej szerokości, takie jak C i C ++.

To nie muszą być w ten sposób, a bardziej abstrakcyjne w językach, które są mniej zainteresowane z mapowaniem do maksymalnie efektywnego kodu mogą swobodnie korzystać z kodowania o zmiennej szerokości (znany również jako „zmienne ilości długość” lub VLQ) dla typów liczbowych.

Dalsza lektura: Jeśli szukasz słowa „ilość o zmiennej długości”, możesz znaleźć kilka przykładów, w których tego rodzaju kodowanie jest rzeczywiście wydajne i warte dodatkowej logiki. Zwykle dzieje się tak, gdy trzeba przechowywać ogromną liczbę wartości, które mogą znajdować się w dowolnym miejscu w dużym zakresie, ale większość wartości ma tendencję do niewielkiego podzakresu.


Zwróć uwagę, że jeśli kompilator może udowodnić , że może uchronić się przed przechowywaniem wartości w mniejszej ilości miejsca bez łamania kodu (na przykład jest to zmienna widoczna tylko wewnętrznie w ramach jednej jednostki tłumaczeniowej), a jego heurystyka optymalizacji sugeruje, że tak ll być bardziej wydajny na sprzęcie docelowym, to zupełnie wolno zoptymalizować go odpowiednio i przechowywać go w mniejszej ilości miejsca, tak długo, jak reszta prac kodem „jak gdyby” to nie standardowe rzeczy.

Ale gdy kod musi współdziałać z innym kodem, który może być skompilowany oddzielnie, rozmiary muszą pozostać spójne lub zapewnić, że każdy fragment kodu jest zgodny z tą samą konwencją.

Bo jeśli nie jest to spójne, pojawia się taka komplikacja: co jeśli mam, int x = 255;ale później w kodzie robię x = y? Gdyby intmogła mieć zmienną szerokość, kompilator musiałby wiedzieć z wyprzedzeniem, aby wstępnie przydzielić maksymalną ilość miejsca, jakiej będzie potrzebować. Nie zawsze jest to możliwe, ponieważ co, jeśli yargument jest przekazywany z innego fragmentu kodu, który jest kompilowany oddzielnie?


26

Java używa do tego klas o nazwach „BigInteger” i „BigDecimal”, podobnie jak interfejs klasy C ++ GMP C ++ (dzięki Digital Trauma). Jeśli chcesz, możesz to łatwo zrobić samodzielnie w praktycznie dowolnym języku.

Procesory zawsze miały możliwość korzystania z BCD (Binary Coded Decimal), który jest przeznaczony do obsługi operacji o dowolnej długości (ale masz tendencję do ręcznego operowania jednym bajtem na raz, co byłoby WOLNE według dzisiejszych standardów GPU).

Dlaczego nie używamy tych lub innych podobnych rozwiązań? Występ. Wasze najbardziej wydajne języki nie mogą sobie pozwolić na rozszerzanie zmiennej w środku jakiejś ścisłej pętli - byłoby to bardzo niedeterministyczne.

W sytuacjach magazynowania masowego i transportu wartości spakowane są często JEDYNYM rodzajem wartości, którego można by użyć. Na przykład pakiet muzyki / wideo przesyłany strumieniowo do komputera może spędzić trochę czasu, aby określić, czy następna wartość to 2 bajty, czy 4 bajty jako optymalizacja rozmiaru.

Kiedy już znajdzie się na komputerze, gdzie będzie można jej używać, pamięć jest tania, ale szybkość i złożoność zmiennych o zmiennym rozmiarze nie są… to naprawdę jedyny powód.


4
Cieszę się, że ktoś wspomina o BigInteger. Nie chodzi o to, że to głupi pomysł, po prostu ma to sens tylko w przypadku bardzo dużych liczb.
Max Barraclough

1
Mówiąc pedantycznie, masz na myśli niezwykle dokładne liczby :) Cóż, przynajmniej w przypadku BigDecimal ...
Bill K

2
A ponieważ jest to oznaczone jako c ++ , prawdopodobnie warto wspomnieć o interfejsie klasy GMP C ++ , który jest tym samym pomysłem, co Big * Javy.
Digital Trauma,

20

Ponieważ posiadanie prostych typów o dynamicznych rozmiarach byłoby bardzo skomplikowane i wymagające obliczeń. Nie jestem pewien, czy to byłoby w ogóle możliwe.
Komputer musiałby sprawdzić, ile bitów zajmuje liczba po każdej zmianie jej wartości. To byłoby całkiem sporo dodatkowych operacji. O wiele trudniej byłoby wykonać obliczenia, gdy nie znasz rozmiarów zmiennych podczas kompilacji.

Aby obsłużyć dynamiczne rozmiary zmiennych, komputer musiałby faktycznie pamiętać, ile bajtów ma teraz zmienna, co ... wymagałoby dodatkowej pamięci do przechowywania tych informacji. I te informacje musiałyby być analizowane przed każdą operacją na zmiennej, aby wybrać właściwą instrukcję procesora.

Aby lepiej zrozumieć, jak działa komputer i dlaczego zmienne mają stałe rozmiary, naucz się podstaw języka asemblera.

Chociaż przypuszczam, że byłoby możliwe osiągnięcie czegoś takiego za pomocą wartości constexpr. Jednak spowodowałoby to, że kod byłby mniej przewidywalny dla programisty. Przypuszczam, że niektóre optymalizacje kompilatora mogą zrobić coś takiego, ale ukrywają to przed programistą, aby wszystko było proste.

Opisałem tutaj tylko problemy, które dotyczą wykonania programu. Pominąłem wszystkie problemy, które należałoby rozwiązać, aby zaoszczędzić pamięć, zmniejszając rozmiary zmiennych. Szczerze mówiąc, nie sądzę, żeby to było w ogóle możliwe.


Podsumowując, użycie mniejszych zmiennych niż zadeklarowane ma sens tylko wtedy, gdy ich wartości są znane podczas kompilacji. Jest całkiem prawdopodobne, że robią to nowoczesne kompilatory. W innych przypadkach spowodowałoby to zbyt wiele trudnych lub nawet nierozwiązywalnych problemów.


Bardzo wątpię, że coś takiego dzieje się w czasie kompilacji. Oszczędzanie pamięci kompilatora w ten sposób nie ma sensu i to jedyna korzyść.
Bartek Banachewicz

1
Myślałem raczej o operacjach takich jak mnożenie zmiennej constexpr przez normalną zmienną. Na przykład mamy (teoretycznie) 8-bajtową zmienną constexpr z wartością 56i mnożymy ją przez jakąś 2-bajtową zmienną. Na niektórych architekturach operacja 64-bitowa byłaby bardziej obciążona obliczeniami, więc kompilator mógł ją zoptymalizować, aby wykonywać tylko 16-bitowe mnożenie.
NO_NAME

Niektóre implementacje APL i niektóre języki z rodziny SNOBOL (myślę, że SPITBOL? Może Icon) zrobiły dokładnie to (ze szczegółowością): dynamicznie zmieniają format reprezentacji w zależności od rzeczywistych wartości. APL przejdzie od wartości logicznej do liczby całkowitej do liczby zmiennoprzecinkowej iz powrotem. SPITBOL przejdzie z reprezentacji kolumnowej wartości logicznych (8 oddzielnych tablic boolowskich przechowywanych w tablicy bajtów) do liczb całkowitych (IIRC).
davidbak

16

Wtedy myIntzajmie 4 bajty z moim kompilatorem. Jednak rzeczywistą wartość 255można przedstawić za pomocą tylko 1 bajtu, więc dlaczego myIntnie miałby zajmować tylko 1 bajtu pamięci?

Jest to znane jako kodowanie o zmiennej długości , istnieją różne zdefiniowane kodowania, na przykład VLQ . Jednak jednym z najbardziej znanych jest prawdopodobnie UTF-8 : UTF-8 koduje punkty kodowe na zmiennej liczbie bajtów, od 1 do 4.

Lub bardziej uogólniony sposób pytania: dlaczego typ ma tylko jeden rozmiar skojarzony, skoro miejsce wymagane do przedstawienia wartości może być mniejsze niż ten rozmiar?

Jak zawsze w inżynierii, chodzi o kompromisy. Nie ma rozwiązania, które ma same zalety, więc podczas projektowania rozwiązania trzeba wyważyć zalety i kompromisy.

Projekt, który został przyjęty, polegał na użyciu podstawowych typów o stałym rozmiarze, a sprzęt / języki po prostu odleciały.

Jaka jest więc podstawowa słabość kodowania zmiennego , która spowodowała, że ​​zostało ono odrzucone na rzecz schematów bardziej wymagających pamięci? Brak losowego adresowania .

Jaki jest indeks bajtu, w którym czwarty punkt kodowy zaczyna się w ciągu znaków UTF-8?

Zależy to od wartości poprzednich punktów kodowych, wymagany jest skan liniowy.

Z pewnością istnieją schematy kodowania o zmiennej długości, które są lepsze w przypadku losowego adresowania?

Tak, ale są też bardziej skomplikowane. Jeśli istnieje idealny, jeszcze go nie widziałem.

Czy przypadkowe adresowanie naprawdę ma znaczenie?

O tak!

Rzecz w tym, że każdy rodzaj agregacji / tablicy opiera się na typach o stałym rozmiarze:

  • Uzyskujesz dostęp do trzeciego pola struct? Losowe adresowanie!
  • Masz dostęp do trzeciego elementu tablicy? Losowe adresowanie!

Co oznacza, że ​​zasadniczo masz następujący kompromis:

Typy o stałym rozmiarze LUB Liniowe skanowanie pamięci


To nie jest aż tak duży problem, jak się wydaje. Zawsze możesz użyć tabel wektorowych. Występuje narzut pamięci i dodatkowe pobieranie, ale skanowanie liniowe nie jest konieczne.
Artelius

2
@Artelius: Jak kodujesz tablicę wektorów, gdy liczby całkowite mają zmienną szerokość? Ponadto, jaki jest narzut pamięci w tablicy wektorów podczas kodowania jednej dla liczb całkowitych, które zajmują od 1 do 4 bajtów pamięci?
Matthieu M.

Słuchaj, masz rację, w konkretnym przykładzie podanym przez OP, używanie tabel wektorowych ma zerową przewagę. Zamiast tworzyć tabelę wektorów, równie dobrze możesz umieścić dane w tablicy elementów o stałym rozmiarze. Jednak PO zażądał również bardziej ogólnej odpowiedzi. W Pythonie tablica liczb całkowitych jest tablicą wektorową liczb całkowitych o zmiennej wielkości! Nie dlatego, że rozwiązuje ten problem, ale dlatego, że Python nie wie w czasie kompilacji, czy elementy listy będą liczbami całkowitymi, zmiennoprzecinkowymi, dyktami, ciągami lub listami, które oczywiście mają różne rozmiary.
Artelius

@Artelius: Zauważ, że w Pythonie tablica zawiera wskaźniki o stałej wielkości do elementów; to sprawia, że ​​dostanie się do elementu jest O (1) kosztem pośredniego kierunku.
Matthieu M.,

16

Pamięć komputera jest podzielona na kolejno adresowane fragmenty o określonej wielkości (często 8-bitowe i określane jako bajty), a większość komputerów jest zaprojektowana tak, aby efektywnie uzyskiwać dostęp do sekwencji bajtów, które mają kolejne adresy.

Jeśli adres obiektu nigdy nie zmienia się w czasie jego istnienia, kod podany jego adres może szybko uzyskać dostęp do danego obiektu. Zasadniczym ograniczeniem tego podejścia jest jednak to, że jeśli adres zostanie przypisany do adresu X, a następnie inny adres zostanie przypisany do adresu Y, który jest oddalony o N bajtów, wówczas X nie będzie w stanie urosnąć większy niż N bajtów w ciągu życia Y, chyba że zostanie przeniesiony X lub Y. Aby X mógł się poruszać, konieczne byłoby, aby wszystko we wszechświecie, które zawiera adres X, zostało zaktualizowane, aby odzwierciedlało nowy, a także, aby Y mógł się poruszać. Chociaż można zaprojektować system ułatwiający takie aktualizacje (zarówno Java, jak i .NET radzą sobie z tym całkiem dobrze), znacznie wydajniejsza jest praca z obiektami, które pozostaną w tej samej lokalizacji przez cały okres ich użytkowania,


„X nie będzie w stanie urosnąć większy niż N bajtów w okresie życia Y, chyba że zostanie przeniesiony X lub Y. Aby X mógł się poruszać, konieczne byłoby zaktualizowanie wszystkiego we wszechświecie, które zawiera adres X, tak, aby odzwierciedlało nowy, podobnie jak Y, aby się poruszyć. " To jest najistotniejszy punkt IMO: obiekty, które używają tylko takiego rozmiaru, jaki potrzebuje ich aktualna wartość, musiałyby dodać tony narzutu dla rozmiarów / wartowników, przenoszenia pamięci, wykresów referencyjnych itp. I całkiem oczywiste, gdy zastanowimy się, jak to w ogóle mogłoby działać ... ale mimo to bardzo warto to jasno powiedzieć, zwłaszcza że tak niewielu innych to zrobiło.
underscore_d

@underscore_d: Języki takie jak Javascript, które zostały zaprojektowane od podstaw do obsługi obiektów o zmiennej wielkości, mogą być w tym niezwykle wydajne. Z drugiej strony, chociaż można uprościć systemy obiektów o zmiennej wielkości i można je uczynić szybko, proste implementacje są powolne, a szybkie implementacje są niezwykle złożone.
supercat

13

Krótka odpowiedź brzmi: ponieważ tak mówi standard C ++.

Długa odpowiedź brzmi: to, co można zrobić na komputerze, jest ostatecznie ograniczone przez sprzęt. Oczywiście możliwe jest zakodowanie liczby całkowitej do zmiennej liczby bajtów w celu przechowywania, ale odczytanie jej wymagałoby albo specjalnych instrukcji procesora, aby działało wydajnie, albo można by zaimplementować to w oprogramowaniu, ale wtedy byłoby to strasznie wolne. W CPU dostępne są operacje o stałym rozmiarze do ładowania wartości o predefiniowanych szerokościach, nie ma żadnych dla zmiennych szerokości.

Kolejną kwestią do rozważenia jest sposób działania pamięci komputera. Powiedzmy, że Twój typ liczby całkowitej może zająć od 1 do 4 bajtów pamięci. Załóżmy, że przechowujesz wartość 42 w swojej liczbie całkowitej: zajmuje ona 1 bajt i umieszczasz ją pod adresem pamięci X. Następnie przechowujesz następną zmienną w lokalizacji X + 1 (nie rozważam wyrównania w tym miejscu) i tak dalej . Później zdecydujesz się zmienić swoją wartość na 6424.

Ale to nie mieści się w jednym bajcie! Więc co robisz? Gdzie kładziesz resztę? Masz już coś na X + 1, więc nie możesz tego tam umieścić. Gdzieś indziej? Skąd będziesz wiedzieć później, gdzie? Pamięć komputera nie obsługuje semantyki wstawiania: nie można po prostu umieścić czegoś w jakimś miejscu i odłożyć wszystkiego na bok, aby zrobić miejsce!

Poza tym: to, o czym mówisz, to tak naprawdę obszar kompresji danych. Istnieją algorytmy kompresji, które pakują wszystko mocniej, więc przynajmniej niektóre z nich rozważą użycie większej ilości miejsca na liczbę całkowitą, niż to konieczne. Jednak skompresowane dane nie są łatwe do zmodyfikowania (jeśli w ogóle to możliwe) i po prostu są ponownie kompresowane za każdym razem, gdy wprowadzasz w nich jakiekolwiek zmiany.


11

Zrobienie tego daje całkiem znaczące korzyści w zakresie wydajności w czasie wykonywania. Gdybyś miał operować na typach o zmiennej wielkości, musiałbyś zdekodować każdą liczbę przed wykonaniem operacji (instrukcje kodu maszynowego mają zwykle stałą szerokość), wykonać operację, a następnie znaleźć miejsce w pamięci wystarczająco duże, aby pomieścić wynik. To są bardzo trudne operacje. O wiele łatwiej jest po prostu przechowywać wszystkie dane w sposób nieco nieefektywny.

Nie zawsze tak się to robi. Rozważ protokół Google Protobuf. Protobufy są zaprojektowane do bardzo wydajnego przesyłania danych. Zmniejszenie liczby przesyłanych bajtów jest warte kosztów dodatkowych instrukcji podczas operacji na danych. W związku z tym protobufs używają kodowania, które koduje liczby całkowite w 1, 2, 3, 4 lub 5 bajtach, a mniejsze liczby całkowite zajmują mniej bajtów. Jednak po odebraniu wiadomości jest ona rozpakowywana do bardziej tradycyjnego formatu liczb całkowitych o stałym rozmiarze, na którym łatwiej jest operować. Tylko podczas transmisji sieciowej używają tak wydajnej przestrzennie liczby całkowitej o zmiennej długości.


11

Podoba mi się analogia do domu Siergieja , ale myślę, że analogia do samochodu byłaby lepsza.

Wyobraź sobie typy zmiennych jako typy samochodów, a ludzi jako dane. Szukając nowego samochodu, wybieramy taki, który najlepiej pasuje do naszego celu. Czy chcemy małego, inteligentnego samochodu, który może pomieścić tylko jedną lub dwie osoby? Albo limuzyną do przewozu większej liczby osób? Oba mają swoje zalety i wady, takie jak prędkość i przebieg paliwa (pomyśl o szybkości i zużyciu pamięci).

Jeśli masz limuzynę i jeździsz sam, nie zmniejszy się, by pasować tylko do Ciebie. Aby to zrobić, musiałbyś sprzedać samochód (czytaj: cofnąć przydział) i kupić sobie nowy, mniejszy.

Kontynuując analogię, możesz pomyśleć o pamięci jako o ogromnym parkingu wypełnionym samochodami, a kiedy idziesz czytać, wyspecjalizowany szofer wyszkolony wyłącznie dla twojego typu samochodu jedzie po to za ciebie. Gdyby Twój samochód mógł zmieniać typ w zależności od znajdujących się w nim osób, za każdym razem, gdy chciałeś odebrać samochód, musiałbyś przyprowadzić całą masę szoferów, ponieważ nigdy nie wiedzieliby, jaki samochód będzie siedział na miejscu.

Innymi słowy, próba określenia, ile pamięci trzeba odczytać w czasie wykonywania, byłaby ogromnie nieefektywna i przeważałaby nad faktem, że być może można by pomieścić na parkingu kilka samochodów więcej.


10

Powodów jest kilka. Jednym z nich jest dodatkowa złożoność obsługi liczb o dowolnych rozmiarach i wynikająca z tego wydajność, ponieważ kompilator nie może już optymalizować na podstawie założenia, że ​​każda liczba int ma dokładnie X bajtów.

Po drugie, przechowywanie prostych typów w ten sposób oznacza, że ​​potrzebują one dodatkowego bajtu do przechowywania długości. Tak więc wartość 255 lub mniej faktycznie wymaga dwóch bajtów w tym nowym systemie, a nie jednego, aw najgorszym przypadku potrzebujesz teraz 5 bajtów zamiast 4. Oznacza to, że wydajność wygrywa pod względem używanej pamięci jest mniejsza niż mogłaby myślę, aw niektórych skrajnych przypadkach może to być strata netto.

Trzecim powodem jest to, że pamięć komputera jest ogólnie adresowalna słowami , a nie bajtami. (Ale zobacz przypis). Słowa to wielokrotność bajtów, zwykle 4 w systemach 32-bitowych i 8 w systemach 64-bitowych. Zwykle nie możesz odczytać pojedynczego bajtu, czytasz słowo i wyodrębniasz n-ty bajt z tego słowa. Oznacza to, że zarówno wyodrębnienie pojedynczych bajtów ze słowa wymaga nieco więcej wysiłku niż samo odczytanie całego słowa, jak i jest bardzo wydajne, jeśli cała pamięć jest równo podzielona na fragmenty o rozmiarze słowa (tj. 4-bajtowe). Ponieważ, jeśli masz dowolne liczby całkowite pływające wokół, możesz skończyć z jedną częścią liczby całkowitej w jednym słowie, a drugą w następnym, co wymaga dwóch odczytów, aby uzyskać pełną liczbę całkowitą.

Przypis: Mówiąc dokładniej, podczas adresowania w bajtach większość systemów ignorowała „nierówne” bajty. To znaczy, adresy 0, 1, 2 i 3 czytają to samo słowo, 4, 5, 6 i 7 czytają następne słowo i tak dalej.

Z tego powodu 32-bitowe systemy miały maksymalnie 4 GB pamięci. Rejestry używane do adresowania lokalizacji w pamięci są zwykle wystarczająco duże, aby pomieścić słowo, tj. 4 bajty, które mają maksymalną wartość (2 ^ 32) -1 = 4294967295. 4294967296 bajtów to 4 GB.


8

Istnieją obiekty, które w pewnym sensie mają zmienną wielkość, w standardowej bibliotece C ++, takie jak std::vector. Jednak wszystkie te dynamicznie przydzielają dodatkową pamięć, której będą potrzebować. Jeśli weźmiesz sizeof(std::vector<int>), otrzymasz stałą, która nie ma nic wspólnego z pamięcią zarządzaną przez obiekt, a jeśli przydzielisz tablicę lub strukturę zawierającą std::vector<int>, zarezerwuje ten rozmiar podstawowy zamiast umieszczać dodatkową pamięć w tej samej tablicy lub strukturze . Istnieje kilka elementów składni języka C, które obsługują coś takiego, w szczególności tablice i struktury o zmiennej długości, ale C ++ nie zdecydował się na ich obsługę.

Standard języka definiuje w ten sposób rozmiar obiektu, aby kompilatory mogły generować wydajny kod. Na przykład, jeśli intzdarzy się, że w jakiejś implementacji ma długość 4 bajtów i zadeklarujesz ajako wskaźnik lub tablicę intwartości, to a[i]tłumaczy się na pseudokod, „dereferencja adresu a + 4 × i”. Można to zrobić w stałym czasie i jest to tak powszechna i ważna operacja, że ​​wiele architektur zestawów instrukcji, w tym x86 i maszyny DEC PDP, na których pierwotnie opracowano C, może to zrobić w jednej instrukcji maszynowej.

Jednym z typowych rzeczywistych przykładów danych przechowywanych kolejno jako jednostki o zmiennej długości są ciągi znaków zakodowane w formacie UTF-8. (Jednak podstawowy typ ciągu UTF-8 dla kompilatora jest nadal chari ma szerokość 1. Umożliwia to interpretowanie ciągów ASCII jako prawidłowego UTF-8 i wiele kodów bibliotecznych, takich jak strlen()i strncpy()kontynuowanie pracy.) Kodowanie dowolnego punktu kodowego UTF-8 może mieć długość od jednego do czterech bajtów, a zatem, jeśli chcesz mieć piąty punkt kodowy UTF-8 w ciągu, może rozpocząć się w dowolnym miejscu od piątego bajtu do siedemnastego bajtu danych. Jedynym sposobem, aby go znaleźć, jest zeskanowanie od początku ciągu i sprawdzenie rozmiaru każdego punktu kodowego. Jeśli chcesz znaleźć piąty grafem, musisz również sprawdzić klasy postaci. Jeśli chciałbyś znaleźć milionowy znak UTF-8 w ciągu, musiałbyś uruchomić tę pętlę milion razy! Jeśli wiesz, że będziesz musiał często pracować z indeksami, możesz raz przejść przez ciąg i zbudować jego indeks - lub możesz przekonwertować na kodowanie o stałej szerokości, takie jak UCS-4. Znalezienie milionowego znaku UCS-4 w ciągu to tylko kwestia dodania czterech milionów do adresu tablicy.

Inną komplikacją związaną z danymi o zmiennej długości jest to, że kiedy je alokujesz, musisz albo przydzielić tyle pamięci, ile może kiedykolwiek wykorzystać, albo dynamicznie zmieniać alokację w razie potrzeby. Przydzielanie środków na najgorszy przypadek może być bardzo rozrzutne. Jeśli potrzebujesz kolejnego bloku pamięci, ponowne przydzielenie może zmusić Cię do skopiowania wszystkich danych do innej lokalizacji, ale zezwolenie na przechowywanie pamięci w niekolejnych fragmentach komplikuje logikę programu.

Tak, to możliwe, aby mieć zmiennej długości bignums zamiast stałej szerokości short int, int, long inta long long int, ale byłoby nieefektywne przeznaczyć i korzystania z nich. Ponadto wszystkie główne procesory są zaprojektowane do wykonywania działań arytmetycznych na rejestrach o stałej szerokości i żaden nie ma instrukcji, które działają bezpośrednio na jakimś rodzaju bignum o zmiennej długości. Trzeba by je było zaimplementować w oprogramowaniu, znacznie wolniej.

W prawdziwym świecie większość (ale nie wszyscy) programistów zdecydowało, że korzyści z kodowania UTF-8, zwłaszcza kompatybilność, są ważne i że tak rzadko zależy nam na czymkolwiek innym niż skanowanie łańcucha od początku do końca lub kopiowanie bloków pamięci, że wady zmiennej szerokości są dopuszczalne. Moglibyśmy użyć spakowanych elementów o zmiennej szerokości podobnych do UTF-8 do innych rzeczy. Ale bardzo rzadko to robimy i nie ma ich w standardowej bibliotece.


7

Dlaczego z typem jest skojarzony tylko jeden rozmiar, skoro miejsce wymagane do przedstawienia wartości może być mniejsze niż ten rozmiar?

Przede wszystkim ze względu na wymagania dotyczące wyrównania.

Zgodnie z basic.align / 1 :

Typy obiektów mają wymagania dotyczące wyrównania, które nakładają ograniczenia na adresy, pod którymi obiekt tego typu może być przydzielony.

Pomyśl o budynku, który ma wiele pięter, a każde piętro ma wiele pomieszczeń.
Każdy pokój ma Twój rozmiar (stała przestrzeń) i może pomieścić N osób lub przedmiotów.
Dzięki znanej wcześniej wielkości pomieszczenia sprawia, że ​​element konstrukcyjny budynku ma dobrą strukturę .

Jeśli pokoje nie są wyrównane, szkielet budynku nie będzie dobrze zbudowany.


7

Może być mniej. Rozważ funkcję:

int foo()
{
    int bar = 1;
    int baz = 42;
    return bar+baz;
}

kompiluje się do kodu asemblera (g ++, x64, usunięte szczegóły)

$43, %eax
ret

Tutaj bari bazkończy się przy użyciu zerowych bajtów do reprezentacji.


5

więc dlaczego myInt nie miałby zajmować tylko 1 bajtu pamięci?

Ponieważ powiedziałeś mu, żeby tak dużo wykorzystywał. Podczas korzystania z unsigned intformatu, niektóre standardy nakazują, że zostaną użyte 4 bajty, a dostępny zakres będzie wynosić od 0 do 4 294 967 295. Gdybyś miał unsigned charzamiast tego użyć an , prawdopodobnie używałbyś tylko 1 bajtu, którego szukasz (w zależności od standardu i C ++ normalnie używa tych standardów).

Gdyby nie te standardy, musiałbyś o tym pamiętać: skąd kompilator lub procesor miałby wiedzieć, że używa tylko 1 bajtu zamiast 4? Później w programie możesz dodać lub pomnożyć tę wartość, co wymagałoby więcej miejsca. Za każdym razem, gdy dokonujesz alokacji pamięci, system operacyjny musi znaleźć, zmapować i udostępnić tę przestrzeń (potencjalnie zamieniając również pamięć na wirtualną pamięć RAM); może to zająć dużo czasu. Jeśli przydzielisz pamięć wcześniej, nie będziesz musiał czekać na zakończenie kolejnego przydziału.

Jeśli chodzi o powód, dla którego używamy 8 bitów na bajt, możesz przyjrzeć się temu: Jaka jest historia, dlaczego bajty mają osiem bitów?

Na marginesie, możesz pozwolić na przepełnienie liczby całkowitej; ale jeśli użyjesz liczby całkowitej ze znakiem, standardy C \ C ++ stwierdzają, że przepełnienie liczb całkowitych powoduje niezdefiniowane zachowanie. Całkowitą przepełnienie


5

Coś prostego, czego brakuje większości odpowiedzi:

ponieważ odpowiada celom projektowym C ++.

Możliwość obliczenia rozmiaru typu w czasie kompilacji pozwala kompilatorowi i programistowi na przyjęcie ogromnej liczby upraszczających założeń, które przynoszą wiele korzyści, szczególnie w odniesieniu do wydajności. Oczywiście typy o stałym rozmiarze wiążą się z pułapkami, takimi jak przepełnienie całkowitoliczbowe. Dlatego różne języki podejmują różne decyzje projektowe. (Na przykład liczby całkowite w Pythonie mają zasadniczo zmienną wielkość).

Prawdopodobnie głównym powodem, dla którego C ++ tak mocno opiera się na typach o stałym rozmiarze, jest jego cel kompatybilności z C. Jednakże, ponieważ C ++ jest językiem z typami statycznymi, który próbuje generować bardzo wydajny kod i unika dodawania rzeczy, które nie zostały wyraźnie określone przez programistę, typy o stałym rozmiarze nadal mają dużo sensu.

Dlaczego więc C wybrał przede wszystkim typy o stałym rozmiarze? Prosty. Został zaprojektowany do pisania systemów operacyjnych z lat 70., oprogramowania serwerowego i narzędzi; rzeczy, które zapewniały infrastrukturę (np. zarządzanie pamięcią) dla innego oprogramowania. Na tak niskim poziomie wydajność jest krytyczna, podobnie jak kompilator robi dokładnie to, co mu każesz.


5

Zmiana rozmiaru zmiennej wymagałaby ponownej alokacji i zwykle nie jest to warte dodatkowych cykli procesora w porównaniu z marnowaniem kilku dodatkowych bajtów pamięci.

Zmienne lokalne trafiają na stos, którym można bardzo szybko manipulować, gdy te zmienne nie zmieniają rozmiaru. Jeśli zdecydowałeś, że chcesz zwiększyć rozmiar zmiennej z 1 bajta do 2 bajtów, musisz przesunąć wszystko na stosie o jeden bajt, aby zrobić dla niej miejsce. Może to potencjalnie kosztować wiele cykli procesora, w zależności od tego, ile rzeczy trzeba przenieść.

Innym sposobem, w jaki możesz to zrobić, jest uczynienie każdej zmiennej wskaźnikiem do lokalizacji sterty, ale w ten sposób marnujesz jeszcze więcej cykli procesora i pamięci. Wskaźniki mają 4 bajty (adresowanie 32-bitowe) lub 8 bajtów (adresowanie 64-bitowe), więc już używasz 4 lub 8 dla wskaźnika, a następnie rzeczywistego rozmiaru danych na stercie. W tym przypadku realokacja nadal wiąże się z kosztami. Jeśli chcesz ponownie przydzielić dane sterty, możesz mieć szczęście i mieć miejsce na ich rozwinięcie w linii, ale czasami musisz przenieść je w inne miejsce na stercie, aby mieć ciągły blok pamięci o żądanym rozmiarze.

Zawsze szybciej jest zdecydować, ile pamięci użyć. Jeśli możesz uniknąć dynamicznego skalowania, uzyskasz wydajność. Marnowanie pamięci jest zwykle warte zwiększenia wydajności. Dlatego komputery mają mnóstwo pamięci. :)


3

Kompilator może wprowadzać wiele zmian w kodzie, o ile wszystko nadal działa (zasada „tak jak jest”).

Możliwe byłoby użycie 8-bitowej instrukcji dosłownego przenoszenia zamiast dłuższej (32/64 bitowej) wymaganej do przeniesienia pełnej int. Jednak do zakończenia ładowania potrzebne byłyby dwie instrukcje, ponieważ przed wykonaniem ładowania musiałbyś najpierw ustawić rejestr na zero.

Po prostu bardziej wydajne (przynajmniej według głównych kompilatorów) jest obsługa wartości jako 32-bitowej. Właściwie nie widziałem jeszcze kompilatora x86 / x86_64, który wykonywałby 8-bitowe ładowanie bez wbudowanego asemblacji.

Jednak sytuacja wygląda inaczej, jeśli chodzi o wersję 64-bitową. Projektując poprzednie rozszerzenia (od 16 do 32 bitów) swoich procesorów, Intel popełnił błąd. Oto dobra reprezentacja tego, jak wyglądają. Głównym wnioskiem jest to, że kiedy piszesz do AL lub AH, nie ma to wpływu na inne (dość sprawiedliwe, o to chodziło i miało to wtedy sens). Ale robi się ciekawie, gdy rozszerzyli go do 32 bitów. Jeśli napiszesz dolne bity (AL, AH lub AX), nic się nie dzieje z górnymi 16 bitami EAX, co oznacza, że ​​jeśli chcesz promować a chardoint , musisz najpierw wyczyścić tę pamięć, ale nie masz możliwości w rzeczywistości używa tylko tych 16 najlepszych bitów, co sprawia, że ​​ta „funkcja” jest bardziej uciążliwa niż cokolwiek innego.

Teraz, mając 64 bity, AMD wykonało znacznie lepszą robotę. Jeśli dotkniesz czegokolwiek w dolnych 32 bitach, górne 32 bity zostaną po prostu ustawione na 0. Prowadzi to do pewnych rzeczywistych optymalizacji, które możesz zobaczyć na tej godle . Możesz zobaczyć, że ładowanie czegoś z 8 lub 32 bitów odbywa się w ten sam sposób, ale gdy używasz 64-bitowych zmiennych, kompilator używa innej instrukcji w zależności od rzeczywistego rozmiaru literału.

Jak więc widzisz, kompilatory mogą całkowicie zmienić rzeczywisty rozmiar zmiennej wewnątrz procesora, gdyby dało to ten sam wynik, ale nie ma sensu robić tego w przypadku mniejszych typów.


korekta: jak gdyby . Nie rozumiem też, w jaki sposób, gdyby można było użyć krótszego ładowania / magazynu, uwolniłoby to pozostałe bajty do użytku - co wydaje się być tym, co dziwi OP: nie tylko unikanie dotykania pamięci, która nie jest potrzebna dla bieżącej wartości, ale możliwość określenia, ile bajtów do odczytania i magicznego przesunięcia całej pamięci RAM w czasie wykonywania, więc spełniony jest jakiś dziwny filozoficzny pomysł na oszczędność miejsca (nie wspominając o gigantycznym koszcie wydajności!) ... Po prostu wygrały instrukcje o mniejszej powierzchni „rozwiązać” to. To, co procesor / system operacyjny musiałby zrobić, byłoby tak złożone, że najlepiej odpowiada na pytanie IMO.
underscore_d

1
Nie można jednak tak naprawdę „oszczędzać pamięci” w rejestrach. O ile nie próbujesz zrobić czegoś dziwnego, nadużywając AH i AL, i tak nie możesz mieć kilku różnych wartości w tym samym rejestrze ogólnego przeznaczenia. Zmienne lokalne często pozostają w rejestrach i nigdy nie trafiają do pamięci RAM, jeśli nie ma takiej potrzeby.
meneldal
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.