DivideConcept.net

le blog de divide.

Archive pour janvier 2015

10 ans

Mercredi 28 janvier 2015

Après Ouamdu, Channie, MathieuFROG et d’autres, à mon tour de faire l’article des 10 ans…

Wefrag a été une formidable plateforme de communication et comme quelques autres ici, elle m’a aussi indirectement apporté mon premier boulot. J’y ai aussi trouvé une communauté avec laquelle les débats sont très souvent enrichissants.

Quand j’ai écris le premier des 140 articles de ce blog j’avais 20 ans, encore au lycée (après un double-redoublement…), je vivais chez mes parents et je n’avais encore jamais travaillé plus de quelques semaines par-ci par là. Quand je regarde en arrière, ce que je retiens c’est à quel point ces 10 années m’ont forgé une certaine vision de la vie. Beaucoup de convictions que je croyais acquises ont été remises en question pendant ce parcours personnel, et dans cette quête du dénominateur commun petit à petit certaines règles de vie, certains adages, maximes et autres citations se sont imposés et m’ont forgé une certaine philosophie.

“Do right and fear no man” (”Fais le bien et ne crains personne”), sorte de “pari de Pascal” sur la vie.
Agir toujours selon le sens moral (en tout cas selon ce qui nous semble le sens moral) amène la sérénité et permet d’assumer tous ses actes, l’inverse est source d’angoisse permanente. C’est la version pragmatique du Karma.

“If”, poème de Rudyard Kipling.
Dans ce poème Kipling décrit toute une série d’épreuves qu’il faudrait selon lui surmonter sans sourciller. À défaut d’être tenable, cette liste est un cap qui permet de surmonter bien des épreuves dans la vie.

“Whatever works”, film de Woody Allen.
Ce film parle des couples improbables, mais “tant que ça marche”, tout le monde y trouve son compte et il n’y a pas lieu de juger. Cette leçon s’étend à tout choix de vie.

“Ne pas prévoir c’est déjà gémir”, Léonard de Vinci.
Indissociablement lié à la Loi de Murphy (également appelée loi de l’emmerdement maximum) et au proverbe “Il ne faut pas vendre la peau de l’ours avant de l’avoir tué”. Bien souvent la vie nous montre que tout ce qui peut potentiellement déraper finira par déraper. Ne jamais négliger le moindre risque dans ses estimations. Le livre Jurassic Park illustre à merveille ce phénomène sous l’angle de la théorie du chaos.

“Patience est mère de toutes les vertus” la preuve, c’est hier qu’était censé sortir GTA V PC.
Repousser d’une heure, d’un jour ou d’une semaine une réponse peut éviter bien des mots et décisions malheureuses à chaud. De la même manière, prendre son mal en patience et se taire quand le jeu en vaut la chandelle.

Rendez-vous dans 10 ans.

Carnet de notes de développement

Mardi 13 janvier 2015

En m’attaquant au développement de la 3eme version de SpectraLayers il y a pile-poil un an, j’ai décidé de tout reprendre à zéro, afin de repartir sur de meilleures bases tant en terme d’architecture que d’expérience utilisateur. SpectraLayers était ma première grosse application et j’ai improvisé en cours de route, ignorant certaines conventions et techniques pourtant courantes. Jusque là mes projets se limitaient pour la plupart à de la R&D d’algorithmes.
Je vais tacher d’énumérer ici tout ce que j’ai pu noter lors du développement de cette nouvelle version.

UX Design

Un des points faibles des deux premières versions était la courbe d’apprentissage bien plus longue que sur les autres soft d’édition. Les concepts de transfert de données direct d’un calque à l’autre et d’inversion de phase étaient loin d’être intuitifs, l’association de couleurs par rapport à des états un peu déroutant, la navigation perfectible, et la première version consommait beaucoup coté CPU (point quelque peu amélioré dans la version 2).

Tous ces éléments ont donc été remis complètement à plat, et l’idée maîtresse pour rendre le tout intuitif a été de s’inspirer le plus directement possible de tous les termes, apparences, actions, comportements et mises en page standards de l’ensemble des logiciels d’édition existants (aussi bien des éditeurs de texte, que d’image, de montage vidéo et audio) pour en tirer ce qui en faisait l’essence même. Puis penser l’intégration de tous ces concepts autour de l’édition spectrale multi-couches. Tous ces éléments constituent l’UX, l’eXpérience Utilisateur, concept qui va au delà de celui d’UI (Interface Utilisateur), même si les deux sont étroitement liés.

