W jaki sposób typy danych C są „obsługiwane bezpośrednio przez większość komputerów”?

114

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?

gwg
źródło
1
Obecnie język C obsługuje złożoną arytmetykę, ale pierwotnie tak nie było, ponieważ komputery nie obsługują bezpośrednio liczb zespolonych jako typów danych.
Jonathan Leffler
12
Właściwie historycznie było odwrotnie: C zostało zaprojektowane na podstawie operacji i typów sprzętu dostępnych w tamtym czasie.
Basile Starynkevitch
2
Większość komputerów nie ma bezpośredniego wsparcia sprzętowego dla
liczb
3
@MSalters: Próbowałem wskazać jakiś kierunek w pytaniu „Czy istnieje przykład typu danych lub struktury kontrolnej, które nie są obsługiwane bezpośrednio przez komputer?” którego nie zinterpretowałem jako ograniczenie do K&R
PlasmaHH
11
Dlaczego nie jest to duplikat więcej niż 6 lat po uruchomieniu Stack Overflow?
Peter Mortensen

Odpowiedzi:

143

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:

float x = 1.0f, y = 2.0f;
return x + y;

Jest tłumaczone na coś takiego:

unsigned x = 0x3f800000, y = 0x40000000;
return _float_add(x, y);

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 longw 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()i longjmp(), 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.

Dietrich Epp
źródło
2
w zasadzie tylko wskaźniki ... raczej, w zasadzie tylko surowe fragmenty pamięci. Nawet jeśli jest to chwytanie nitek, a odpowiedź i tak jest dobra.
Deduplicator
2
Można argumentować, że łańcuchy zakończone znakiem null mają „wsparcie sprzętowe”, ponieważ terminator łańcucha pasuje do operacji „skok, jeśli zero” większości procesorów, a zatem jest nieco szybszy niż inne możliwe implementacje łańcuchów.
Peteris,
1
Opublikowałem własną odpowiedź, aby rozwinąć, jak zaprojektowano C do mapowania po prostu do ASM.
Peter Cordes
1
Proszę nie używać kolokacji „tablice to w zasadzie tylko wskaźniki”, może to poważnie, bardzo zmylić początkującego, takiego jak OP. Coś w rodzaju „tablice są implementowane bezpośrednio przy użyciu wskaźników na poziomie sprzętowym” byłoby lepszym IMO.
Rogalik paramagnetyczny
1
@TheParamagneticCroissant: Myślę, że w tym kontekście jest to właściwe ... przejrzystość odbywa się kosztem precyzji.
Dietrich Epp
37

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

John Castleman
źródło
14

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:

int myInt = 10;

przetłumaczyłoby się na coś takiego w asemblerze:

myInt dw 1
mov myInt,10

Porównaj to z czymś takim jak C ++:

MyClass myClass;
myClass.set_myInt(10);

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:

#include <stdio.h>

void main()
{
    int myInt=10;

    printf("%d\n", myInt);
}

Skompilowałem to do asemblacji przy użyciu gcc. Użyłem następującego wiersza poleceń, aby go skompilować:

gcc -S -O2 test.c

Oto wynikowy język asemblera:

    .file   "test.c"
    .section    .rodata.str1.1,"aMS",@progbits,1
.LC0:
    .string "%d\n"
    .section    .text.unlikely,"ax",@progbits
.LCOLDB1:
    .section    .text.startup,"ax",@progbits
.LHOTB1:
    .p2align 4,,15
    .globl  main
    .type   main, @function
main:
.LFB24:
    .cfi_startproc
    movl    $10, %edx
    movl    $.LC0, %esi
    movl    $1, %edi
    xorl    %eax, %eax
    jmp __printf_chk
    .cfi_endproc
.LFE24:
    .size   main, .-main
    .section    .text.unlikely
.LCOLDE1:
    .section    .text.startup
