Comparaison des paradigmes de programmation - Comparison of programming paradigms

Cet article tente de présenter les diverses similitudes et différences entre les différents paradigmes de programmation sous forme de résumé sous forme graphique et tabulaire avec des liens vers les discussions séparées concernant ces similitudes et différences dans les articles Wikipédia existants.

Principales approches paradigmatiques

Il existe deux approches principales de la programmation :

Les éléments suivants sont largement considérés comme les principaux paradigmes de programmation, comme on le voit lors de la mesure de la popularité du langage de programmation :

Voici les types courants de programmation qui peuvent être mis en œuvre à l'aide de différents paradigmes :

Les sous-programmes qui implémentent les méthodes OOP peuvent être finalement codés dans un style impératif, fonctionnel ou procédural qui peut, ou non, modifier directement l' état au nom du programme appelant. Il y a inévitablement un certain chevauchement entre les paradigmes, mais les principales caractéristiques ou différences identifiables sont résumées dans ce tableau :

Paradigme La description Caractéristiques principales Paradigme(s) associé(s) La critique Exemples
Impératif Programmes sous forme d' instructions qui modifient directement l' état calculé ( champs de données ) Affectations directes , structures de données communes , variables globales Edsger W. Dijkstra , Michael A. Jackson C , C++ , Java , Kotlin , PHP , Python , Ruby
Structuré Un style de programmation impérative avec une structure de programme plus logique Structograms , indentation , non ou une utilisation limitée de goto déclarations Impératif C , C++ , Java , Kotlin , Pascal , PHP , Python
De procédure Dérivé de la programmation structurée, basée sur le concept de programmation modulaire ou l' appel de procédure Variables locales , séquence, sélection, itération et modularisation Structuré, impératif C , C++ , Lisp , PHP , Python
Fonctionnel Traite le calcul comme l'évaluation de fonctions mathématiques en évitant les données d' état et mutables Calcul lambda , compositionnalité , formule , récursivité , transparence référentielle , aucun effet secondaire Déclaratif C++ , C# , Clojure , CoffeeScript , Elixir , Erlang , F# , Haskell , Java (depuis la version 8), Kotlin , Lisp , Python , R , Ruby , Scala , SequenceL , Standard ML , JavaScript , Elm
Basé sur les événements, y compris sur le temps Le flux de contrôle est déterminé principalement par des événements , tels que des clics de souris ou des interruptions, y compris une minuterie Boucle principale , gestionnaires d'événements, processus asynchrones Procédure, flux de données JavaScript , ActionScript , Visual Basic , Elm
Orienté objet Traite les champs de données comme des objets manipulés via des méthodes prédéfinies uniquement Objets , méthodes, passage de messages , dissimulation d'information , abstraction de données , encapsulation , polymorphisme , héritage , sérialisation -marshalling De procédure Wikipédia , autres Common Lisp , C++ , C# , Eiffel , Java , Kotlin , PHP , Python , Ruby , Scala , JavaScript
Déclaratif Définit la logique du programme, mais pas le flux de contrôle détaillé Langages de quatrième génération , tableurs , générateurs de programmes de rapports SQL , expressions régulières , Prolog , OWL , SPARQL , Datalog , XSLT
Programmation basée sur des automates Traite les programmes comme un modèle de machine à états finis ou de tout autre automate formel Énumération d' état , variable de contrôle , changements d' état , isomorphisme , table de transition d'état Impératif, événementiel Langage machine à états abstraits

Différences de terminologie

Malgré de multiples (types de) paradigmes de programmation existant en parallèle (avec parfois des définitions apparemment contradictoires), de nombreux composants fondamentaux sous-jacents restent plus ou moins les mêmes ( constantes , variables , champs de données , sous - routines , appels, etc.) et doivent donc inévitablement être incorporé dans chaque paradigme séparé avec des attributs ou des fonctions également similaires. Le tableau ci-dessus n'est pas conçu comme un guide des similitudes précises, mais plutôt comme un index des endroits où chercher plus d'informations, basé sur les différentes dénominations de ces entités, au sein de chaque paradigme. Les implémentations non standardisées de chaque paradigme, dans de nombreux langages de programmation , en particulier les langages prenant en charge plusieurs paradigmes , chacun avec son propre jargon compliquent encore les choses .

Support linguistique

