Page 1 sur 1

Une approche comparative programmation classique / POO

Publié : jeu. 22/sept./2005 12:34
par fweil
Voilà, on se demandait avec Heis si l'utilisation de la POO apportait ou non quelque chose au niveau des performances.

Le style de programmation par interfaces est séduisant dans certains cas, mais est-il aussi rapide qu'une programmation classique.

Après avoir réfléchi à une approche de test, comment trouver un jeu d'essai simple permettant de comparer ce qui est comparable ? j'ai produit ce petit bout pour vérifier les choses.

A faire tourner, pour ceux que ça intéresse, avec le debug off, sinon attention les temps d'exécutions et les résultats non significatifs.

Le principe de ce test consiste à effectuer une addition de trois entiers. On effectue cette opération de différentes manières, à commencer par une addition directe sans appel de procédure, à titre de comparaison.

L'interface que Heis a écrit est assez simple, et offre un petit avantage par rapport à celui de la doc, c'est qu'il fonctionne tout de suite.

Code : Tout sélectionner

Interface Class
  ClassInit(Val1.l, Val2.l, Val3.l)
  ClassAdd()
  ClassFree()
EndInterface

Structure Class_Functions
  ClassInit.l
  ClassAdd.l
  ClassFree.l
EndStructure

Structure Class_Vars
  *VirtualTable.Class_Functions
  Val1.l
  Val2.l
  Val3.l
EndStructure

Procedure ClassAdd(*this.Class_Vars)
  ProcedureReturn *this\Val1 + *this\Val2 + *this\Val3
EndProcedure

Structure Class_Holder
  VT.Class_Functions
  Impl.Class_Vars
EndStructure

NewList Instances.Class_Holder()

Procedure AtomAdd_by_val(x, y, z)
  ProcedureReturn x + y + z
EndProcedure

Procedure AtomAdd_by_ref(*x, *y, *z)
  !  MOV     esi, dword [esp]
  !  MOV     eax, [esi]
  !  ADD     eax, [esi+4]
  !  ADD     eax, [esi+8]
  ProcedureReturn
EndProcedure

Procedure ClassFree(*this.Class_Vars)
  *this\Val1 = 0
  *this\Val2 = 0
  *this\Val3 = 0
  DeleteElement(Instances())
EndProcedure

Procedure ClassInit(Val1.l, Val2.l, Val3.l)
  AddElement(Instances())
  Instances()\VT\ClassAdd = @ClassAdd()
  Instances()\VT\ClassFree = @ClassFree()
  Instances()\Impl\VirtualTable = Instances()\VT
  Instances()\Impl\Val1 = Val1
  Instances()\Impl\Val2 = Val2
  Instances()\Impl\Val3 = Val3
  ProcedureReturn @Instances()\Impl