Quelques exemples de questions que je me suis posé:
-Quel est le raccourcis clavier standard sur chaque plateforme pour telle action ?
-Quels sont les outils/méthodes en vogue pour naviguer au sein d’un projet ?
-Que se passe t-il généralement lorsqu’on sélectionne quelque chose, et qu’on cherche à le déplacer directement ?
-Quelle est la dénomination la plus courante pour un outil qui fait ce genre de travail ? Et sa représentation ?
-Dans quel ordre ce type de processus est t-il généralement appliqué ?
-Quels éléments de l’interface sont superflus, et lesquels devraient être accessibles en priorité ?

sl3banner

Certaines questions se sont posés en amont, et le reste en cours de développement, au cas par cas, une fois confronté aux dilemmes. Il existe néanmoins quelques exceptions à cette règle, quand on pense apporter un réponse bien plus intuitive ou pratique que la pratique la plus répandue; mais dans ce cas, il faut s’assurer que cela semblera couler de source pour l’utilisateur.
Si il ne fallait retenir qu’une chose sur ce point, c’est que tout ce qui n’est pas l’innovation à proprement parler du projet doit absolument se conformer aux us et coutumes en rigueur dans son domaine. Dans le cas contraire il en résulte généralement un apprentissage plus long, avec le risque d’aboutir sur une incompréhension voire un rejet pur et simple par l’utilisateur.

Le choix des outils

Vient ensuite le choix des outils pour commencer la réalisation à proprement parler. Ce choix est extrêmement important, car il va conditionner la bonne conduite du projet jusqu’à son terme, et assurer sa pérennité ensuite. Et il n’est pas question d’en changer en cours de route (au risque de se retrouver avec les symptômes d’un Duke Nukem Forever). Il n’y a pas de réponse universelle : il faut faire en fonction de ses connaissances, de sa capacité d’adaptation, du type de projet, des plateformes ciblées, de la qualité de l’outil en lui même (ses fonctionnalités, sa stabilité, son suivis, sa documentation)… Bref prendre quelques jours pour bien étudier l’offre disponible, télécharger quelques outils pour voir comment ils se présentent sur le terrain, tout ça se fait aussi un peu au feeling.

Pour mon projet Qt s’est montré d’une richesse incroyable, très bien fournis en exemples et documentation, suivis par une grosse communauté et évolutif.
Sa dernière version (5.4) intègre le support High-DPI, Windows RT, et de nombreuses autres améliorations appréciables.
Coté compilateur je m’appuie sur VS2013 Express pour Windows, et XCode 5 sur Mac OS. L’ensemble est totalement gratuit d’utilisation, même pour des projets commerciaux.

Git, c’est fantastique

Believe it or not, mais jusque là tous mes projets étaient versionnés… avec des archives zip quotidiennes de la totalité du projet. J’étais conscient qu’il s’agissait d’une mauvaise pratique, mais c’était simple et j’avais un peu peur que la plupart des systèmes de versionning soient des usines à gaz.
Quelques jours de test avec Git ont suffit à me convaincre du contraire (merci les wefragés), la mise en place est très simple, ça peut s’utiliser offline et/ou online au choix, et un ou deux scripts simples peuvent en simplifier l’utilisation quotidienne encore plus qu’un archivage zip. Le gain de place est monstrueux par rapport à mon ancien système, même en archivant les binaires du projet (Git gère très bien les redondances au sein des binaires via sa compression interne, contrairement à une croyance répandue). Couplé à Github (pour les projets open source) ou Bitbucket (pour les projets fermés) c’est l’assurance de ne jamais perdre son projet.

Architecture : ce qui se conçoit bien s’énonce clairement…

Afin d’établir des fondations solides pour ce “nouveau” logiciel, je me suis appuyé sur l’excellent yEd, un éditeur de schémas très simple à prendre en main (et gratuit). Merci Darkstryder !
En quelques clics on peut rapidement définir des modules, les relier entre eux, modifier tout ça aisément en cours de réflexion et même demander au logiciel de tout réorganiser automatiquement au mieux.

yed

…Mais les mots pour le dire ne viennent qu’avec le temps.

Déterminer la structure prend du temps, mais ce qui prend certainement autant de temps est de trouver le nom juste pour chaque module. Décrire en un seul, maximum deux mots tout ce que chaque objet est censé faire, sans employer d’expression tarabiscotée est tout un art qui ne se plie pas à l’urgence. Autant tirer des traits entre des modules dont on connait le rôle peut se faire (relativement) rapidement, autant trouver leur description exacte et leur véritable lien ne se fait qu’avec le temps de la réflexion au fil des jours, voire des semaines. Mais quoi qu’il en soit, je recommanderai ici de garder les choses aussi simples que possible, partir dans des diagrammes UML complexes tel qu’enseigné en école d’ingénieur me semble complètement inutile, et au final une grosse perte de temps. La vraie réflexion n’arrive que lorsqu’on commence à coder et qu’on se rend alors compte de certaines réalités et de la véritable nature de chaque objet.

