Donc peu d'intérêt pour le cas qui nous occupe : comme je le disais au message précédent, si c'est pour réécrire la plupart des méthodes du template, aucun intérêt d'utiliser la STL et vive le code en dur ! Plus rapide à développer, plus facile à maintenir, plus performant... Enfin, dans le contexte que je décris bien entendu.
Ca ne fait rien (une exception, ça serait justement un comportement de vérification des bornes, là, c'est généralement du segfault).
Je suis par ailleurs d'accord avec toi, c'est contraire à l'esprit, mais Microsoft s'est tellement fait allumer sur la sécurité qu'ils sont allés au bout de l'idée...
Comme on dit, c'est un comportement indéfini...
L'intérêt est beaucoup plus important qu'il ne peut y paraître de prime abord.
Bien sur, l'héritage privé doit être considéré avec une extrême prudence.
Mais, justement, si tu décide d'utiliser des politiques personnelles, tu peux, malgré tout, te contenter de "remonter" les fonctions (publiques)qui t'intéressent "telles quelles" de la classe vector vers l'accessibilité publique, tout en cachant complètement le fait que tu travail avec un std::vector, en laissant privées les fonctions qui ne t'intéressent pas, et en fournissant, pourquoi pas, des fonctions qui te sont propres (basées sur tes politiques personnelles ou simplement sur des besoins que ta classe doit rejoindre)
Sauf que, comme je le disais, c'est du boulot "énorme" pour un gain absolument nul, voire "négatif", par rapport à une implémentation optimisée et "en dur"... Y compris avec un template dédié au besoin !
OK, on peut le faire avec la STL, et arriver aux mêmes perfs qu'un code optimisé violent. Ceci étant dit, ce n'est pas forcément souhaitable : plus de code source, plus de maintenance, plus de risques d'être dépendant de l'implémentation de la STL, et, pire, plus long à faire que le code brutal...
Soyons bien d'accord sur un truc, par contre : ce que je dis, c'est pour du code très bas niveau, ultra-spécifique, et pour lequel les performances sont primordiales. En second, comme d'habitude, le temps de dév initial ainsi que la maintenabilité sont également importants.
Or, dans ce cas précis et spécifique, passer par la STL n'offre aucun avantage particulier, et même au contraire.
Alors autant faire du C... Pour du si bas niveau (même si c'est possible en C++) plus naturellement je me tournerais vers le C.
Sauf qu'un module C, intégré dans du code C++, demande un module à part entière, en extern, donc potentiellement adieu l'inlining automatique et l'optimisation au delà de la frontière de l'appel de la fonction... Et optimiser à mort comme je le fais ne veut pas dire forcément se passer de classes et/ou de templates, c'est juste une manière particulière de concevoir ce genre de structures.
C'est aussi pour ce genre de raisons qu'écrire du C compatible C++ (sous-ensemble commun, avec notamment de beaux casts de malloc) est intéressant. Exemple typique : protocole de communication avec d'un côté un µC n'acceptant que du C, et de l'autre un truc plus bourrin programmé en C++. Un seul fichier source, pas de risques de régressions, et t'es certain d'avoir les mêmes constantes/contraintes/limitations des deux côtés de la barrière.
Je sais : l'optimisation bas niveau, c'est parfois un peu space... Mais c'est ça qui est marrant, justement !
Il est vrai qu'un code proche de
est vachement implementation dépendant, est vachement lourd à écrire, tout à fait implémentation dépendant et difficilement maintenable
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10 /* ici, je ne regarde que les fonctions qui ne changent pas */ class MonTableau : private std::vector<int> { public: typedef typename std::vector<int>::iterator iterator; typedef typenamle std::vector<int>::const_iterator; using std::vector<int>::operator[]; using std::vector<int>::push_back; using std::vector<int>::clear; };
Ma vision est à l'opposé de la tienne.
A laisser le premier venu réinventer la roue (alors qu'elle était standard), il va y avoir un nouveau code qu'il va falloir tester et maintenir.
Ce que j'ai régulièrement constaté :
- une écriture catastrophique avec des perfs pires que celles de la STL
- 0-error safety (dans error, j'englobe exceptions et autres codes de retour)
Le pire scénario, cette super alternative que l'on se passe en interne, on la copie-colle entre projets. Chaque projet assure sa propre maintenance -- bien ouais, coûts séparés, maintenance séparée ; sans parler d'éventuels risques de régression à introduire les corrections des autres.
Je suis des plus prudent et sceptique à chaque roue pourtant standard que je vois réinventée.
Maintenant, je ne fais pas d'embarqué même si cela reste du "temps réel", et j'utilise principalement les vecteurs, parfois mêmes triés en place de maps.
Et t'as de fortes chances d'avoir dedans des instructions inutiles malgré tout...
La dépendance à l'implémentation de la STL se produit quand tu commences à créer des types particuliers (passés en paramètres à ton container STL) pour éliminer, justement, ces appels "inutiles", via l'optimisation du compilateur sur le code mort/constant.
Comprends bien que je parle d'optimisations de code critique, où tu peux réellement dire que chaque instruction inutile est une plaie... Par exemple, la tâche RT principale, fonctionnant sous IT assez souvent, et qui paralyse le reste du système tant qu'elle est active. Des parties de code où l'idée même de coller un mutex mérite la crucifixion car bien trop coûteux en temps CPU.
La STL n'est pas parfaite, c'est tout : elle fournit d'excellents algos, adaptés à énormément de situations, mais pas à toutes les situations. C'est le seul et unique message que je cherche à faire passer, et forcément, je donne un exemple dans MON domaine pour ça.
D'autres t'auraient donné l'exemple de collections monstrueuses, de plusieurs millions d'éléments, pour montrer les limites de la STL en passant de l'autre côté du miroir : la limite sur la cardinalité des containers, plutôt que la limite sur leurs performances que je montre. Certes, peu de gens ont besoin de performances aussi lourdes, ou de collections aussi énormes.
Mais ces développeurs existent malgré tout, et ont pris l'habitude de passer outre la STL de façon générale dès qu'ils arrivent dans un cas où, par expérience, ils savent bien qu'ils feront mieux "à la main" (ou avec le container "maison").
Oui, c'est un des points d'optimisation dont je parlais un peu plus haut. Sauf que tu as une protection contre les erreurs, simplement elle est faite largement en amont et/ou par conception, en dimensionnant avec des marges de sécurité suffisantes les buffers par exemple. Cela demande d'ailleurs un boulot assez sévère parfois pour justifier le choix d'une taille considérée comme "sûre".
Il y a temps réel et temps réel... Quand le code à optimiser est appelé plusieurs milliers de fois à chaque milliseconde, et qu'il n'est pas question d'upgrader le matos de quelque façon que ce soit, il est évident que tu réfléchis beaucoup plus aux pertes "idiotes" de temps CPU.
De façon générale, je suis d'accord avec toi. Mais comme pour toute règle, il y a des exceptions, et le "tout-STL" limite intégriste n'est pas systématiquement une bonne idée, en fonction de ce que tu fais comme code bien sûr.
Ni plus, ni moins.
J'ai l'impression qu'on a un peu de mal à se comprendre.
Je ne remets pas en cause la nécessité de faire du code spécifique. Par contre, j'estime qu'il y a une réelle plus value à le faire en restant dans l'interface de la STL, via spécialisation de templates, car :
- cela garde un code auquel le développeur est habitué
- cela peut être fait après coup (et donc, après mesure, car il est difficile de déterminer à l'avance où seront les goulets d'étranglement), avec quasiment zéro coût de migration (aucun code à changer).
Je me trompes peut-être, ou ce n'est pas assez clair, mais c'est l'impression générale qui découlait de tes propos (et de ceux de koala01 et Luc Hermitte) : l'impression que "STL=panacée"... Après, je le martèle encore une fois : vous avez raison de façon générale, SAUF quand on va vers les limites de la machine (taille des données ou performances).
Et c'est justement ce que je reproche : j'ai souvent vu le cas avec des débutants dans le code critique (et, hélas, pas forcément des débutants en développement) qui, "parce que ça y ressemble", te collent une std::map<std::string,std::vector> (ou autre horreur du même genre) en plein milieu d'un goulet d'étranglement...
L'avantage d'utiliser un container non standard, c'est que cela oblige le développeur qui reprends le code à réfléchir avant de faire n'importe quoi. Même si, au final, il a exactement les mêmes fonctions que dans le container STL, simplement nommées différemment et/ou avec certaines défections (méthodes ou paramètres).
C'est hélas un vœu pieux : d'une part, par expérience, il y a des endroits / types de code où l'on sait par avance qu'un container générique sera moins efficace.
D'autre part, les mesures, tests et autres analyses dynamiques dans ce genre de code sont souvent pénibles à faire, et prennent pas mal de temps. Comme dans 99% des cas, on connait le résultat à l'avance, autant passer directement sur la solution "correcte" (d'un point de vue performances) plutôt que d'essayer deux méthodes et, au final, perdre du temps.
De plus, ce genre de code est en général tellement "en dur" que les codes du genre "zéro modifications" sont souvent illusoires, même si je suis le premier à le déplorer. Je sais, ça parait toujours étrange et bizarre à ceux qui n'ont jamais mis les mains dans ce genre de code, mais c'est une réalité pourtant.
On peut faire des templates maison pour améliorer un peu ce phénomène, mais ce n'est jamais du code "bisounours" où tout le monde est beau et modulaire...
Une bonne règle est celle de Michael A. Jackson : "1. do not optimize. 2. do not optimize (yet), for guru/expert only". (de mémoire)
Pour l'error safety, offrir la garantie forte n'est souvent qu'une question d'ordre des instructions plus un test et une affectation, et elle va intervenir sur les ajouts -- relativement à la partie qui concerne un conteneur. Je n'estime pas qu'il s'agisse d'un prix exorbitant à payer -- face au risque de quelqu'un qui implémente à une croissance à coups de new[] avec une taille incrémentée de 1.
Se passer du test ? Hum ... Rien de tel pour avoir des comportement non déterminés ou des plantages. Reste la solution d'interdire les allocations. ~~> Pas de croissance possible ? Facile. On s'expose pas le push_back sur un héritage privé.
Reste la zéro-initialisation, qui fait en fait souvent parti de règles qualité de toutes façons. J'avais croisé une solution itérable à cette pessimisation.
Pour les mutex, mon problème n'est pas tant dans leurs performances, que dans le fait que personne n'est correctement formé pour s'en servir sans introduire de deadlock -- les sémaphores que je n'utilise jamais, j'avais vu ; les hiérarchies de mutex, les tâches, les compteurs atomiques, ..., pratiquement que dalle. (A ce sujet, Stroutrup a sorti des articles au sujet de conteneurs lock-free il y a peu).
PS: ma réaction vient de "mon code à moi coute moins cher à écrire et maintenir" -- je simplifie.
Oui, c'est même une règle générale : si on fait du code maison, conserver autant que possible interfaces et conventions standard. Il me semble que la STL aide beaucoup, dans ce domaine. En fait, il n'est pas facile, une fois qu'on en a pris l'habitude, d'écrire du code "incompatible".
C'est là où je ne suis pas d'accord. Les cas où l'optimisations est nécessaire, ils sont peu nombreux et on doit les connaitre à l'avance. Savoir où sont les quelques appels cruciaux, les volumes de données impliqués, et les algorithmes qu'il faut appliquer, c'est un problème de conception, ca se traite avant le dev. Après, c'est trop tard, et c'est là que l'optimisation mène à des horreurs.
Francois
Pour moi, tu mélanges deux choses. L'optimisation des algorithmes, qui est effectivement faite en face de conception, et l'optimisation de l'implémentation, qui elle se fait pendant/après l'implémentation.C'est là où je ne suis pas d'accord. Les cas où les optimisations sont nécessaires, ils sont peu nombreux et on doit les connaitres à l'avance. Savoir où sont les quelques appels cruciaux, les volumes de données impliqués, et les algorithmes qu'il faut appliquer, c'est un problème de conception, ca se traite avant le dev. Après, c'est trop tard, et c'est là que l'optimisation mène à des horreurs.
Si tu en es à réfléchir à implémenter un vecteur maison pour ton type de données pour supprimer deux trois tests qui traînent dans la STL, c'est clairement de l'optimisation d'implémentation.
Ce qui commence mal : une allocation dynamique dans un code critique ????
Hum hum....
Je ne sais pas pour toi, mais pour ma part, j'ai eu des cours d'archi parallèle en fac, où l'on a été bassinés en long, en large et en travers sur tous les éléments de synchronisation usuels, sur les cas vicieux de deadlocks, etc. Ayant justement très peu de deadlocks dans mes programmes grâce à ça (99% du temps un simple oubli, visible en dix secondes de debug maximum), je peux difficilement laisser dire que "personne n'est formé".
Quant aux conteneurs lock-free... J'utilise des techniques lock-free sur ce genre de code depuis des années, apprises "sur le tas" et/ou conçues exprès pour virer, justement, des mutex beaucoup trop gourmands en temps CPU. Pour moi, ce n'est pas quelque chose de "récent", c'est quasiment la règle de base.
C'est juste pour que tu comprennes bien que c'est un domaine tout à fait spécifique et particulier, justement parce que situé aux limites normales du code, et donc forcément en dehors de la normale par plusieurs aspects.
De la même manière qu'un moteur de BD ne fonctionne pas avec des "std::map" pour chercher dans les tables, d'ailleurs...
Je comprends ta réaction. Mais c'est pourtant le cas, en conservant le facteur primaire de performances avant tout le reste.
Pas d'alloc dynamique ? (comment ça j'attendais que tu le confirmes ? ) Il ne reste que la 0-initialisation qui peut couter plus cher que nécessaire sur un vecteur.
Les hiérarchies de mutex sont le concept essentiel que je n'ai découvert que récemment. Je ne sais pas si c'est parce que je dormais durant ce cours ou si cela avait été occulté, mais j'avais retenu plus de choses sur les outils de synchronisation que je n'utilise jamais que sur ces hiérarchies de mutex.
Qu'est-ce qui me fait dire que je n'étais pas le seul ? Des codes que j'ai croisé et qui abusent de mutex -- car abus d'états partagés. Avec une réflexion dès la conception pour voir qu'il n'y avait un gros sac de noeud en place d'une hiérarchie, les choses auraient été différentes.
Pour les lock-free, j'ai l'impression que c'est récent comme approche (du moins, c'est moins confidentiel qu'avant), mais les implémentations qui marchent me semblent brevetées...
Vous avez un bloqueur de publicités installé.
Le Club Developpez.com n'affiche que des publicités IT, discrètes et non intrusives.
Afin que nous puissions continuer à vous fournir gratuitement du contenu de qualité, merci de nous soutenir en désactivant votre bloqueur de publicités sur Developpez.com.
Partager