EndProcedure

  First.Class = ClassInit(1, 2, 3)
  Second.Class = ClassInit(4, 5, 6)

  NTimes = 100000000
  a = 1
  b = 2
  c = 3
  d = 4
  e = 5
  f = 6
  *a = @a
  *b = @b
  *c = @c
  *d = @d
  *e = @e
  *f = @f
  
  Titre.s = "Addition directe de trois entiers dans le code principal"
  tz = ElapsedMilliseconds()
  For i = 1 To NTimes
    iFirst.l = a + b + c
    iSecond.l = d + e + f
  Next
  tz = ElapsedMilliseconds() - tz
  Result.s = Titre + Chr(10) + "First : " + Str(iFirst) + "  Second : " + Str(iSecond) + " Done in " + Str(tz) + "ms" + Chr(10)

  Titre.s = "Addition de trois entiers par appel de procédure classique avec transmission par valeurs"
  tz = ElapsedMilliseconds()
  For i = 1 To NTimes
    iFirst.l = AtomAdd_by_val(a, b, c)
    iSecond.l = AtomAdd_by_val(d, e, f)
  Next
  tz = ElapsedMilliseconds() - tz
  Result + Titre + Chr(10) + "First : " + Str(iFirst) + "  Second : " + Str(iSecond) + " Done in " + Str(tz) + "ms" + Chr(10)

  Titre.s = "Addition de trois entiers par appel de procédure classique avec transmission par adresses"
  tz = ElapsedMilliseconds()
  For i = 1 To NTimes
    iFirst.l = AtomAdd_by_ref(@a, @b, @c)
    iSecond.l = AtomAdd_by_ref(@d, @e, @f)
  Next
  tz = ElapsedMilliseconds() - tz
  Result + Titre + Chr(10) + "First : " + Str(iFirst) + "  Second : " + Str(iSecond) + " Done in " + Str(tz) + "ms" + Chr(10)

  Titre.s = "Addition de trois entiers par appel de procédure classique avec transmission par pointeurs"
  tz = ElapsedMilliseconds()
  For i = 1 To NTimes
    iFirst.l = AtomAdd_by_ref(*a, *b, *c)
    iSecond.l = AtomAdd_by_ref(*d, *e, *f)
  Next
  tz = ElapsedMilliseconds() - tz
  Result + Titre + Chr(10) + "First : " + Str(iFirst) + "  Second : " + Str(iSecond) + " Done in " + Str(tz) + "ms" + Chr(10)

  Titre.s = "Addition de trois entiers par appel de procédure par interface"
  tz = ElapsedMilliseconds()
  For i = 1 To NTimes
    iFirst.l = First\ClassAdd()
    iSecond.l = Second\ClassAdd()
  Next
  tz = ElapsedMilliseconds() - tz
  Result + Titre + Chr(10) + "First : " + Str(iFirst) + "  Second : " + Str(iSecond) + " Done in " + Str(tz) + "ms" + Chr(10)

  MessageRequester("Result", Result, #PB_MessageRequester_Ok)
  
  First\ClassFree()
  Second\ClassFree()
  
End

Publié : jeu. 22/sept./2005 15:03
par dlolo
La réponse semble être claire, non ?

Image

Publié : jeu. 22/sept./2005 15:18
par fweil
Oui je trouve cela très clair. L'approche POO est performante.

Enfin ce n'est pas une démonstration définitive et absolue. Ici il s'agit d'un cas de figure choisi, non pas pour démontrer ce que l'on constate, mais disons choisi pour faire simple.

Je voulais m'assurer que les transferts de données via les interfaces ne donnaient pas de dégradation de performances, ce qui est ici bien mis en évidence à priori.

Publié : jeu. 22/sept./2005 15:46
par nico
Ce qui fait une légère différence de 0.054% par rapport à l'appel classique.

Tout va bien. :)

Publié : jeu. 22/sept./2005 16:02
par fweil
nico, attention aux écarts affichés, si ce n'est pas du simple au double, ce n'est pas toujours très significatif.

Enfin j'exagère, mais on constate souvent en première approche de benchmarking que des écarts ressortent des fois dans un sens, des fois dans l'autre, selon comment on place les séquences de test dans le code, etc.

Là on a en gros des temps qui sont les mêmes, voir selon la machine utilisée à l'avantage de l'adressage par interface.

Le plus difficile quand on fait des mesures de perfs est de comprendre quand on trouve un mieux de 5 ou 10% en modifiant le code, si il ne s'agit pas d'un artefact, parce que le PC au moment du test avat fini de ranger son swap !!! Comme on tourne sur des systèmes évolués, il ne faut pas oublier qu'on a jamais droit vraiment à toute la CPU, les bus, les trucs et les machins en direct.

Ce qui doit nous amener à déduire que tant qu'on est dans les +- 10% il n'y a pas de quoi faire une université.

Mais je suis en tout cas rassuré sur le fait que l'utilisation des interfaces est bien gérée par le compilateur.

Publié : jeu. 22/sept./2005 22:25
par LeCyb
Moi ce qui me laisse sur le carreau c'est le rapport de 10 entre le code direct et par procédure.
J'aurais jamais pensé qu'il y avait un tel écart de performance entre les deux 8O.

Publié : jeu. 22/sept./2005 23:27
par fweil
@LeCyb,

Il faut bien raisonner et comparer ce qui est comparable.

Sur les CPU de maintenant, on doit empiler, lors de l'appel de procédure, et dépiler au retour, une demie douzaine de registres.

Donc quand on fait une simple addition dans une procédure, le poids de la gestion est lourd en proportion.

Si la procédure effectue un long et laborieux travail, ce poids relatif devient souvent insignifiant.