Pour mieux organiser l’ensemble, j’ai catégorisé chacun de mes modules selon l’une de ces cinq catégories, que je pense universelles:

-Les “primitives”: classes/structures très simples, ne contenant qu’une donnée unique (matrice, vecteur, zone rectangulaire…) et quelques opérations simples pour agir dessus et les combiner. Un header suffit à les définir intégralement.
-Les “entités”: classes contenant des données plus complexes (une piste audio par exemple, avec ses caractéristiques, ou des buffers de calcul complexes), et dont le nombre est variable pendant une session.
-Les “applicatives”: classes qui n’ont de sens qu’au sein de ce logiciel spécifiquement, et dont le nombre est généralement fixe au cours d’une session. Elles organisent tout le fonctionnement du programme et de l’interface en se servant des autres types de classes.
-Les “interfaces”: des classes dont l’unique but est d’être un intermédiaire de communication, entre classe d’un même programme, ou entre un programme et ses plugins par exemple. On dérive généralement de ces classes dans plusieurs classes entités ou applicatives.
-Les “utilitaires”: prend le plus souvent la forme de namespace que de classes, ne contiennent aucune données, ils ont un rôle purement moteur: ça peut être des opérations mathématiques, des routines qui prennent une info en entrée et en donne une autre en sortie, mais qui dans tous les cas n’ont aucun intérêt à stocker quelque état que ce soit.

Communication interne

Une fois la structure en place, il va falloir l’irriguer; c’est à dire faire communiquer les différentes classes entre-elles, pas n’importe comment ni dans n’importe quel ordre !
Il y a plusieurs écoles à ce niveau, pour ma part j’ai opté pour le système suivant, conseillé par skaven, qui m’a semblé à la fois élégant, robuste et sécurisé :

Pour l’irrigation générale de l’application, j’ai une classe d’interface IEvent qui se charge de passer tous les principaux évènements globaux (format de projet, liste des calques, mouvements de souris…) aux classes principales du projet (qui dérivent de IEvent). Libre à chaque classe de faire une copie locale de ces nouvelles informations, le tout se propage progressivement dans toute l’application, mais chaque objet s’auto-gère de manière indépendante et renvoit d’autres évènements si besoin (la classe IEvent gérant un système de queue). Avec ce système, je peux débrancher n’importe quel objet et l’application continuera de fonctionner (de manière incomplète bien sur). Pas de synchronisation à gérer.

Qt propose également un système Signal/Slot qui permet de connecter directement à la volée différents objets entre eux. J’ai réservé cet usage à la connection des objets Qt au reste de mon application, pour communiquer entre threads de manière asynchrone, et pour que certains objets enfants puissent communiquer certains changement d’état à leur parent sans connaissance préalable de leur parent.

Enfin l’accès direct aux méthodes a été réservé pour la communication avec les entités (voire définition plus haut), cas d’accès le plus fréquent. Pas de mise en place, pas de perte de temps niveau exécution.

Factoriser le code…

Une fois le travail de structuration, de catégorisation et d’irrigation bien définie, il va falloir factoriser un maximum de code. Pour la clarté, la concision (on code plus vite et on maintient ce code plus facilement) et (en partie) pour l’optimisation. Mais encore une fois pas n’importe quoi, n’importe comment, ni n’importe où… Sous peine d’obtenir l’effet inverse. Il faut bien identifier les taches, les objets ou les bouts de code qui sont susceptibles de se répéter de nombreuses fois au moment de l’écriture de l’application, et dans ses futurs développements.
Quelques techniques C++ pour pousser la factorisation:

-L’héritage/Le polymorphisme/héritage multiple: grande force du C++, mais à utiliser avec précaution (il ne s’agit pas d’empiler les niveaux d’héritages au risque de rendre le code incompréhensible), son utilisation judicieuse peut créer d’élégantes structures aux possibilités décuplées. Savoir comment ou sur quoi l’utiliser sera à déterminer en amont en même temps que la structure générale du programme et de la classification des objets.

-Les macros: on y pense pas forcément, mais on peut factoriser et simplifier énormément de code répétitif via les macros. Certes, cela à l’effet pervers de cacher le code réel, mais ça peut s’avérer être de redoutables outils pour créer presque un nouveau language propre à l’application. Bien identifier les quelques blocs de codes qui vont se répéter énormément au cours de l’application, et bien trier entre ceux qui relèvent d’une nouvelle fonction et ceux qui relèvent de la macro.