.LHOTE1:
    .ident  "GCC: (Ubuntu 4.9.1-16ubuntu6) 4.9.1"
    .section    .note.GNU-stack,"",@progbits

Następnie tworzę plik o nazwie „test.cpp”, który definiuje klasę i wyświetla to samo, co „test.c”:

#include <iostream>
using namespace std;

class MyClass {
    int myVar;
public:
    void set_myVar(int);
    int get_myVar(void);
};

void MyClass::set_myVar(int val)
{
    myVar = val;
}

int MyClass::get_myVar(void)
{
    return myVar;
}

int main()
{
    MyClass myClass;
    myClass.set_myVar(10);

    cout << myClass.get_myVar() << endl;

    return 0;
}

Skompilowałem to w ten sam sposób, używając tego polecenia:

g++ -O2 -S test.cpp

Oto wynikowy plik zespołu:

    .file   "test.cpp"
    .section    .text.unlikely,"ax",@progbits
    .align 2
.LCOLDB0:
    .text
.LHOTB0:
    .align 2
    .p2align 4,,15
    .globl  _ZN7MyClass9set_myVarEi
    .type   _ZN7MyClass9set_myVarEi, @function
_ZN7MyClass9set_myVarEi:
.LFB1047:
    .cfi_startproc
    movl    %esi, (%rdi)
    ret
    .cfi_endproc
.LFE1047:
    .size   _ZN7MyClass9set_myVarEi, .-_ZN7MyClass9set_myVarEi
    .section    .text.unlikely
.LCOLDE0:
    .text
.LHOTE0:
    .section    .text.unlikely
    .align 2
.LCOLDB1:
    .text
.LHOTB1:
    .align 2
    .p2align 4,,15
    .globl  _ZN7MyClass9get_myVarEv
    .type   _ZN7MyClass9get_myVarEv, @function
_ZN7MyClass9get_myVarEv:
.LFB1048:
    .cfi_startproc
    movl    (%rdi), %eax
    ret
    .cfi_endproc
.LFE1048:
    .size   _ZN7MyClass9get_myVarEv, .-_ZN7MyClass9get_myVarEv
    .section    .text.unlikely
.LCOLDE1:
    .text
.LHOTE1:
    .section    .text.unlikely
.LCOLDB2:
    .section    .text.startup,"ax",@progbits
.LHOTB2:
    .p2align 4,,15
    .globl  main
    .type   main, @function
main:
.LFB1049:
    .cfi_startproc
    subq    $8, %rsp
    .cfi_def_cfa_offset 16
    movl    $10, %esi
    movl    $_ZSt4cout, %edi
    call    _ZNSolsEi
    movq    %rax, %rdi
    call    _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
    xorl    %eax, %eax
    addq    $8, %rsp
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc
.LFE1049:
    .size   main, .-main
    .section    .text.unlikely
.LCOLDE2:
    .section    .text.startup
.LHOTE2:
    .section    .text.unlikely
.LCOLDB3:
    .section    .text.startup
.LHOTB3:
    .p2align 4,,15
    .type   _GLOBAL__sub_I__ZN7MyClass9set_myVarEi, @function
_GLOBAL__sub_I__ZN7MyClass9set_myVarEi:
.LFB1056:
    .cfi_startproc
    subq    $8, %rsp
    .cfi_def_cfa_offset 16
    movl    $_ZStL8__ioinit, %edi
    call    _ZNSt8ios_base4InitC1Ev
    movl    $__dso_handle, %edx
    movl    $_ZStL8__ioinit, %esi
    movl    $_ZNSt8ios_base4InitD1Ev, %edi
    addq    $8, %rsp
    .cfi_def_cfa_offset 8
    jmp __cxa_atexit
    .cfi_endproc
.LFE1056:
    .size   _GLOBAL__sub_I__ZN7MyClass9set_myVarEi, .-_GLOBAL__sub_I__ZN7MyClass9set_myVarEi
    .section    .text.unlikely
.LCOLDE3:
    .section    .text.startup
