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:
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ć
- mnożenie to pojedyncza instrukcja (
imul), która jest bardzo szybka
- 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.
unsingedwartość, którą można przedstawić za pomocą 1 bajtu to255. 2) Weź pod uwagę narzuty obliczania optymalnego rozmiaru pamięci i zmniejszania / powiększania obszaru przechowywania zmiennej, gdy zmienia się wartość.