Le framework Qt fait un large usage de ces deux concepts, et c’est en partie ce qui fait son élégance. Cette approche a permit de grandement réduire la complexité du code de SpectraLayers 3 vs SpectraLayers 2, et malgré le plus grand nombre de features le nombre de lignes de code reste similaire; il est répartie sur beaucoup plus d’objets spécifiques. Le nombre de commentaires a également été divisé par deux, ce que j’attribue au fait que le code est naturellement plus facile à suivre.

graph
text(statistiques SourceMonitor)

…Et factoriser les calculs

L’optimisation de la vitesse d’exécution d’un programme ne repose pas que sur la concision des algorithmes qui le composent; avec le recul je me suis aperçu que l’exploitation optimale des techniques et contraintes hardware et la vérification minutieuse de la vitesse d’exécution de chaque élément de la chaîne logicielle comptait tout autant.

Ainsi, il ne suffit pas de lancer aléatoirement plein de threads de calculs divers pour exploiter au mieux les cœurs d’un CPU. La meilleure technique (et le gain est réellement notable) est de subdiviser au maximum les calculs en sous-calculs indépendant, jusqu’à obtenir de petites unités qui prennent très peu de temps d’exécution (par rapport à l’ensemble de la tâche), lancer autant de threads “exécuteurs” que de cœurs sur le CPU, et assigner cette liste de micro-calculs à ces threads qui vont tourner en permanence. Je compare ces micro-calculs à des grains de sable qui vont remplir de manière optimale la capacité d’exécution de chaque cœur CPU. Il faut également savoir qu’arrêter et relancer un thread est coûteux, il vaut mieux donc avoir des threads dédiés prêt à prendre une tache dès que nécessaire.

Toujours dans l’idée d’exploiter au mieux les capacités des CPU, il ne faut pas négliger les instructions vectorielles, car depuis le lancement du Pentium MMX celles-ci n’ont pas arrêté d’évoluer en parallèle de la montée en puissance et en cœurs. Les instructions vectorielles sont comme autant de sous-cœurs par cœur, capables d’effectuer en parallèle et de manière synchronisés plusieurs opérations élémentaires.
Ainsi, si le MMX se limitait à 2 opérations sur des nombres entier (int32), le SSE a apporté 4 opérations sur des nombres flottants (float32), l’AVX double ça, et le future AVX-512 le redouble encore.
Du coup une application correctement optimisée pour les derniers CPUs est capable d’effectuer 8 coeurs x 8 composants par vecteur = 64 opérations en parallèle !
Même si le gain réel n’atteint pas 64, il reste néanmoins important par rapport à un algorithme non pensée multi-thread/vectoriel. Un facteur à ne pas négliger donc.

Pour en finir avec les CPU, un point sur le gain réel entre 32bits et 64bits : si il est vrai que certaines opérations sont légèrement plus optimisés en 64bits qu’en 32bits, car bâtis sur un jeu d’instruction plus moderne, le gain est néanmoins difficilement mesurable (toutes les opérations ne sont pas systématiquement plus rapides, et un calcul est un ensemble d’opérations très divers). En revanche, pouvoir s’affranchir de la barrière mémoire 32bit offre des possibilités d’architecture nouvelles et bien plus optimisés en terme de vitesse d’accès. Des opérations qui devaient se limiter à des buffers de tailles bien délimités sous peine de crash peuvent maintenant demander allègrement toute la place qu’elles désirent, et à charge de l’OS de répartir au mieux en fonction des capacités hardware réelles. Des concepts comme le mapping mémoire de fichiers (faire correspondre le contenu d’un fichier à un emplacement mémoire) prend alors tout son sens, puisque même les gros fichiers pourront trouver leur équivalent en RAM dans la plupart des cas, accélérant ainsi d’autant plus la vitesse de traitement des données.

Quand au GPU, si son utilisation semble évidente dans le contexte d’un jeu vidéo, c’est bien moins souvent le cas dans un cadre professionnel (en dehors des logiciels de 3D, et bien que cela se généralise petit à petit au sein des applications de montage et de traitement d’image). Ainsi dans le domaine de l’audio, l’utilisation des GPU est quasi-inexistante. Mais au vu de la particularité de mon application (éditeur spectral multi-couches tridimensionnel) il me semblait essentiel de l’intégrer dans les optimisations. Il a fallut alors composer en connaissance de cause du marché, peu fournis en GPU “gamer”, en ne posant qu’un pré-requis OpenGL 3 (ce que toutes les machines sont heureusement capables de fournir maintenant), et l’utiliser uniquement là où il s’avère le plus efficace, c’est à dire en tant qu’accélérateur graphique 2D/3D.