Le sucre syntaxique est l' adoucissement des fonctionnalités du programme en introduisant des fonctionnalités de langage qui facilitent un usage donné, même si le résultat final pourrait être atteint sans eux. Un exemple de sucre syntaxique peut sans doute être les classes utilisées dans les langages de programmation orientés objet . Le langage impératif C peut prendre en charge la programmation orientée objet via ses fonctionnalités de pointeurs de fonction, de conversion de type et de structures. Cependant, des langages tels que C++ visent à rendre la programmation orientée objet plus pratique en introduisant une syntaxe spécifique à ce style de codage. De plus, la syntaxe spécialisée travaille pour mettre l'accent sur l'approche orientée objet. De même, les fonctions et la syntaxe de boucle en C (et d'autres langages de programmation procéduraux et structurés) pourraient être considérées comme du sucre syntaxique. Le langage assembleur peut prendre en charge la programmation procédurale ou structurée via ses fonctions de modification des valeurs de registre et de branchement d'exécution en fonction de l'état du programme. Cependant, des langages tels que C ont introduit une syntaxe spécifique à ces styles de codage pour rendre la programmation procédurale et structurée plus pratique. Les fonctionnalités du langage C# (C Sharp), telles que les propriétés et les interfaces, ne permettent pas non plus de nouvelles fonctions, mais sont conçues pour rendre les bonnes pratiques de programmation plus évidentes et naturelles.

Certains programmeurs estiment que ces fonctionnalités sont sans importance ou même futiles. Par exemple, Alan Perlis a plaisanté une fois, dans une référence aux langues délimitées par des crochets , que "le sucre syntaxique provoque le cancer du point - virgule " (voir Épigrammes sur la programmation ).

Une extension de ceci est la saccharine syntaxique , ou syntaxe gratuite qui ne facilite pas la programmation.

Comparaison des performances

Dans la longueur totale du chemin d'instruction uniquement, un programme codé dans un style impératif, n'utilisant aucun sous-programme, aurait le nombre le plus bas. Cependant, la taille binaire d'un tel programme peut être plus grande que le même programme codé à l'aide de sous-routines (comme dans la programmation fonctionnelle et procédurale) et ferait référence à davantage d' instructions physiques non locales qui pourraient augmenter les échecs de cache et la surcharge de récupération d'instructions dans les processeurs modernes .

Les paradigmes qui utilisent largement les sous-routines (y compris fonctionnelles, procédurales et orientées objet) et n'utilisent pas également une expansion en ligne significative (inline, via les optimisations du compilateur ) utiliseront, par conséquent, une plus grande fraction des ressources totales sur les liaisons de sous-routine. Les programmes orientés objet qui ne modifient pas délibérément l' état du programme directement, utilisant à la place des méthodes de mutation (ou setters ) pour encapsuler ces changements d'état, auront, en conséquence directe, plus de temps système. En effet , la transmission de messages est essentiellement un appel de sous-routine, mais avec trois surcharges supplémentaires : allocation de mémoire dynamique , copie de paramètres et répartition dynamique . L'obtention de mémoire à partir du tas et la copie des paramètres pour le passage des messages peuvent impliquer des ressources importantes qui dépassent de loin celles nécessaires au changement d'état. Les accesseurs (ou getters ) qui renvoient simplement les valeurs des variables membres privées dépendent également de sous-routines de transmission de messages similaires, au lieu d'utiliser une affectation (ou une comparaison) plus directe, ajoutant à la longueur totale du chemin.

Code géré

Pour les programmes s'exécutant dans un environnement de code managé , tel que le .NET Framework , de nombreux problèmes affectent les performances qui sont considérablement affectées par le paradigme du langage de programmation et les diverses fonctionnalités du langage utilisées.

Exemples de pseudocodes comparant différents paradigmes

