Lors de la conception et implenting un langage de programmation orienté objet, à un un point doit faire un choix sur la mise en œuvre des types fondamentaux (comme int
, float
, double
ou équivalents) sous forme de classes ou autre chose. De toute évidence, les langages de la famille C ont tendance à ne pas les définir comme des classes (Java a des types primitifs spéciaux, C # les implémente comme des structures immuables, etc.).
Je peux penser à un avantage très important lorsque les types fondamentaux sont implémentés en tant que classes (dans un système de types avec une hiérarchie unifiée): ces types peuvent être des sous-types Liskov appropriés du type racine. Ainsi, nous évitons de compliquer le langage avec la boxe / unboxing (explicite ou implicite), les types de wrapper, les règles de variance spéciales, le comportement spécial, etc.
Bien sûr, je peux comprendre en partie pourquoi les concepteurs de langage décident de leur façon: les instances de classe ont tendance à avoir une surcharge spatiale (car les instances peuvent contenir une vtable ou d'autres métadonnées dans leur disposition de mémoire), que les primitives / structures n'ont pas besoin de avoir (si la langue ne permet pas l'héritage sur ceux-ci).
L'efficacité spatiale (et l'amélioration de la localisation spatiale, en particulier dans les grands tableaux) est-elle la seule raison pour laquelle les types fondamentaux ne sont souvent pas des classes?
J'ai généralement supposé que la réponse était oui, mais les compilateurs ont des algorithmes d'analyse d'échappement et ils peuvent donc déduire s'ils peuvent (sélectivement) omettre la surcharge spatiale lorsqu'une instance (n'importe quelle instance, pas seulement un type fondamental) se révèle être strictement local.
Est-ce que ce qui précède est faux ou y a-t-il autre chose qui me manque?
la source
Réponses:
Oui, cela revient à peu près à l'efficacité. Mais vous semblez sous-estimer l'impact (ou surestimer le fonctionnement de diverses optimisations).
Tout d'abord, il ne s'agit pas seulement de "surcharge spatiale". Rendre les primitives en boîte / allouées en tas a également des coûts de performance. Il y a une pression supplémentaire sur le GC pour allouer et collecter ces objets. Cela va doublement si les "objets primitifs" sont immuables, comme ils devraient l'être. Ensuite, il y a plus d'échecs de cache (à la fois en raison de l'indirection et parce que moins de données tiennent dans une quantité donnée de cache). De plus, le simple fait que "charger l'adresse d'un objet, puis charger la valeur réelle à partir de cette adresse" nécessite plus d'instructions que "charger la valeur directement".
Deuxièmement, l'analyse des fuites n'est pas une poussière de fée plus rapide. Cela ne s'applique qu'aux valeurs qui, bien, n'échappent pas. Il est certainement agréable d'optimiser les calculs locaux (tels que les compteurs de boucles et les résultats intermédiaires des calculs) et cela donnera des avantages mesurables. Mais une bien plus grande majorité de valeurs vivent dans les domaines des objets et des tableaux. Certes, ceux-ci peuvent être eux-mêmes soumis à une analyse d'échappement, mais comme il s'agit généralement de types de référence mutables, leur alias présente un défi important pour l'analyse d'échappement, qui doit maintenant prouver que ces alias (1) n'échappent pas non plus , et (2) ne font pas de différence dans le but d'éliminer les allocations.
Étant donné que l'appel à n'importe quelle méthode (y compris les getters) ou le passage d'un objet en argument à toute autre méthode peut aider l'objet à s'échapper, vous aurez besoin d'une analyse interprocédurale dans tous les cas, sauf les plus triviaux. C'est beaucoup plus cher et compliqué.
Et puis il y a des cas où les choses s'échappent vraiment et ne peuvent pas être raisonnablement optimisées. Beaucoup d'entre eux, en fait, si vous considérez la fréquence à laquelle les programmeurs C ont du mal à allouer des tas. Lorsqu'un objet contenant un int s'échappe, l'analyse d'échappement cesse de s'appliquer également à l'int. Dites adieu aux champs primitifs efficaces .
Cela rejoint un autre point: les analyses et optimisations nécessaires sont sérieusement compliquées et constituent un domaine de recherche actif. On peut se demander si une implémentation de langage a atteint le degré d'optimisation que vous proposez, et même si c'est le cas, cela a été un effort rare et herculéen. Il est certainement plus facile de se tenir sur les épaules de ces géants que d'être un géant vous-même, mais c'est encore loin d'être trivial. Ne vous attendez pas à des performances compétitives à tout moment au cours des premières années, voire jamais.
Cela ne veut pas dire que de telles langues ne peuvent pas être viables. Ils le sont clairement. Ne présumez pas que ce sera ligne par ligne aussi vite que les langues avec des primitives dédiées. En d'autres termes, ne vous trompez pas avec des visions d'un compilateur suffisamment intelligent .
la source
Non.
L'autre problème est que les types fondamentaux ont tendance à être utilisés par les opérations fondamentales. Le compilateur doit savoir que cela
int + int
ne va pas être compilé en un appel de fonction, mais en une instruction CPU élémentaire (ou un octet-code équivalent). À ce stade, si vous avez l'int
objet en tant qu'objet normal, vous devrez de toute façon déballer efficacement la chose.Ce type d'opérations n'est pas non plus vraiment agréable avec le sous-typage. Vous ne pouvez pas envoyer à une instruction CPU. Vous ne pouvez pas envoyer d' une instruction CPU. Je veux dire que le point entier du sous-typage est que vous pouvez utiliser un
D
où vous pouvezB
. Les instructions du processeur ne sont pas polymorphes. Pour que les primitives le fassent, vous devez encapsuler leurs opérations avec une logique de répartition qui coûte plusieurs fois la quantité d'opérations comme simple ajout (ou autre). L'avantage d'avoirint
fait partie de la hiérarchie de types devient un peu discutable lorsqu'elle est scellée / finale. Et cela ignore tous les maux de tête avec la logique de répartition pour les opérateurs binaires ...Fondamentalement, les types primitifs devraient avoir beaucoup de règles spéciales sur la façon dont le compilateur les gère et sur ce que l'utilisateur peut faire avec leurs types de toute façon , il est donc souvent plus simple de simplement les traiter comme complètement distincts.
la source
int + int
peut être un opérateur régulier au niveau du langage qui invoque une instruction intrinsèque qui est garantie de compiler (ou de se comporter comme) l'opération d'addition d'entier CPU native. L'avantage d'int
hériter deobject
n'est pas seulement la possibilité d'hériter d'un autre typeint
, mais aussi la possibilité deint
se comporter comme unobject
sans boxe. Considérez les génériques C #: vous pouvez avoir la covariance et la contravariance, mais elles ne s'appliquent qu'aux types de classe - les types de structure sont automatiquement exclus, car ils ne peuventobject
passer que par la boxe (implicite, générée par le compilateur).Il n'y a que très peu de cas où vous avez besoin de «types fondamentaux» pour être des objets complets (ici, un objet est des données qui contiennent soit un pointeur vers un mécanisme de répartition, soit sont étiquetées avec un type qui peut être utilisé par un mécanisme de répartition):
Vous souhaitez que les types définis par l'utilisateur puissent hériter des types fondamentaux. Ceci n'est généralement pas souhaité car il introduit des maux de tête liés aux performances et à la sécurité. Il s'agit d'un problème de performances car la compilation ne peut pas supposer qu'une
int
aura une taille fixe spécifique ou qu'aucune méthode n'a été remplacée, et c'est un problème de sécurité car la sémantique deint
s peut être subvertie (considérez un entier égal à n'importe quel nombre, ou qui change sa valeur plutôt que d'être immuable).Vos types primitifs ont des supertypes et vous voulez avoir des variables avec le type d'un supertype d'un type primitif. Par exemple, supposez que vos
int
s le sontHashable
et que vous souhaitez déclarer une fonction qui accepte unHashable
paramètre qui pourrait recevoir des objets normaux mais également desint
s.Cela peut être «résolu» en rendant ces types illégaux: supprimez le sous-typage et décidez que les interfaces ne sont pas des types mais des contraintes de type. Évidemment, cela réduit l'expressivité de votre système de type, et un tel système de type ne serait plus appelé orienté objet. Voir Haskell pour un langage qui utilise cette stratégie. C ++ est à mi-chemin car les types primitifs n'ont pas de supertypes.
L'alternative est la boxe complète ou partielle des types fondamentaux. Le type de boxe n'a pas besoin d'être visible par l'utilisateur. Essentiellement, vous définissez un type encadré interne pour chaque type fondamental et des conversions implicites entre le type encadré et le type fondamental. Cela peut devenir gênant si les types encadrés ont une sémantique différente. Java présente deux problèmes: les types encadrés ont un concept d'identité tandis que les primitives n'ont qu'un concept d'équivalence de valeur, et les types encadrés sont annulables tandis que les primitives sont toujours valides. Ces problèmes sont complètement évitables en n'offrant pas de concept d'identité pour les types de valeur, en offrant une surcharge d'opérateur et en ne rendant pas tous les objets annulables par défaut.
Vous ne disposez pas de saisie statique. Une variable peut contenir n'importe quelle valeur, y compris les types ou objets primitifs. Par conséquent, tous les types primitifs doivent toujours être encadrés afin de garantir un typage fort.
Les langages qui ont un typage statique font bien d'utiliser des types primitifs dans la mesure du possible et ne retiennent que les types encadrés en dernier recours. Bien que de nombreux programmes ne soient pas extrêmement sensibles aux performances, il existe des cas où la taille et la composition des types primitifs sont extrêmement pertinentes: pensez à la compression de nombres à grande échelle où vous devez insérer des milliards de points de données en mémoire. Passer de
double
àfloat
pourrait être une stratégie d'optimisation de l'espace viable en C, mais elle n'aurait pratiquement aucun effet si tous les types numériques sont toujours encadrés (et donc gaspillent au moins la moitié de leur mémoire pour un pointeur de mécanisme de répartition). Lorsque des types primitifs encadrés sont utilisés localement, il est assez simple de supprimer la boxe en utilisant les intrinsèques du compilateur, mais il serait à courte vue de parier les performances globales de votre langage sur un «compilateur suffisamment avancé».la source
int
n'est guère immuable dans toutes les langues.int
valeur est immuable, mais pas uneint
variable.get rid of subtyping and decide that interfaces aren't types but type constraints.... such a type system wouldn't be called object-oriented any longer
mais cela ressemble à une programmation basée sur un prototype, qui est définitivement une POO.La plupart des implémentations que je connais imposent trois restrictions à ces classes qui permettent au compilateur d'utiliser efficacement les types primitifs comme représentation sous-jacente la grande majorité du temps. Ces restrictions sont:
Les situations dans lesquelles un compilateur doit placer une primitive dans un objet dans la représentation sous-jacente sont relativement rares, comme lorsqu'une
Object
référence pointe vers elle.Cela ajoute un peu de gestion de cas spéciaux dans le compilateur, mais ce n'est pas seulement limité à un compilateur super avancé mythique. Cette optimisation se fait dans de vrais compilateurs de production dans les principales langues. Scala vous permet même de définir vos propres classes de valeur.
la source
Dans Smalltalk, tous (int, float, etc.) sont des objets de première classe. Le seul cas particulier est que les SmallIntegers sont codifiés et traités différemment par la machine virtuelle pour des raisons d'efficacité, et donc la classe SmallInteger n'admettra pas de sous-classes (ce qui n'est pas une limitation pratique). Notez que cela ne nécessite aucune considération particulière de la part du programmeur car la distinction est circonscrite à des routines automatiques comme la génération de code ou la récupération de place.
Le Smalltalk Compiler (code source -> VM bytecodes) et le VM nativizer (bytecodes -> machine code) optimisent le code généré (JIT) afin de réduire la pénalité des opérations élémentaires avec ces objets de base.
la source
Je concevais une jauge OO et un runtime (cela a échoué pour un ensemble complètement différent de raisons).
Il n'y a rien de mal à créer des choses comme des classes vraies int; en fait, cela rend le GC plus facile à concevoir car il n'y a maintenant que 2 types d'en-têtes de tas (classe et tableau) plutôt que 3 (classe, tableau et primitif) [le fait que nous pouvons fusionner classe et tableau après que cela ne soit pas pertinent ].
Le vrai cas important, les types primitifs devraient avoir principalement des méthodes finales / scellées (+ importe vraiment, ToString pas tellement). Cela permet au compilateur de résoudre statiquement presque tous les appels aux fonctions elles-mêmes et de les aligner. Dans la plupart des cas, cela n'a pas d'importance en tant que comportement de copie (j'ai choisi de rendre l'incorporation disponible au niveau du langage [comme l'a fait .NET]), mais dans certains cas, si les méthodes ne sont pas scellées, le compilateur sera forcé de générer l'appel à la fonction utilisée pour implémenter int + int.
la source