Comment documenter et enseigner aux autres du code «optimisé au-delà de la reconnaissance» à forte intensité de calcul?

11

Parfois, il y a 1% de code suffisamment intensif en calcul qui nécessite le type d'optimisation de bas niveau le plus lourd. Les exemples sont le traitement vidéo, le traitement d'image et toutes sortes de traitement du signal, en général.

Les objectifs sont de documenter et d'enseigner les techniques d'optimisation, afin que le code ne devienne pas impossible à maintenir et sujette à suppression par les nouveaux développeurs. (*)

(*) Nonobstant la possibilité que l'optimisation particulière soit complètement inutile dans certains futurs CPU imprévisibles, de sorte que le code sera supprimé de toute façon.

Étant donné que les offres logicielles (commerciales ou open source) conservent leur avantage concurrentiel en ayant le code le plus rapide et en utilisant la toute dernière architecture CPU, les rédacteurs de logiciels ont souvent besoin de modifier leur code pour le faire fonctionner plus rapidement tout en obtenant la même sortie pendant un certain temps. tâche, liste tolérant une petite quantité d’erreurs d’arrondi.

En règle générale, un rédacteur de logiciels peut conserver de nombreuses versions d'une fonction en tant que documentation de chaque réécriture d'optimisation / algorithme qui a lieu. Comment mettre ces versions à disposition des autres pour étudier leurs techniques d'optimisation?

En relation:

rwong
la source
1
Vous pouvez simplement conserver les différentes versions dans le code, commentées, avec de nombreux commentaires indiquant au lecteur ce qui se passe.
Mike Dunlavey
1
Et ne leur dites pas seulement ce que fait le code, mais pourquoi il est plus rapide de cette façon. Incluez des liens vers des algorithmes si nécessaire, soit les vôtres, ceux de type wiki, des documents ou des ressources disponibles sur Internet (il suffit de faire attention à la pourriture des liens dans ce cas, il peut être judicieux de la copier dans votre propre système de documentation avec un lien vers l'original). .)
Marjan Venema
1
@MikeDunlavey: Aïe, veuillez ne pas le commenter. Ayez simplement plusieurs implémentations de la même fonction et appelez celle qui est la plus rapide. De cette façon, vous pouvez facilement passer à une autre version du code et les comparer tous.
sleske
2
@sleske Parfois, le simple fait d'avoir plus de code binaire peut le ralentir.
quant_dev
@quant_dev: Oui, cela peut arriver. Je pense simplement qu'il est important que le code soit construit et exécuté (idéalement) régulièrement, pour le garder à jour. Peut-être le construire uniquement en mode débogage.
sleske

Réponses:

10

Réponse courte

Gardez les optimisations locales, rendez-les évidentes, documentez-les bien et simplifiez la comparaison des versions optimisées entre elles et avec la version non optimisée, à la fois en termes de code source et de performances d'exécution.

Réponse complète

Si de telles optimisations sont vraiment importantes pour votre produit, vous devez non seulement savoir pourquoi les optimisations étaient utiles auparavant, mais également fournir suffisamment d'informations pour aider les développeurs à savoir si elles seront utiles à l'avenir.

Idéalement, vous devez inscrire les tests de performances dans votre processus de génération afin de savoir quand les nouvelles technologies invalident les anciennes optimisations.

Rappelles toi:

La première règle d'optimisation de programme: ne le faites pas.

La deuxième règle de l'optimisation des programmes (pour les experts seulement!): Ne le faites pas encore. "

- Michael A. Jackson

Afin de savoir si le moment est venu, il faut des analyses comparatives et des tests.

Comme vous le mentionnez, le plus gros problème avec le code hautement optimisé est qu'il est difficile à maintenir, donc, dans la mesure du possible, vous devez garder les portions optimisées séparées des portions non optimisées. Que vous le fassiez via la liaison au moment de la compilation, les appels de fonctions virtuelles d'exécution ou quelque chose entre les deux, cela n'a pas d'importance. Ce qui devrait être important, c'est que lorsque vous exécutez vos tests, vous souhaitez pouvoir tester toutes les versions qui vous intéressent actuellement.

Je serais enclin à construire un système de telle manière que la version de base non optimisée du code de production puisse toujours être utilisée pour comprendre l' intention du code, puis à construire différents modules optimisés à côté de celui-ci contenant la ou les versions optimisées, documentant explicitement partout la version optimisée diffère de la ligne de base. Lorsque vous exécutez vos tests (unitaires et d'intégration), vous les exécutez sur la version non optimisée et sur tous les modules optimisés actuels.

