Czytam „Język programowania C” K&R i natknąłem się na to stwierdzenie [Wstęp, str. 3]:
Ponieważ typy danych i struktury kontrolne udostępniane przez C są obsługiwane bezpośrednio przez większość komputerów , biblioteka czasu wykonywania wymagana do implementacji samodzielnych programów jest niewielka.
Co oznacza pogrubione stwierdzenie? Czy istnieje przykład typu danych lub struktury kontrolnej, która nie jest obsługiwana bezpośrednio przez komputer?
Odpowiedzi:
Tak, istnieją typy danych, które nie są bezpośrednio obsługiwane.
W wielu systemach wbudowanych nie ma sprzętowej jednostki zmiennoprzecinkowej. Więc kiedy piszesz taki kod:
Jest tłumaczone na coś takiego:
Następnie kompilator lub biblioteka standardowa musi dostarczyć implementację
_float_add()
, która zajmuje pamięć w systemie osadzonym. Jeśli liczysz bajty w naprawdę małym systemie, to może się sumować.Innym typowym przykładem są 64-bitowe liczby całkowite (
long long
w standardzie C od 1999 roku), które nie są bezpośrednio obsługiwane przez systemy 32-bitowe. Stare systemy SPARC nie obsługiwały mnożenia liczb całkowitych, więc mnożenie musiało być dostarczane przez środowisko wykonawcze. Są inne przykłady.Inne języki
Dla porównania, inne języki mają bardziej skomplikowane prymitywy.
Na przykład symbol Lispa wymaga dużej ilości wsparcia w czasie wykonywania, podobnie jak tabele w Lua, łańcuchy znaków w Pythonie, tablice w Fortranie i tak dalej. Równoważne typy w C zwykle albo w ogóle nie są częścią biblioteki standardowej (brak standardowych symboli ani tabel), albo są znacznie prostsze i nie wymagają dużej obsługi w czasie wykonywania (tablice w C to w zasadzie tylko wskaźniki, ciągi zakończone znakiem nul prawie tak proste).
Struktury kontrolne
Godną uwagi strukturą kontrolną, której brakuje w C, jest obsługa wyjątków. Wyjście nielokalne jest ograniczone do
setjmp()
ilongjmp()
, które po prostu zapisują i przywracają określone części stanu procesora. Dla porównania, środowisko uruchomieniowe C ++ musi przejść stos i wywołać destruktory oraz programy obsługi wyjątków.źródło
Właściwie założę się, że treść tego wstępu nie zmieniła się zbytnio od 1978 roku, kiedy Kernighan i Ritchie po raz pierwszy napisali je w pierwszym wydaniu książki i odnoszą się one do historii i ewolucji języka C w tamtym czasie bardziej niż współczesne wdrożenia.
Komputery to zasadniczo tylko banki pamięci i procesory centralne, a każdy procesor działa przy użyciu kodu maszynowego; częścią projektu każdego procesora jest architektura zestawu instrukcji, zwana językiem asemblerowym , która odwzorowuje jeden do jednego z zestawu czytelnych dla człowieka mnemoników na kod maszynowy, czyli wszystkie liczby.
Autorzy języka C - oraz języków B i BCPL, które go bezpośrednio poprzedzały - zamierzali zdefiniować konstrukcje w języku, które były jak najbardziej efektywnie wkompilowane do asemblera ... w rzeczywistości byli do tego zmuszeni przez ograniczenia w celu sprzęt komputerowy. Jak wskazywały inne odpowiedzi, dotyczyło to gałęzi (GOTO i inne sterowanie przepływem w C), ruchów (przypisanie), operacji logicznych (& | ^), podstawowych działań arytmetycznych (dodawanie, odejmowanie, zwiększanie, zmniejszanie) i adresowanie pamięci (wskaźniki ). Dobrym przykładem są operatory pre- / post-inkrementacji i dekrementacji w C, które rzekomo zostały dodane do języka B przez Kena Thompsona, szczególnie dlatego, że po skompilowaniu były w stanie bezpośrednio przetłumaczyć na pojedynczy kod operacji.
To właśnie mieli na myśli autorzy, mówiąc „obsługiwane bezpośrednio przez większość komputerów”. Nie chodziło im o to, że inne języki zawierały typy i struktury, które nie były obsługiwane bezpośrednio - mieli na myśli to, że konstrukcje C zostały przetłumaczone najbardziej bezpośrednio (czasami dosłownie ) na asemblerze.
To bliskie powiązanie z podstawowym zestawem, mimo że nadal zapewnia wszystkie elementy wymagane do programowania strukturalnego, doprowadziło do wczesnego przyjęcia języka C i sprawia, że jest on dziś popularnym językiem w środowiskach, w których wydajność kompilowanego kodu jest nadal kluczowa.
Aby zapoznać się z interesującym zapisem historii języka, zobacz The Development of the C Language - Dennis Ritchie
źródło
Krótka odpowiedź brzmi: większość konstrukcji językowych obsługiwanych przez C jest również obsługiwana przez mikroprocesor komputera docelowego, dlatego skompilowany kod C przekłada się bardzo ładnie i wydajnie na język asemblera mikroprocesora, co skutkuje mniejszym kodem i mniejszą powierzchnią.
Dłuższa odpowiedź wymaga trochę znajomości języka asemblera. W C stwierdzenie takie jak to:
przetłumaczyłoby się na coś takiego w asemblerze:
Porównaj to z czymś takim jak C ++:
Wynikowy kod asemblera (w zależności od wielkości MyClass ()) może dodać do setek linii asemblera.
Bez faktycznego tworzenia programów w języku asemblera, czysty C jest prawdopodobnie „najbardziej chudym” i „najwęższym” kodem, w którym można stworzyć program.
EDYTOWAĆ
Biorąc pod uwagę komentarze do mojej odpowiedzi, zdecydowałem się przeprowadzić test, tylko dla własnego zdrowia psychicznego. Stworzyłem program o nazwie „test.c”, który wyglądał tak:
Skompilowałem to do asemblacji przy użyciu gcc. Użyłem następującego wiersza poleceń, aby go skompilować:
Oto wynikowy język asemblera:
Następnie tworzę plik o nazwie „test.cpp”, który definiuje klasę i wyświetla to samo, co „test.c”:
Skompilowałem to w ten sam sposób, używając tego polecenia:
Oto wynikowy plik zespołu:
Jak widać, wynikowy plik asemblera jest znacznie większy w pliku C ++ niż w pliku C. Nawet jeśli odetniesz wszystkie inne rzeczy i po prostu porównasz „główny” C z „głównym” C ++, jest wiele dodatkowych rzeczy.
źródło
MyClass myClass { 10 }
w C ++, z dużym prawdopodobieństwem zostanie skompilowany do dokładnie tego samego zestawu. Nowoczesne kompilatory C ++ wyeliminowały karę za abstrakcję. W rezultacie często pokonują kompilatory C. Np. Kara za abstrakcję w Cqsort
jest prawdziwa, ale w C ++std::sort
nie ma kary za abstrakcję nawet po podstawowej optymalizacji.K&R oznacza, że większość wyrażeń C (znaczenie techniczne) odwzorowuje jedną lub kilka instrukcji asemblera, a nie wywołanie funkcji do biblioteki pomocniczej. Typowe wyjątki to dzielenie liczb całkowitych na architekturach bez sprzętowej instrukcji div lub zmiennoprzecinkowe na maszynach bez FPU.
Jest cytat:
( znaleziono tutaj . Wydawało mi się, że zapamiętałem inną odmianę, na przykład „szybkość języka asemblera z wygodą i wyrazistością języka asemblera”.
long int ma zwykle taką samą szerokość jak rodzime rejestry maszynowe.
Niektóre języki wyższego poziomu definiują dokładną szerokość swoich typów danych, a implementacje na wszystkich komputerach muszą działać tak samo. Ale nie C.
Jeśli chcesz pracować z 128-bitowymi intami na x86-64 lub w ogólnym przypadku BigInteger o dowolnym rozmiarze, potrzebujesz do tego biblioteki funkcji. Wszystkie procesory używają teraz dopełnienia 2 jako binarnej reprezentacji ujemnych liczb całkowitych, ale nawet to nie miało miejsca, gdy projektowano C. (Dlatego niektóre rzeczy, które dawałyby inne wyniki na maszynach bez dopełnienia 2s, są technicznie niezdefiniowane w standardach C.)
Wskaźniki C do danych lub funkcji działają tak samo, jak adresy asemblerów.
Jeśli chcesz referencji zliczanych referencyjnie, musisz to zrobić sam. Jeśli potrzebujesz wirtualnych funkcji składowych C ++, które wywołują inną funkcję w zależności od rodzaju obiektu, na który wskazuje wskaźnik, kompilator C ++ musi wygenerować znacznie więcej niż tylko
call
instrukcję ze stałym adresem.Łańcuchy to po prostu tablice
Poza funkcjami bibliotecznymi jedyne dostępne operacje na łańcuchach to odczyt / zapis znaku. Bez konkatacji, bez podciągu, bez wyszukiwania. (Ciągi znaków są przechowywane jako
'\0'
tablice 8-bitowych liczb całkowitych zakończone znakiem nul ( ), a nie wskaźnik + długość, więc aby uzyskać podciąg, musisz wpisać wartość nul w oryginalnym ciągu).Procesory czasami mają instrukcje przeznaczone do użycia przez funkcję wyszukiwania łańcuchów, ale nadal zwykle przetwarzają jeden bajt na każdą wykonywaną instrukcję, w pętli. (lub z prefiksem rep x86. Może gdyby C został zaprojektowany na platformie x86, wyszukiwanie lub porównywanie ciągów znaków byłoby operacją natywną, a nie wywołaniem funkcji biblioteki).
Wiele innych odpowiedzi podaje przykłady rzeczy, które nie są natywnie obsługiwane, takich jak obsługa wyjątków, tabele skrótów, listy. Filozofia projektowania K&R jest powodem, dla którego C nie ma żadnego z nich natywnie.
źródło
Język asemblera procesu generalnie zajmuje się skokami (idź do), instrukcjami, instrukcjami ruchu, binarnymi artretycznymi (XOR, NAND, AND OR itp.), Polami pamięci (lub adresem). Kategoryzuje pamięć na dwa typy: instrukcje i dane. Chodzi o wszystko, czym jest język asemblera (jestem pewien, że programiści asemblera będą argumentować, że jest w nim coś więcej, ale sprowadza się to ogólnie do tego). C bardzo przypomina tę prostotę.
C polega na złożeniu tego, czym algebra jest dla arytmetyki.
C zawiera podstawy asemblacji (język procesora). Jest prawdopodobnie prawdziwszym stwierdzeniem niż „Ponieważ typy danych i struktury kontrolne dostarczane przez C są obsługiwane bezpośrednio przez większość komputerów”
źródło
Uważaj na mylące porównania
źródło
Wszystkie podstawowe typy danych i ich operacje w języku C można zaimplementować za pomocą jednej lub kilku instrukcji języka maszynowego bez zapętlania - są one bezpośrednio obsługiwane przez (praktycznie każdy) procesor.
Kilka popularnych typów danych i ich operacje wymagają dziesiątek instrukcji języka maszynowego lub wymagają iteracji pewnej pętli czasu wykonywania lub obu.
Wiele języków ma specjalną, skróconą składnię dla takich typów i ich operacji - używanie takich typów danych w C generalnie wymaga wpisania o wiele więcej kodu.
Takie typy danych i operacje obejmują:
Wszystkie te operacje wymagają dziesiątek instrukcji w języku maszynowym lub wymagają iteracji pętli wykonawczej na prawie każdym procesorze.
Niektóre popularne struktury kontrolne, które również wymagają dziesiątek instrukcji języka maszynowego lub pętli, obejmują:
Niezależnie od tego, czy program jest napisany w C, czy w jakimś innym języku, kiedy program manipuluje takimi typami danych, CPU musi ostatecznie wykonać wszelkie instrukcje wymagane do manipulowania tymi typami danych. Instrukcje te są często zawarte w „bibliotece”. Każdy język programowania, nawet C, ma „bibliotekę wykonawczą” dla każdej platformy, która jest domyślnie dołączona do każdego pliku wykonywalnego.
Większość osób piszących kompilatory umieszcza instrukcje dotyczące manipulowania wszystkimi typami danych „wbudowanymi w język” w swojej bibliotece wykonawczej. Ponieważ C nie ma żadnego z powyższych typów danych i operacji ani struktur kontrolnych wbudowanych w język, żaden z nich nie jest uwzględniony w bibliotece wykonawczej C - co sprawia, że biblioteka wykonawcza języka C jest mniejsza niż biblioteka wykonawcza C biblioteka czasu innych języków programowania, które mają więcej z powyższych elementów wbudowanych w język.
Gdy programista chce, aby program - w C lub jakimkolwiek innym wybranym przez siebie języku - manipulował innymi typami danych, które nie są „wbudowane w język”, zazwyczaj mówi kompilatorowi, aby dołączył dodatkowe biblioteki do tego programu lub czasami (aby „uniknąć zależności”) pisze kolejną implementację tych operacji bezpośrednio w programie.
źródło
Jakie są wbudowane typy danych
C
? Są takie rzeczyint
,char
,* int
,float
, tablice itp ... Te typy danych są rozumiane przez CPU. Procesor wie, jak pracować z tablicami, jak wyłuskiwać wskaźniki i jak wykonywać działania arytmetyczne na wskaźnikach, liczbach całkowitych i liczbach zmiennoprzecinkowych.Ale kiedy przechodzisz do języków programowania wyższego poziomu, masz wbudowane abstrakcyjne typy danych i bardziej złożone konstrukcje. Na przykład spójrz na szeroki wachlarz wbudowanych klas w języku programowania C ++. Procesor nie rozumie klas, obiektów ani abstrakcyjnych typów danych, więc środowisko wykonawcze C ++ wypełnia lukę między procesorem a językiem. To są przykłady typów danych, które nie są bezpośrednio obsługiwane przez większość komputerów.
źródło
To zależy od komputera. Na PDP-11, gdzie wynaleziono C,
long
był słabo obsługiwany (był opcjonalny moduł dodatkowy, który można było kupić, obsługujący niektóre, ale nie wszystkie operacje 32-bitowe). To samo dotyczy w różnym stopniu dowolnego systemu 16-bitowego, w tym oryginalnego IBM PC. Podobnie jest w przypadku operacji 64-bitowych na maszynach 32-bitowych lub w programach 32-bitowych, chociaż język C w czasach książki K&R nie miał w ogóle żadnych operacji 64-bitowych. Oczywiście w latach 80. i 90. istniało wiele systemów (w tym procesory 386 i około 486), a nawet niektóre systemy wbudowane, które nie obsługują bezpośrednio arytmetyki zmiennoprzecinkowej (float
lubdouble
).Dla bardziej egzotycznego przykładu, niektóre architektury komputerów obsługują jedynie wskaźniki „zorientowane na słowa” (wskazujące na dwu- lub czterobajtową liczbę całkowitą w pamięci), a wskaźniki bajtowe (
char *
lubvoid *
) musiały zostać zaimplementowane poprzez dodanie dodatkowego pola przesunięcia. To pytanie zawiera pewne szczegóły dotyczące takich systemów.Funkcje " biblioteki wykonawczej" , do których się odnosi, nie są tymi, które zobaczysz w podręczniku, ale funkcje takie jak te w nowoczesnej bibliotece wykonawczej kompilatora , które są używane do implementacji podstawowych operacji typu, które nie są obsługiwane przez maszynę . Bibliotekę uruchomieniową, do której odnosili się sami K&R, można znaleźć na stronie internetowej The Unix Heritage Society - można zobaczyć takie funkcje
ldiv
(różniące się od funkcji C o tej samej nazwie, która nie istniała w tym czasie), która służy do implementacji podziału 32-bitowe wartości, których PDP-11 nie obsługiwał nawet z dodatkiem icsv
(acret
także w csv.c), które zapisują i przywracają rejestry na stosie w celu zarządzania wywołaniami i zwrotami z funkcji.Prawdopodobnie odnosili się również do swojego wyboru, aby nie obsługiwać wielu typów danych, które nie są bezpośrednio obsługiwane przez maszynę bazową, w przeciwieństwie do innych współczesnych języków, takich jak FORTRAN, które miały semantykę tablicową, która nie była tak dobrze odwzorowana na podstawową obsługę wskaźników procesora, jak Tablice C. Fakt, że tablice C są zawsze indeksowane przez zero i zawsze mają znany rozmiar we wszystkich szeregach, ale pierwszy oznacza, że nie ma potrzeby przechowywania zakresów indeksów lub rozmiarów tablic i nie ma potrzeby posiadania funkcji biblioteki wykonawczej, aby uzyskać do nich dostęp - kompilator może po prostu zakodować na stałe niezbędną arytmetykę wskaźnika.
źródło
To stwierdzenie oznacza po prostu, że struktury danych i sterowania w C są zorientowane maszynowo.
Należy wziąć pod uwagę dwa aspekty. Jednym z nich jest to, że język C ma definicję (standard ISO), która pozwala na dowolność w definiowaniu typów danych. Oznacza to, że implementacje języka C są dostosowane do maszyny . Typy danych kompilatora C są zgodne z tym, co jest dostępne na maszynie, do której jest przeznaczony kompilator, ponieważ język ma na to swobodę. Jeśli maszyna ma nietypowy rozmiar słowa, na przykład 36 bitów, wówczas typ
int
lublong
można dostosować do tego. Programy, które zakładają, żeint
ma dokładnie 32 bity, zostaną przerwane.Po drugie, z powodu takich problemów z przenoszeniem istnieje drugi efekt. W pewnym sensie stwierdzenie w K&R stało się rodzajem samospełniającej się przepowiedni , a może wręcz przeciwnie. Innymi słowy, twórcy nowych procesorów są świadomi pilnej potrzeby wspierania kompilatorów C i wiedzą, że istnieje wiele kodu C, który zakłada, że „każdy procesor wygląda jak 80386”. Architektury są projektowane z myślą o C: i nie tylko o C, ale także o powszechnych błędnych przekonaniach dotyczących przenośności C. Po prostu nie możesz już wprowadzać maszyny z 9-bitowymi bajtami lub czymkolwiek do ogólnego użytku. Programy, które zakładają, że typ
char
ma dokładnie 8 bitów. Tylko niektóre programy napisane przez ekspertów w zakresie przenośności będą nadal działać: prawdopodobnie nie wystarczy, aby zebrać cały system z łańcuchem narzędzi, jądrem, przestrzenią użytkownika i użytecznymi aplikacjami, przy rozsądnym wysiłku. Innymi słowy, typy C wyglądają jak sprzęt dostępny ze sprzętu, ponieważ sprzęt został wykonany tak, aby wyglądał jak inny sprzęt, dla którego napisano wiele nieprzenoszalnych programów w C.Typy danych nieobsługiwane bezpośrednio w wielu językach maszyn: liczba całkowita o dużej precyzji; połączona lista; tabela skrótów; łańcuch znaków.
Struktury sterujące nie są bezpośrednio obsługiwane w większości języków maszyn: kontynuacja pierwszej klasy; coroutine / wątek; generator; Obsługa wyjątków.
Wszystko to wymaga znacznego kodu pomocniczego w czasie wykonywania, utworzonego przy użyciu wielu instrukcji ogólnego przeznaczenia i bardziej elementarnych typów danych.
C ma pewne standardowe typy danych, które nie są obsługiwane przez niektóre komputery. Od C99, C ma liczby zespolone. Są zbudowane z dwóch wartości zmiennoprzecinkowych i przystosowane do pracy z procedurami bibliotecznymi. Niektóre maszyny w ogóle nie mają jednostki zmiennoprzecinkowej.
W odniesieniu do niektórych typów danych nie jest to jasne. Jeśli maszyna obsługuje adresowanie pamięci przy użyciu jednego rejestru jako adresu bazowego, a drugiego jako skalowanego przemieszczenia, czy oznacza to, że tablice są bezpośrednio obsługiwanym typem danych?
Mówiąc o zmiennoprzecinkowych, istnieje standaryzacja: zmiennoprzecinkowa IEEE 754. Dlaczego twój kompilator C ma a,
double
który zgadza się z formatem zmiennoprzecinkowym obsługiwanym przez procesor, nie tylko dlatego, że oba zostały uzgodnione, ale dlatego, że istnieje niezależny standard dla tej reprezentacji.źródło
Rzeczy takie jak
Listy używane w prawie wszystkich językach funkcjonalnych.
Wyjątki .
Tablice asocjacyjne (mapy) - zawarte np. W PHP i Perlu.
Wywóz śmieci .
Typy danych / struktury kontrolne zawarte w wielu językach, ale nie są bezpośrednio obsługiwane przez procesor.
źródło
Obsługiwane bezpośrednio należy rozumieć jako wydajne mapowanie na zestaw instrukcji procesora.
Regułą jest bezpośrednia obsługa typów całkowitych, z wyjątkiem długich (mogą wymagać rozszerzonych procedur arytmetycznych) i krótkich (mogą wymagać maskowania).
Bezpośrednia obsługa typów zmiennoprzecinkowych wymaga dostępności jednostki FPU.
Bezpośrednia obsługa pól bitowych jest wyjątkowa.
Struktury i tablice wymagają obliczania adresów, do pewnego stopnia obsługiwanego bezpośrednio.
Wskaźniki są zawsze obsługiwane bezpośrednio poprzez adresowanie pośrednie.
goto / if / while / for / do są bezpośrednio obsługiwane przez gałęzie bezwarunkowe / warunkowe.
przełącznik może być obsługiwany bezpośrednio, gdy ma zastosowanie tabela skoków.
Wywołania funkcji są bezpośrednio obsługiwane za pomocą funkcji stosu.
źródło