Une comparaison de pseudocodes d'approches impératives, procédurales et orientées objet utilisées pour calculer l'aire d'un cercle (πr²), en supposant qu'il n'y a pas d' incorporation de sous-programmes , pas de préprocesseurs de macros , arithmétique de registre et pondération de chaque instruction « étape » comme une seule instruction - en tant que mesure brute de la longueur du chemin d'instruction - est présentée ci-dessous. L'étape d'instruction qui effectue conceptuellement le changement d'état est mise en évidence en caractères gras dans chaque cas. Les opérations arithmétiques utilisées pour calculer l'aire du cercle sont les mêmes dans les trois paradigmes, à la différence que les paradigmes procédural et orienté objet enveloppent ces opérations dans un appel de sous-programme qui rend le calcul général et réutilisable. Le même effet pourrait être obtenu dans un programme purement impératif utilisant un préprocesseur de macro uniquement au prix d'une augmentation de la taille du programme (uniquement à chaque site d'invocation de macro) sans un coût d'exécution au prorata correspondant (proportionnel à n invocations - qui peut être situé dans un boucle intérieure par exemple). Inversement, l'incorporation de sous-programmes par un compilateur pourrait réduire les programmes procéduraux à quelque chose de similaire en taille au code purement impératif. Cependant, pour les programmes orientés objet, même avec l'inlining, les messages doivent toujours être construits (à partir de copies des arguments) pour être traités par les méthodes orientées objet. La surcharge des appels, virtuels ou autres, n'est pas dominée par la modification du flux de contrôle - mais par les coûts de la convention d'appel environnante , comme le code du prologue et de l'épilogue , la configuration de la pile et le passage d' arguments (voir ici pour une longueur de chemin d'instruction plus réaliste, une pile et d'autres coûts associés aux appels sur une plate-forme x86 ). Voir aussi ici pour une présentation de diapositives d' Eric S. Roberts ("The Allocation of Memory to Variables", chapitre 7) - illustrant l'utilisation de la mémoire de pile et de tas lors de la somme de trois nombres rationnels dans le langage Java orienté objet.

Impératif De procédure Orienté objet
 load r;                      1
 r2 = r * r;                  2
 result = r2 * "3.142";       3
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.... storage .............
result variable
constant "3.142"
area proc(r2,res):
   push stack                                 5
   load r2;                                   6
   r3 = r2 * r2;                              7
   res = r3 * "3.142";                        8
   pop stack                                  9
   return;                                   10
...............................................
main proc:
   load r;                                    1
   call area(r,result);
    +load p = address of parameter list;      2
    +load v = address of subroutine 'area';   3
    +goto v with return;                      4
.
.
.
.
.... storage .............
result variable
constant "3.142"
parameter list variable
function pointer (==>area)
stack storage
circle.area method(r2):
   push stack                                 7
   load r2;                                   8
   r3 = r2 * r2;                              9
   res = r3 * "3.142";                       10
   pop stack                                 11
   return(res);                           12,13
...............................................
main proc:
   load r;                                    1
   result = circle.area(r);
      +allocate heap storage;                 2
      +copy r to message;                     3
      +load p = address of message;           4
      +load v = addr. of method 'circle.area' 5
      +goto v with return;                    6
.
.
.... storage .............
result variable (assumed pre-allocated)
immutable variable "3.142" (final)
(heap) message variable for circle method call
vtable(==>area)
stack storage

Les avantages de l'abstraction procédurale et du polymorphisme de style orienté objet sont mal illustrés par un petit exemple comme celui ci-dessus. Cet exemple est principalement conçu pour illustrer certaines différences de performances intrinsèques, et non l'abstraction ou la réutilisation de code.

Sous-programme, surcharge d'appel de méthode

La présence d'un sous-programme (appelé) dans un programme n'apporte rien de plus à la fonctionnalité du programme quel que soit le paradigme, mais peut grandement contribuer à la structuration et à la généralité du programme, le rendant beaucoup plus facile à écrire, modifier et étendre. La mesure dans laquelle différents paradigmes utilisent des sous-routines (et leurs besoins en mémoire qui en découlent) influence les performances globales de l'algorithme complet, bien que, comme Guy Steele l'a souligné dans un article de 1977, une implémentation de langage de programmation bien conçue puisse avoir des frais généraux très faibles pour l'abstraction procédurale. (mais déplore, dans la plupart des implémentations, qu'ils y parviennent rarement dans la pratique - étant "plutôt inconsidérés ou négligents à cet égard"). Dans le même article, Steele plaide également en faveur de la programmation basée sur des automates (en utilisant des appels de procédure avec récursivité de la queue ) et conclut que "nous devrions avoir un respect sain pour les appels de procédure" (car ils sont puissants) mais a suggéré de "les utiliser avec parcimonie "

Dans la fréquence des appels de sous-programme :

  • Pour la programmation procédurale, la granularité du code est largement déterminée par le nombre de procédures ou de modules discrets .
  • Pour la programmation fonctionnelle, les appels fréquents aux sous-programmes de la bibliothèque sont courants, mais peuvent être souvent intégrés par le compilateur d'optimisation
  • Pour la programmation orientée objet, le nombre d'appels de méthode invoqués est également en partie déterminé par la granularité des structures de données et peut donc inclure de nombreux accès en lecture seule à des objets de bas niveau qui sont encapsulés, et donc accessibles dans aucun autre, plus direct, manière. Étant donné qu'une granularité accrue est une condition préalable à une plus grande réutilisation du code , la tendance est aux structures de données à grain fin et à une augmentation correspondante du nombre d'objets discrets (et de leurs méthodes) et, par conséquent, d'appels de sous-programmes. La création d' objets divins est activement découragée. Les constructeurs ajoutent également au nombre car ils sont également des appels de sous-programme (à moins qu'ils ne soient en ligne). Les problèmes de performances causés par une granularité excessive peuvent ne pas devenir apparents jusqu'à ce que l' évolutivité devienne un problème.
  • Pour d'autres paradigmes, où un mélange des paradigmes ci-dessus peut être utilisé, l'utilisation de sous-programmes est moins prévisible.