Exemple

Par exemple, supposons que vous ayez une fonction de transformation de Fourier rapide . Peut-être que vous avez une implémentation algorithmique de base fft.cet des tests fft_tests.c.

Vient ensuite le Pentium et vous décidez d'implémenter une version à virgule fixe en fft_mmx.cutilisant les instructions MMX . Plus tard, le pentium 3 arrive et vous décidez d'ajouter une version qui utilise les extensions Streaming SIMD dans fft_sse.c.

Maintenant, vous voulez ajouter CUDA , donc vous ajoutez fft_cuda.c, mais trouvez qu'avec le jeu de données de test que vous utilisez depuis des années, la version CUDA est plus lente que la version SSE! Vous faites une analyse et finissez par ajouter un jeu de données 100 fois plus grand et vous obtenez la vitesse que vous attendez, mais maintenant vous savez que le temps de configuration pour utiliser la version CUDA est important et qu'avec de petits jeux de données, vous devriez utiliser un algorithme sans ce coût d'installation.

Dans chacun de ces cas, vous implémentez le même algorithme, tous doivent se comporter de la même manière, mais s'exécuteront avec des efficacités et des vitesses différentes sur différentes architectures (si elles s'exécutent). Du point de vue du code cependant, vous pouvez comparer n'importe quelle paire de fichiers source pour savoir pourquoi la même interface est implémentée de différentes manières et généralement, le plus simple sera de se référer à la version originale non optimisée.

Il en va de même pour une implémentation OOP où une classe de base qui implémente l'algorithme non optimisé et des classes dérivées implémentent différentes optimisations.

L'important est de garder les mêmes choses qui sont les mêmes , pour que les différences soient évidentes .

Mark Booth
la source
7

Plus précisément, puisque vous avez pris l'exemple du traitement vidéo et image, on peut conserver le code dans la même version mais actif ou inactif selon le contexte.

Bien que vous ne l'ayez pas mentionné, je suppose Cici.

La manière la plus simple dans le Ccode, on fait l'optimisation (et s'applique également lorsque l'on essaie de rendre les choses portables) est de garder

 
#ifdef OPTIMIZATION_XYZ_ENABLE 
   // your optimzied code here... 
#else  
   // your basic code here...

Lorsque vous activez #define OPTIMIZATION_XYZ_ENABLEpendant la compilation dans Makefile, tout fonctionne en conséquence.

Habituellement, couper quelques lignes de code au milieu des fonctions peut devenir compliqué quand il y a trop de fonctions optimisées. Par conséquent, dans ce cas, on définit différents pointeurs de fonction pour effectuer une fonction spécifique.

le code principal s'exécute toujours via un pointeur de fonction comme


   codec->computed_idct(blocks); 

Mais les pointeurs de fonction sont définis en fonction du type d'exemple (par exemple ici la fonction idct est optimisée pour différentes architectures CPU.



if(OPTIMIZE_X86) {
  codec->computed_idct = compute_idct_x86; 
}
else if(OPTIMZE_ARM) {
  codec->computed_idct = compute_idct_ARM;
}
else {
  codec->computed_idct = compute_idct_C; 
}

vous devriez voir le code libjpeg et le code libmpeg2 et vous pouvez être ffmpeg pour de telles techniques.

Dipan Mehta
la source
6

En tant que chercheur, je finis par écrire un peu du code "goulot d'étranglement". Cependant, une fois mis en production, il incombe aux développeurs de l'intégrer dans le produit et de fournir un support ultérieur. Comme vous pouvez l'imaginer, il est de la plus haute importance de communiquer clairement quoi et comment le programme est censé fonctionner.

J'ai trouvé qu'il y a trois ingrédients essentiels pour réussir cette étape

  1. L'algorithme utilisé doit être absolument clair.
  2. Le but de chaque ligne de mise en œuvre doit être clair.
  3. Les écarts par rapport aux résultats attendus doivent être identifiés dès que possible.