Enfin, last but not least, il est important de penser la chaîne d’exécution d’un calcul dans son ensemble, de ce qui l’a déclenché jusqu’à l’affichage du résultat final. On a tendance à se focaliser sur le calcul lui-même mais en réalité c’est l’ensemble de la chaîne qui compte, et en particulier son maillon le plus faible. Si un calcul est très rapidement exécuté mais que le composant qui envois les ordres ou qui affichera le résultat n’est pas prévu pour un rafraîchissement aussi rapide, c’est toute la chaîne qui est impacté !
Ainsi, j’ai déjà vu une simple boite de texte standard ralentir l’ensemble de mon programme. Pour les mêmes raisons, j’ai désactivé d’emblée le vertical refresh de mon application, et l’affichage de certains éléments ne sont que partiellement recalculés.

L’ensemble de ces optimisations a conduit à un boost perf de x5 entre SL2 et SL3:
bench

Une touche de modernisme

Les outils et le hardware évoluant constamment, il est important de continuer à s’adapter aux changements. Voici quelques points que j’ai pu intégrer en codant la nouvelle version:

-Auto-vectorization: les compilateurs de VS2013 et XCode 5 sont maintenant capables d’auto-vectoriser certains calculs (voir le point sur l’optimisation plus haut). Même si en pratique il vaut mieux vectoriser les calculs cruciaux soit-même en connaissance de cause, il est appréciable de pouvoir laisser la main au compilateur sur les tâches annexes.

-C++11: là aussi une nouveauté apportée par VS2013/XCode 5, quelques facilités sont très appréciables, en particulier les fonctions lambdas (aussi appelés fonctions anonymes), qui permettent d’optimiser grandement son code pour des micro-tâches temporaires. Elles se marient particulièrement bien avec le système de signal de Qt.
La possibilité d’utiliser des <> imbriqués et de déclarer des listes directement à l’initialisation sont aussi des détails qui m’ont simplifié le boulot et rendu le code plus élégant.

-Le support High-DPI et Touch: Il s’agit la d’évolutions hardwares quasi-simultanés, qui en plus du marché mobile trouvent écho aussi bien sur Mac (Rétina, TrackPad) que sur Windows (High-DPI, Touchscreen). Le support High-DPI devient de plus en plus indispensable, à minima pour s’assurer que le logiciel est bien compatible (c’est à dire que l’OS se charge bien d’upscaler correctement l’application, au risque de se retrouver avec des fenêtres et icônes minuscules, le programme considérent que le device pixel ratio est toujous de 1), au mieux pour en profiter au maximum avec des graphismes haute-résolution. Quand au support touch, il apporte un plus indéniable pour naviguer au sein d’un projet, le contrôle étant beaucoup plus intuitif et précis.

Quality Assurance

Étape finale du développement, indispensable dès que le logiciel fait une certaine taille ou doit être utilisé dans des environnements/situations critiques. Le récent scandale des sorties Ubisoft est clairement le résultat d’une défaillance à cette étape (sans doute plus du à la direction qui a décidé de passer outre qu’à l’incompétence des testeurs et ingénieurs, ceci dit).
Le QA ne se limite d’ailleurs pas à la recherche de bugs, il peut également apporter des suggestions sur des comportements qui peuvent sembler étrange ou peu intuitifs.
L’expérience m’a néanmoins montré que le seul département QA ne peut trouver tous les bugs, ils ont leurs techniques pour en débusquer (beaucoup), mais multiplier les sources de test externes permet aussi d’avoir autant de regards neufs avec autant de configurations exotiques. Je rajoute donc généralement quelques testeurs supplémentaires externes en plus de l’équipe de base.
Allouer environ un mois à cette étape pour avoir assez de recul semble indispensable, quelle que soit la qualité du code il survient toujours quelque chose à cette étape, et ce jusqu’à la dernière minute. Foutue loi de Murphy.
Quoi qu’il en soit, le travail abattus à ce moment là est autant de travail de support en moins une fois le logiciel sortie.

Release

La version 3 vient de sortir aujourd’hui, ceux qui ont connus les versions 1 et 2 pourront donc se faire une idée par eux-même. Je suis plutôt content du résultat final, et j’espère que le logiciel est maintenant plus abordable d’utilisation :)

SpectraLayers Pro 3