.LHOTE3:
    .section    .init_array,"aw"
    .align 8
    .quad   _GLOBAL__sub_I__ZN7MyClass9set_myVarEi
    .local  _ZStL8__ioinit
    .comm   _ZStL8__ioinit,1,1
    .hidden __dso_handle
    .ident  "GCC: (Ubuntu 4.9.1-16ubuntu6) 4.9.1"
    .section    .note.GNU-stack,"",@progbits

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.

Lodziarz
źródło
14
Ten „kod C ++” po prostu nie jest C ++. A prawdziwy kod, taki jak 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 C qsortjest prawdziwa, ale w C ++ std::sortnie ma kary za abstrakcję nawet po podstawowej optymalizacji.
MSalters
1
Za pomocą IDA Pro możesz łatwo zobaczyć, że większość konstrukcji C ++ kompiluje się do tego samego co robienie tego ręcznie w C, konstruktorzy i dtors są wstawiani do trywialnych obiektów, a następnie stosowana jest przyszła optymalizacja
paulm
7

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:

C łączy elastyczność i moc języka asemblera z łatwością obsługi języka asemblera.

( 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 callinstrukcję 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.

Peter Cordes
źródło
„K&R oznacza, że ​​większość wyrażeń C (znaczenie techniczne) odwzorowuje jedną lub kilka instrukcji asemblera, a nie wywołanie funkcji do biblioteki obsługi”. To bardzo intuicyjne wyjaśnienie. Dzięki.
gwg
1
Właśnie natknąłem się na termin „język von Neumann” ( en.wikipedia.org/wiki/Von_Neumann_programming_languages ). To DOKŁADNIE to, czym jest C.
Peter Cordes
1
Właśnie dlatego używam C. Ale to, co mnie zaskoczyło, gdy uczę się C, to fakt, że próbując być wydajnym dla szerokiej gamy sprzętu, czasami jest to nieudolne i nieefektywne w przypadku większości nowoczesnych urządzeń. Mam na myśli na przykład brak użytecznego i niezawodnego sposobu wykrywania przepełnienia liczby całkowitej w ci i dodawania wielu słów przy użyciu flagi przenoszenia .
Bozon Z
6

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”

terary
źródło
5

Uważaj na mylące porównania

  1. Oświadczenie opiera się na pojęciu „biblioteki wykonawczej” , która od tamtego czasu w większości wyszła z mody, przynajmniej w przypadku głównych języków wysokiego poziomu. (Nadal ma znaczenie dla najmniejszych systemów osadzonych). Czas wykonywania to minimalne wsparcie, jakie program w tym języku wymaga do wykonania, gdy używasz tylko konstrukcji wbudowanych w język (w przeciwieństwie do jawnego wywoływania funkcji udostępnianej przez bibliotekę) .
  2. W przeciwieństwie do tego, współczesne języki nie rozróżniają między biblioteką wykonawczą a biblioteką standardową , która często jest dość obszerna.
  3. W czasach książki K&R C nie miał nawet standardowej biblioteki . Raczej, dostępne biblioteki C różniły się nieco między różnymi odmianami Uniksa.
  4. Aby zrozumieć instrukcję, nie powinieneś porównywać języków ze standardową biblioteką (takich jak Lua i Python wspomniane w innych odpowiedziach), ale z językami z bardziej wbudowanymi konstrukcjami (takimi jak stary LISP i stary FORTRAN wspomniany w innych odpowiedzi). Innymi przykładami byłyby BASIC (interaktywne, jak LISP) lub PASCAL (skompilowane, jak FORTRAN), które mają (między innymi) funkcje wejścia / wyjścia wbudowane bezpośrednio w sam język.
  5. W przeciwieństwie do tego nie ma standardowego sposobu uzyskania wyników obliczeń z programu C, który używa tylko czasu wykonywania, a nie żadnej biblioteki.
Lutz Prechelt
źródło
Z drugiej strony większość współczesnych języków działa w dedykowanych środowiskach wykonawczych, które zapewniają takie udogodnienia, jak czyszczenie pamięci.
Nate CK
5

Czy istnieje przykład typu danych lub struktury kontrolnej, która nie jest obsługiwana bezpośrednio przez komputer?

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ą:

  • Manipulowanie ciągiem tekstowym o dowolnej długości - konkatenacja, podciąg, przypisanie nowego ciągu do zmiennej zainicjowanej innym ciągiem znaków itp. ('s = "Hello World!"; s = (s + s) [2: -2] 'w Pythonie)
  • zestawy
  • obiekty z zagnieżdżonymi wirtualnymi destruktorami, jak w C ++ i każdym innym języku programowania obiektowego
  • Mnożenie i dzielenie macierzy 2D; rozwiązywanie układów liniowych ("C = B / A; x = A \ b" w MATLAB i wielu językach programowania tablicowego)
  • wyrażenia regularne
  • tablice o zmiennej długości - w szczególności dołączanie pozycji na końcu tablicy, co (czasami) wymaga alokacji większej ilości pamięci.
  • odczytywanie wartości zmiennych, które zmieniają typ w czasie wykonywania - czasami jest to float, innym razem jest to string
  • tablice asocjacyjne (często nazywane „mapami” lub „słownikami”)
  • listy
  • współczynniki ("(+ 1/3 2/7)" daje "13/21" w Lisp )
  • arytmetyka o dowolnej precyzji (często nazywana „bignum”)
  • konwertowanie danych na reprezentację do druku (metoda „.tostring” w JavaScript)
  • nasycanie liczb stałoprzecinkowych (często używane w osadzonych programach C)
  • ocenianie ciągu wpisanego w czasie wykonywania, tak jakby był to wyrażenie („eval ()” w wielu językach programowania).

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ą:

  • domknięcia
  • kontynuacje
  • wyjątki
  • leniwa ocena

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.

David Cary
źródło
Jeśli Twoja implementacja Lispa ocenia (+ 1/3 2/7) jako 3/21, myślę, że musisz mieć szczególnie kreatywną implementację ...
RobertB
4

Jakie są wbudowane typy danych C? Są takie rzeczy int, 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.

hhafez
źródło
2
x86 wie, że działa z niektórymi tablicami, ale nie ze wszystkimi. W przypadku dużych lub nietypowych rozmiarów elementów konieczne będzie wykonanie arytmetyki liczb całkowitych, aby przekonwertować indeks tablicy na przesunięcie wskaźnika. Na innych platformach jest to zawsze potrzebne. A pomysł, że procesor nie rozumie klas C ++, jest śmieszny. To tylko przesunięcia wskaźnika, jak struktury C. Nie potrzebujesz do tego środowiska wykonawczego.
MSalters
@MSalters tak, ale rzeczywiste metody klas standardowych bibliotek, takich jak iostreams itp., Są funkcjami bibliotecznymi, a nie są bezpośrednio obsługiwane przez kompilator. Jednak językami wyższego poziomu, z którymi prawdopodobnie go porównywali, nie był C ++, ale języki współczesne, takie jak FORTRAN i PL / I.
Random832
1
Klasy C ++ z wirtualnymi funkcjami składowymi to dużo więcej niż tylko przesunięcie w strukturę.
Peter Cordes
4

To zależy od komputera. Na PDP-11, gdzie wynaleziono C, longbył 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 ( floatlub double).

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 *lub void *) 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 i csv(a crettakż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.

Losowo 832
źródło
3

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 intlub longmożna dostosować do tego. Programy, które zakładają, że intma 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 typcharma 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.

Czy istnieje przykład typu danych lub struktury kontrolnej, która nie jest obsługiwana bezpośrednio przez komputer?

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, doublektó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.

Kaz
źródło
2

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.

MTilsted
źródło
2

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.

Yves Daoust
źródło