Pour la première étape, j'écris toujours un petit livre blanc qui documente l'algorithme. Le but ici est de le rédiger afin qu'une autre personne puisse l'implémenter à partir de zéro en utilisant uniquement le livre blanc. S'il s'agit d'un algorithme bien connu et publié, il suffit de donner les références et de répéter les équations clés. S'il s'agit d'un travail original, vous devrez être un peu plus explicite. Cela vous dira ce que le code est censé faire .

L'implémentation réelle qui est transférée au développement doit être documentée de manière à ce que toutes les subtilités soient rendues explicites. Si vous acquérez des verrous dans un ordre particulier pour éviter un blocage, ajoutez un commentaire. Si vous parcourez les colonnes plutôt que les lignes d'une matrice en raison de problèmes de cohérence du cache, ajoutez un commentaire. Si vous faites quelque chose, même légèrement intelligent, commentez-le. Si vous pouvez garantir le livre blanc et que le code ne sera jamais séparé (via VCS ou un système similaire), vous pouvez vous référer au livre blanc. Le résultat peut facilement être supérieur à 50% de commentaires. Ça va. Cela vous dira pourquoi le code fait ce qu'il fait.

Enfin, vous devez être en mesure de garantir l'exactitude face aux changements. Heureusement, nous sommes un outil pratique dans les plates-formes de tests automatisés et d'intégration continue . Ceux-ci vous diront ce que le code fait réellement .

Ma recommandation la plus chaleureuse serait de ne lésiner sur aucune des étapes. Vous en aurez besoin plus tard;)

drxzcl
la source
Merci pour votre réponse complète. Je suis d'accord avec tous vos points. En termes de tests automatisés, je trouve qu'il est difficile de couvrir correctement la plage numérique de code arithmétique à virgule fixe et SIMD, quelque chose que j'ai brûlé deux fois. Les conditions précisées dans les commentaires uniquement (sans code à renforcer) n'étaient pas toujours remplies.
rwong
Si je n'ai pas encore accepté votre réponse, c'est parce que j'ai besoin de plus de conseils sur ce que signifie "un petit livre blanc" et sur les efforts qui devraient être déployés pour le produire. Pour certaines industries, cela fait partie de l'activité principale, mais dans d'autres industries, le coût doit être pris en compte et des raccourcis légalement disponibles auraient dû être pris.
rwong
Tout d'abord, je ressens votre douleur concernant les tests automatisés, l'arithmétique à virgule flottante et le code parallèle. J'ai bien peur qu'il n'y ait pas de solution valable pour tous les cas. Habituellement, je travaille avec des tolérances assez libérales, mais dans votre secteur, cela pourrait ne pas être possible.
drxzcl
2
Dans la pratique, le livre blanc ressemble souvent à la première ébauche d'un document scientifique, sans les parties "fluff" (pas d'introduction significative, pas de résumé / conclusions / discussion minimales et uniquement les références nécessaires pour le comprendre). Je vois la rédaction de l'article comme un rapport et une partie intégrante du développement et / ou de la sélection d'algorithmes. Vous avez choisi d'implémenter cet algorithme (disons FFT spectrale). C'est quoi exactement? Pourquoi avez-vous choisi celui-ci parmi les autres? Quelles sont ses caractéristiques de parallélisation? L'effort doit être proportionnel au travail de sélection / développement.
drxzcl
5

Je crois que cela peut être mieux résolu par des commentaires complets du code, au point que chaque bloc de code significatif a des commentaires explicatifs au préalable.

Les commentaires doivent inclure des citations des spécifications ou du matériel de référence matériel.

Utilisez la terminologie et les noms d'algorithme à l'échelle de l'industrie, le cas échéant - par exemple, «l'architecture X génère des interruptions CPU pour les lectures non alignées, de sorte que ce périphérique de Duff se remplisse jusqu'à la limite d'alignement suivante».

J'utiliserais un nom de variable en face à face pour ne pas mal comprendre ce qui se passe. Pas hongrois, mais des choses comme «foulée» pour décrire la distance en octets entre deux pixels verticaux.

Je voudrais également compléter cela avec un court document lisible par l'homme qui a des diagrammes de haut niveau et une conception de bloc.

JBRWilkinson
la source
1
L'utilisation d'une seule terminologie cohérente pour une seule chose (par exemple, en utilisant «foulée» sur des termes de significations similaires, par exemple «étape», «alignement») dans le même projet serait utile. Cela est quelque peu difficile lors de l'intégration de la base de code de plusieurs projets dans un seul projet.
rwong