C'est la raison pour laquelle en optimisation on doit éviter de faire appel à des procédures pour un oui ou pour un non.

Il est par exemple louable de faire une procédure Min() ou Max(), mais si on veut traiter en termes de performances, il est préférable de faire un test plutôt qu'un appel à une procédure certe élégante, mais beaucoup plus coûteuse qu'il n'y paraît.

Tout dépend de ce que l'on souhaite comme résultat global. Est-ce le temps ou la valeur qui compte ?

J'ai posté ce benchmark en Discussion Générale, après réflexion, parce qu'il conduit à plein de réflexions.

Pour échapper à ce coût de gestion des registres, la seule solution serait de faire appel à des procédures optimisées en assembleur. Ce qui veut dire qu'en matière d'optimisation, il restera toujours de beaux jours à l'assembleur.

C'est normal car le propos ne se situe pas du tout au même niveau.

Si je décide de monopoliser le processeur et de ne rien respecter de l'environnement extérieur, je peux prétendre obtenir une addition en un cycle d'horloge, ou deux si je fais appel à un mot mémoire indirect.

Mais si je veux continuer de pouvoir utiliser mon système d'exploitation, mes périphériques, mon réseau et tout ce qui se trouve autour pendant que je fais mon addition, celà a un coût.

Lorsque je mets en place une procédure sous le contrôle du langage évolué et du compilateur, je laisse le système d'exploitation maître du jeu.

Si je veux aller dix fois plus vite, j'empêche pratiquement toute intervention de celui-ci pendant la suite d'instructions qui effectuent ce que j'ai décidé en tant que programmeur.

C'est en gros la raison pour laquelle on voit des différences aussi importantes.

Mais le corrolaire de ce gain de performances, lorsque je requière le processeur pour moi tout seul, c'est que le style de programation est moins sympathique.

Quand je concois un programme d'un niveau intellectuellement plus avancé, je suis vite lassé d'écrire mon test de variables plutôt que Min() ou Max().

L'abstraction que me permet l'appel à des procédures me permet de tenter une opération plus intelligente, mais c'est au prix du temps !

Publié : ven. 23/sept./2005 0:10
par Backup
finalement la programmation "spagheti" est encore le plus rapide !
pas de procedure ! que des variables globale bien typé !
des GOTO en veut tu en voila !
des calcul direct la ou tu te trouve (dans le listing) ! bref le bonheur ! :D

ps : j'ai quand meme fini par adopter les structures et liste chainés !! :D

mais j'ai du mal a lacher mes tableaux DIM
et le precalculé avec les DATA !! :D


pour moi une interface c'est la RS232 :lol:

Publié : ven. 23/sept./2005 8:29
par Dräc
Je propose de s’intéresser aussi aux pointeurs de fonction.
Bien sur, on peut tester beaucoup de choses (exemple n°1), mais j’ai tenté de traduire comment le compilateur interprète l’utilisation de l’interface. (exemple n°2)

Code : Tout sélectionner

Titre.s = "Addition de trois entiers par appel de procédure par pointer de fonction version 1)" 
tz = ElapsedMilliseconds() 
For i = 1 To NTimes 
  iFirst.l = CallFunctionFast(@AtomAdd_by_ref(),  *a, *b, *c) 
  iSecond.l = CallFunctionFast(@AtomAdd_by_ref(),  *d, *e, *f) 
Next 
tz = ElapsedMilliseconds() - tz 
result + Titre + Chr(10) + "First : " + Str(iFirst) + "  Second : " + Str(iSecond) + " Done in " + Str(tz) + "ms" + Chr(10) 

*First.Class_Vars = First
*Second.Class_Vars= Second
Titre.s = "Addition de trois entiers par appel de procédure par pointer de fonction (version 2)" 
tz = ElapsedMilliseconds() 
For i = 1 To NTimes 
  iFirst.l = CallFunctionFast(*First\VirtualTable\ClassAdd,  *First) 
  iSecond.l = CallFunctionFast(*Second\VirtualTable\ClassAdd,  *Second) 
Next 
tz = ElapsedMilliseconds() - tz 
result + Titre + Chr(10) + "First : " + Str(iFirst) + "  Second : " + Str(iSecond) + " Done in " + Str(tz) + "ms" + Chr(10)