Allocation de mémoire dynamique pour le stockage de messages et d'objets

De manière unique, le paradigme orienté objet implique une allocation de mémoire dynamique à partir du stockage de tas pour la création d'objets et la transmission de messages. Une référence de 1994 - "Coûts d'allocation de mémoire dans les grands programmes C et C++" menée par Digital Equipment Corporation sur une variété de logiciels, à l'aide d'un outil de profilage au niveau des instructions, a mesuré le nombre d'instructions nécessaires par allocation de stockage dynamique. Les résultats ont montré que le plus petit nombre absolu d'instructions exécutées était en moyenne d'environ 50, mais que d'autres atteignaient 611. Voir aussi « Heap : Pleasures and pains » de Murali R. Krishnan qui indique que « les implémentations de tas ont tendance à rester générales pour toutes les plateformes, et donc avoir des frais généraux lourds". L'article d'IBM de 1996 "Scalabilité des algorithmes d'allocation de stockage dynamique" par Arun Iyengar d'IBM démontre divers algorithmes de stockage dynamique et leurs nombres d'instructions respectifs. Même l'algorithme MFLF I recommandé (HS Stone, RC 9674) affiche un nombre d'instructions compris entre 200 et 400. L'exemple de pseudocode ci-dessus n'inclut pas d'estimation réaliste de cette longueur de chemin d'allocation de mémoire ou des frais généraux de préfixe de mémoire impliqués et des déchets associés qui en découlent. frais généraux de collecte. Suggérant fortement que l'allocation de tas est une tâche non triviale, un micro-allocateur de logiciel open source , par le développeur de jeux John W. Ratcliff , se compose de près de 1 000 lignes de code.

Appels de message distribués dynamiquement contre frais généraux d'appel de procédure directe

Dans leur résumé " Optimization of Object-Oriented Programs Using Static Class Hierarchy Analysis ", Jeffrey Dean, David Grove et Craig Chambers du Department of Computer Science and Engineering, à l' Université de Washington , affirment que " Heavy use of legacy and dynamically -bound messages est susceptible de rendre le code plus extensible et réutilisable, mais il impose également une surcharge de performances significative, par rapport à un programme équivalent mais non extensible écrit d'une manière non orientée objet.Dans certains domaines, tels que les packages graphiques structurés , le coût en termes de performances de la flexibilité supplémentaire fournie par l'utilisation d'un style fortement orienté objet est acceptable. Cependant, dans d'autres domaines, tels que les bibliothèques de structure de données de base, les progiciels de calcul numérique, les bibliothèques de rendu et les cadres de simulation basés sur les traces, le coût de le passage de messages peut être trop important, obligeant le programmeur à éviter la programmation orientée objet dans les « points chauds » de leur application."

Sérialisation d'objets

La sérialisation impose des frais généraux importants lors du transfert d' objets d'un système à un autre, en particulier lorsque le transfert se fait dans des formats lisibles par l'homme tels que le langage de balisage extensible ( XML ) et la notation d'objets JavaScript ( JSON ). Cela contraste avec les formats binaires compacts pour les données non orientées objet. L'encodage et le décodage de la valeur des données de l'objet et de ses attributs sont impliqués dans le processus de sérialisation, qui comprend également la prise de conscience de problèmes complexes tels que l'héritage, l'encapsulation et le masquage des données.

Traitement en parallèle

Robert Harper, professeur à l'Université Carnegie-Mellon, a écrit en mars 2011 : « Ce semestre, Dan Licata et moi-même enseignons un nouveau cours sur la programmation fonctionnelle pour les futurs étudiants de première année en informatique... La programmation orientée objet est entièrement éliminée du programme d'introduction. , car il est à la fois anti-modulaire et anti-parallèle de par sa nature même, et donc inadapté à un programme d'enseignement informatique moderne. Un nouveau cours proposé sur la méthodologie de conception orientée objet sera proposé au niveau de deuxième année pour les étudiants qui souhaitent étudier ce sujet."

Voir également

Les références

Lectures complémentaires

Liens externes