Je lis «Le langage de programmation C» de K&R et je suis tombé sur cette déclaration [Introduction, p. 3]:
Étant donné que les types de données et les structures de contrôle fournis par C sont pris en charge directement par la plupart des ordinateurs , la bibliothèque d'exécution requise pour implémenter des programmes autonomes est minuscule.
Que signifie la déclaration en gras? Existe-t-il un exemple de type de données ou de structure de contrôle qui n'est pas pris en charge directement par un ordinateur?
Réponses:
Oui, certains types de données ne sont pas directement pris en charge.
Sur de nombreux systèmes embarqués, il n'y a pas d'unité matérielle à virgule flottante. Donc, lorsque vous écrivez un code comme celui-ci:
Cela se traduit par quelque chose comme ceci:
Ensuite, le compilateur ou la bibliothèque standard doit fournir une implémentation de
_float_add()
, qui occupe de la mémoire sur votre système embarqué. Si vous comptez des octets sur un système très petit, cela peut s'additionner.Un autre exemple courant est celui des entiers 64 bits (
long long
dans la norme C depuis 1999), qui ne sont pas directement pris en charge par les systèmes 32 bits. Les anciens systèmes SPARC ne supportaient pas la multiplication d'entiers, donc la multiplication devait être fournie par le runtime. Il y a d'autres exemples.Autres langues
En comparaison, d'autres langages ont des primitives plus compliquées.
Par exemple, un symbole Lisp nécessite beaucoup de support à l'exécution, tout comme les tables en Lua, les chaînes en Python, les tableaux en Fortran, et cetera. Les types équivalents en C ne font généralement pas partie de la bibliothèque standard du tout (pas de symboles ou de tables standard) ou ils sont beaucoup plus simples et ne nécessitent pas beaucoup de support d'exécution (les tableaux en C ne sont fondamentalement que des pointeurs, les chaînes à terminaison nulle sont presque aussi simple).
Structures de contrôle
Une structure de contrôle notable absente de C est la gestion des exceptions. La sortie non locale est limitée à
setjmp()
etlongjmp()
, qui ne font que sauvegarder et restaurer certaines parties de l'état du processeur. Par comparaison, le runtime C ++ doit parcourir la pile et appeler des destructeurs et des gestionnaires d'exceptions.la source
En fait, je parie que le contenu de cette introduction n'a pas beaucoup changé depuis 1978 lorsque Kernighan et Ritchie les ont écrits pour la première fois dans la première édition du livre, et ils se réfèrent à l'histoire et à l'évolution de C à cette époque plus que moderne. implémentations.
Les ordinateurs ne sont fondamentalement que des banques de mémoire et des processeurs centraux, et chaque processeur fonctionne à l'aide d'un code machine; une partie de la conception de chaque processeur est une architecture de jeu d'instructions, appelée langage d'assemblage , qui mappe un à un d'un ensemble de mnémoniques lisibles par l'homme au code machine, qui est tous des nombres.
Les auteurs du langage C - et des langages B et BCPL qui l'ont immédiatement précédé - avaient l'intention de définir des constructions dans le langage qui étaient aussi efficacement compilées dans Assembly que possible ... en fait, ils ont été contraints par des limitations dans la cible Matériel. Comme d'autres réponses l'ont souligné, cela impliquait des branches (GOTO et autres contrôles de flux en C), des déplacements (affectation), des opérations logiques (& | ^), de l'arithmétique de base (ajouter, soustraire, incrémenter, décrémenter) et l'adressage mémoire (pointeurs ). Un bon exemple est les opérateurs pré / post-incrémentation et décrémentation en C, qui auraient été ajoutés au langage B par Ken Thompson spécifiquement parce qu'ils étaient capables de se traduire directement en un seul opcode une fois compilés.
C'est ce que les auteurs voulaient dire lorsqu'ils disaient «pris en charge directement par la plupart des ordinateurs». Ils ne signifiaient pas que d'autres langages contenaient des types et des structures qui n'étaient pas pris en charge directement - ils signifiaient que, par conception, les constructions C traduites le plus directement (parfois littéralement directement) en Assembly.
Cette relation étroite avec l'assembly sous-jacent, tout en fournissant tous les éléments nécessaires à la programmation structurée, est ce qui a conduit à l'adoption précoce de C, et ce qui en fait un langage populaire aujourd'hui dans des environnements où l'efficacité du code compilé est toujours essentielle.
Pour un article intéressant sur l'histoire du langage, voir Le développement du langage C - Dennis Ritchie
la source
La réponse courte est que la plupart des constructions de langage prises en charge par C sont également prises en charge par le microprocesseur de l'ordinateur cible.Par conséquent, le code C compilé se traduit très bien et efficacement dans le langage d'assemblage du microprocesseur, ce qui entraîne un code plus petit et une empreinte plus petite.
La réponse plus longue nécessite un peu de connaissance du langage assembleur. En C, une déclaration comme celle-ci:
se traduirait par quelque chose comme ça dans l'assemblage:
Comparez cela à quelque chose comme C ++:
Le code de langage d'assemblage résultant (selon la taille de MyClass ()), pourrait ajouter jusqu'à des centaines de lignes de langage d'assemblage.
Sans réellement créer de programmes en langage assembleur, le C pur est probablement le code le plus «maigre» et le plus «serré» dans lequel vous pouvez créer un programme.
ÉDITER
Compte tenu des commentaires sur ma réponse, j'ai décidé de faire un test, juste pour ma propre raison. J'ai créé un programme appelé "test.c", qui ressemblait à ceci:
J'ai compilé cela jusqu'à l'assemblage en utilisant gcc. J'ai utilisé la ligne de commande suivante pour le compiler:
Voici le langage d'assemblage résultant:
Je crée ensuite un fichier appelé "test.cpp" qui définit une classe et produit la même chose que "test.c":
Je l'ai compilé de la même manière, en utilisant cette commande:
Voici le fichier d'assemblage résultant:
Comme vous pouvez le voir clairement, le fichier d'assemblage résultant est beaucoup plus volumineux sur le fichier C ++ que sur le fichier C. Même si vous supprimez tous les autres éléments et comparez simplement le C "principal" au "principal" C ++, il y a beaucoup de choses supplémentaires.
la source
MyClass myClass { 10 }
C ++ est très susceptible de se compiler exactement dans le même assembly. Les compilateurs C ++ modernes ont éliminé la pénalité d'abstraction. Et en conséquence, ils peuvent souvent battre les compilateurs C. Par exemple, la pénalité d'abstraction en Cqsort
est réelle, mais celle en C ++std::sort
n'a aucune pénalité d'abstraction même après une optimisation de base.K&R signifie que la plupart des expressions C (signification technique) correspondent à une ou plusieurs instructions d'assemblage, et non à un appel de fonction à une bibliothèque de support. Les exceptions habituelles sont la division entière sur les architectures sans instruction div matérielle, ou la virgule flottante sur les machines sans FPU.
Il y a une citation:
( trouvé ici . Je pensais me souvenir d'une variante différente, comme "la vitesse du langage d'assemblage avec la commodité et l'expressivité du langage d'assemblage".)
long int a généralement la même largeur que les registres de la machine native.
Certains langages de niveau supérieur définissent la largeur exacte de leurs types de données et les implémentations sur toutes les machines doivent fonctionner de la même manière. Pas C, cependant.
Si vous voulez travailler avec des ints 128 bits sur x86-64, ou dans le cas général BigInteger de taille arbitraire, vous avez besoin d'une bibliothèque de fonctions pour cela. Tous les processeurs utilisent maintenant le complément 2s comme représentation binaire d'entiers négatifs, mais même ce n'était pas le cas à l'époque de la conception de C. (C'est pourquoi certaines choses qui donneraient des résultats différents sur des machines non complémentaires de 2s ne sont techniquement pas définies dans les normes C.)
Les pointeurs C vers des données ou des fonctions fonctionnent de la même manière que les adresses d'assemblage.
Si vous voulez des références ref-countées, vous devez le faire vous-même. Si vous voulez des fonctions membres virtuelles C ++ qui appellent une fonction différente selon le type d'objet vers lequel pointe votre pointeur, le compilateur C ++ doit générer beaucoup plus qu'une simple
call
instruction avec une adresse fixe.Les chaînes ne sont que des tableaux
En dehors des fonctions de bibliothèque, les seules opérations de chaîne fournies sont la lecture / écriture d'un caractère. Pas de concat, pas de sous-chaîne, pas de recherche. (Les chaînes sont stockées sous forme de
'\0'
tableaux à terminaison nulle ( ) d'entiers 8 bits, pas de pointeur + longueur, donc pour obtenir une sous-chaîne, vous devez écrire un nul dans la chaîne d'origine.)Les processeurs ont parfois des instructions conçues pour être utilisées par une fonction de recherche de chaînes, mais traitent toujours généralement un octet par instruction exécutée, dans une boucle. (ou avec le préfixe de rep x86. Peut-être que si C était conçu sur x86, la recherche ou la comparaison de chaînes serait une opération native, plutôt qu'un appel de fonction de bibliothèque.)
Beaucoup d'autres réponses donnent des exemples de choses qui ne sont pas prises en charge nativement, comme la gestion des exceptions, les tables de hachage, les listes. La philosophie de conception de K&R est la raison pour laquelle C n'en a pas nativement.
la source
Le langage d'assemblage d'un processus traite généralement des sauts (aller à), des instructions, des instructions de déplacement, des arthrites binaires (XOR, NAND, AND OR, etc.), des champs de mémoire (ou d'adresse). Classe la mémoire en deux types, instruction et données. C'est à peu près tout ce qu'est un langage d'assemblage (je suis sûr que les programmeurs d'assemblage diront qu'il y a plus que cela, mais cela se résume à cela en général). C ressemble beaucoup à cette simplicité.
C est d'assembler ce que l'algèbre est à l'arithmétique.
C encapsule les bases de l'assemblage (le langage du processeur). Est probablement une déclaration plus vraie que "Parce que les types de données et les structures de contrôle fournis par C sont pris en charge directement par la plupart des ordinateurs"
la source
Méfiez-vous des comparaisons trompeuses
la source
Tous les types de données fondamentaux et leurs opérations en langage C peuvent être implémentés par une ou quelques instructions en langage machine sans boucle - ils sont directement pris en charge par le (pratiquement tous) CPU.
Plusieurs types de données populaires et leurs opérations nécessitent des dizaines d'instructions en langage machine, ou nécessitent l'itération d'une boucle d'exécution, ou des deux.
De nombreux langages ont une syntaxe abrégée spéciale pour ces types et leurs opérations - l'utilisation de ces types de données en C nécessite généralement de taper beaucoup plus de code.
Ces types de données et opérations comprennent:
Toutes ces opérations nécessitent des dizaines d'instructions en langage machine ou nécessitent l'itération d'une boucle d'exécution sur presque tous les processeurs.
Certaines structures de contrôle populaires qui nécessitent également des dizaines d'instructions en langage machine ou des boucles incluent:
Qu'il soit écrit en C ou dans un autre langage, lorsqu'un programme manipule de tels types de données, la CPU doit finalement exécuter toutes les instructions nécessaires pour manipuler ces types de données. Ces instructions sont souvent contenues dans une "bibliothèque". Chaque langage de programmation, même C, a une "bibliothèque d'exécution" pour chaque plate-forme qui est incluse par défaut dans chaque exécutable.
La plupart des gens qui écrivent des compilateurs mettent les instructions pour manipuler tous les types de données qui sont «intégrés dans le langage» dans leur bibliothèque d'exécution. Étant donné que C n'a aucun des types de données, des opérations et des structures de contrôle ci-dessus intégrés dans le langage, aucun d'entre eux n'est inclus dans la bibliothèque d'exécution C - ce qui rend la bibliothèque d'exécution C plus petite que l'exécution- bibliothèque temporelle d'autres langages de programmation qui ont plus des éléments ci-dessus intégrés au langage.
Lorsqu'un programmeur veut qu'un programme - en C ou dans tout autre langage de son choix - manipule d'autres types de données qui ne sont pas "intégrés au langage", ce programmeur dit généralement au compilateur d'inclure des bibliothèques supplémentaires avec ce programme, ou parfois (pour "éviter les dépendances") écrit encore une autre implémentation de ces opérations directement dans le programme.
la source
Quels sont les types de données intégrés dans
C
? Ce sont des choses commeint
,char
,* int
,float
, tableaux , etc ... Ces types de données sont compris par la CPU. Le CPU sait comment travailler avec des tableaux, comment déréférencer des pointeurs et comment effectuer de l'arithmétique sur des pointeurs, des entiers et des nombres à virgule flottante.Mais lorsque vous passez à des langages de programmation de niveau supérieur, vous avez intégré des types de données abstraits et des constructions plus complexes. Par exemple, regardez le vaste éventail de classes intégrées dans le langage de programmation C ++. Le processeur ne comprend pas les classes, les objets ou les types de données abstraits, de sorte que l'exécution C ++ comble le fossé entre le processeur et le langage. Voici des exemples de types de données qui ne sont pas directement pris en charge par la plupart des ordinateurs.
la source
Cela dépend de l'ordinateur. Le PDP-11, où C a été inventé,
long
était mal pris en charge (il y avait un module complémentaire optionnel que vous pouviez acheter qui prenait en charge certaines, mais pas toutes, les opérations 32 bits). La même chose est vraie à divers degrés sur n'importe quel système 16 bits, y compris l'IBM PC d'origine. Et de même pour les opérations 64 bits sur des machines 32 bits ou dans des programmes 32 bits, bien que le langage C à l'époque du livre K&R n'ait pas du tout d'opérations 64 bits. Et bien sûr, il y a eu de nombreux systèmes au cours des années 80 et 90 [y compris les processeurs 386 et 486], et même certains systèmes embarqués aujourd'hui, qui ne supportaient pas directement l'arithmétique en virgule flottante (float
oudouble
).Pour un exemple plus exotique, certaines architectures informatiques ne prennent en charge que les pointeurs «orientés mot» (pointant vers un entier de deux ou quatre octets en mémoire), et les pointeurs d'octets (
char *
ouvoid *
) ont dû être implémentés en ajoutant un champ de décalage supplémentaire. Cette question entre dans certains détails sur ces systèmes.Les fonctions de « bibliothèque d'exécution» auxquelles il fait référence ne sont pas celles que vous verrez dans le manuel, mais des fonctions comme celles-ci, dans la bibliothèque d'exécution d'un compilateur moderne , qui sont utilisées pour implémenter les opérations de type de base qui ne sont pas prises en charge par la machine . La bibliothèque d'exécution à laquelle K&R se référait se trouve sur le site Web de la Unix Heritage Society - vous pouvez voir des fonctions comme
ldiv
(distincte de la fonction C du même nom, qui n'existait pas à l'époque) qui est utilisée pour implémenter la division de Valeurs 32 bits, que le PDP-11 ne supportait pas même avec l'add-on, etcsv
(etcret
aussi dans csv.c) qui sauvegardent et restaurent les registres sur la pile pour gérer les appels et les retours de fonctions.Ils faisaient probablement également référence à leur choix de ne pas prendre en charge de nombreux types de données qui ne sont pas directement pris en charge par la machine sous-jacente, contrairement à d'autres langages contemporains tels que FORTRAN, qui avaient une sémantique de tableau qui ne correspondait pas aussi bien au support de pointeur sous-jacent du processeur que Les tableaux de C. Le fait que les tableaux C soient toujours indexés à zéro et toujours de taille connue dans tous les rangs, mais le premier signifie qu'il n'est pas nécessaire de stocker les plages d'index ou les tailles des tableaux, et pas besoin d'avoir des fonctions de bibliothèque d'exécution pour y accéder - le compilateur peut simplement coder en dur l'arithmétique nécessaire du pointeur.
la source
L'instruction signifie simplement que les données et les structures de contrôle en C sont orientées machine.
Il y a deux aspects à considérer ici. La première est que le langage C a une définition (norme ISO) qui permet une certaine latitude dans la manière dont les types de données sont définis. Cela signifie que les implémentations du langage C sont adaptées à la machine . Les types de données d'un compilateur C correspondent à ce qui est disponible dans la machine ciblée par le compilateur, car le langage a la latitude pour cela. Si une machine a une taille de mot inhabituelle, comme 36 bits, alors le type
int
oulong
peut être rendu conforme à cela. Les programmes qui supposent queint
c'est exactement 32 bits seront interrompus.Deuxièmement, en raison de ces problèmes de portabilité, il y a un deuxième effet. D'une certaine manière, la déclaration dans le K&R est devenue une sorte de prophétie auto-réalisatrice , ou peut-être à l'inverse. C'est-à-dire que les implémenteurs de nouveaux processeurs sont conscients du besoin criant de supporter les compilateurs C, et ils savent qu'il existe beaucoup de code C qui suppose que "chaque processeur ressemble à un 80386". Les architectures sont conçues avec C à l'esprit: et pas seulement C à l'esprit, mais avec les idées fausses courantes sur la portabilité C à l'esprit également. Vous ne pouvez tout simplement plus introduire une machine avec des octets de 9 bits ou quoi que ce soit à des fins générales. Programmes qui supposent que le type
char
est exactement 8 bits de large cassera. Seuls certains programmes écrits par des experts en portabilité continueront de fonctionner: probablement pas assez pour rassembler un système complet avec une chaîne d'outils, un noyau, un espace utilisateur et des applications utiles, avec un effort raisonnable. En d'autres termes, les types C ressemblent à ce qui est disponible à partir du matériel parce que le matériel a été conçu pour ressembler à un autre matériel pour lequel de nombreux programmes C non portables ont été écrits.Les types de données ne sont pas directement pris en charge dans de nombreux langages machine: entier multi-précision; liste chaînée; table de hachage; chaîne de caractères.
Structures de contrôle non directement prises en charge dans la plupart des langages machine: continuation de première classe; coroutine / thread; Générateur; gestion des exceptions.
Tous ces éléments nécessitent un code de support d'exécution considérable créé à l'aide de nombreuses instructions à usage général et de types de données plus élémentaires.
C a des types de données standard qui ne sont pas pris en charge par certaines machines. Depuis C99, C a des nombres complexes. Ils sont constitués de deux valeurs à virgule flottante et conçus pour fonctionner avec des routines de bibliothèque. Certaines machines n'ont pas du tout d'unité à virgule flottante.
En ce qui concerne certains types de données, ce n'est pas clair. Si une machine prend en charge l'adressage de la mémoire en utilisant un registre comme adresse de base et un autre comme un déplacement mis à l'échelle, cela signifie-t-il que les tableaux sont un type de données directement pris en charge?
Aussi, en parlant de virgule flottante, il y a une normalisation: IEEE 754 virgule flottante. La
double
raison pour laquelle votre compilateur C a un qui est d'accord avec le format à virgule flottante pris en charge par le processeur n'est pas seulement parce que les deux ont été mis d'accord, mais parce qu'il existe une norme indépendante pour cette représentation.la source
Des choses telles que
Listes Utilisées dans presque tous les langages fonctionnels.
Exceptions .
Tableaux associatifs (Maps) - inclus par exemple dans PHP et Perl.
Collecte des ordures .
Types de données / structures de contrôle inclus dans de nombreux langages, mais pas directement pris en charge par la CPU.
la source
La prise en charge directe doit être comprise comme une correspondance efficace avec le jeu d'instructions du processeur.
La prise en charge directe des types entiers est la règle, sauf pour les tailles longues (peuvent nécessiter des routines arithmétiques étendues) et courtes (peuvent nécessiter un masquage).
La prise en charge directe des types à virgule flottante nécessite qu'un FPU soit disponible.
La prise en charge directe des champs de bits est exceptionnelle.
Les structures et les tableaux nécessitent un calcul d'adresse, directement pris en charge dans une certaine mesure.
Les pointeurs sont toujours directement pris en charge via l'adressage indirect.
goto / if / while / for / do sont directement supportés par les branches inconditionnelles / conditionnelles.
Le commutateur peut être directement pris en charge lorsqu'une table de sauts s'applique.
Les appels de fonction sont directement pris en charge au moyen des fonctionnalités de pile.
la source