Sommaire
- Pour être en bonne santé, exercez-vous régulièrement
- Présentation de l'exercice
- Implémentation initiale
- Le jugement
- Des benchmarks et trop d'idées
- La période sombre
- Changement d'outils
- Quand et quoi optimiser
Bonjour 'nal,
Un petit exercice d'algorithmique m'a récemment poussé à regarder en détail l'impact de différentes approches sur les performances et à remettre en question des connaissances que je croyais solides. Laisse-moi te raconter ce voyage.
Pour être en bonne santé, exercez-vous régulièrement
J'aime bien pratiquer des exercices de programmation sur des sites tels que CodinGame ou CodeSignal. Si tu ne connais pas, ces sites proposent un petit IDE en ligne et divers problèmes d'algorithmiques avec jeux de tests associés. Le jeu consiste à fournir dans l'éditeur en ligne un programme pour résoudre le problème donné ; l'implémentation se faisant dans le langage de ton choix. La plate-forme permet de lancer la solution sur les jeux de tests pour la valider.
Selon son profil on utilisera le service dans l'optique d'écrire le programme le plus court, ou le plus efficace, ou simplement un programme qui fonctionne. Pour moi c'est un peu un coding dojo : un lieu ou pratiquer et où se comparer aux autres pour apprendre et s'améliorer.
En particulier, lors d'une séance sur CodeSignal je me suis laissé allé à me comparer à la meilleure solution d'après les votants : pour chaque exercice j'implémente une solution d'une manière assez immédiate, un peu comme ça me vient, puis quand la solution est validée je regarde celles des autres. J'essaye de trouver leurs points forts, les miens, et je nous critique. Comme je fais les exercices en C++ je ne me compare qu'à la meilleure solution dans ce langage.
Présentation de l'exercice
Étant donné une matrice d'entiers, l'exercice consiste à calculer la somme des nombres qui ne sont pas sous un zéro. Par exemple, pour
0, 2, 3
1, 0, 4
5, 6, 7
Le résultat attendu est 2 + 3 + 4 + 7 = 16. Le 1, le 5 et le 6 ne sont pas comptés car il y a un zéro plus haut dans la colonne.
La matrice est représentée en row-major, c’est-à-dire par une table de tables où chaque sous table représente une ligne de la matrice. J'ai déjà rencontré des traitements de grandes matrices par le passé et ma première réflexion est « si je traite le problème par colonne comme le suggère l'énoncé je vais sauter d'une zone mémoire à une autre et avoir des perfs nases. Il vaut mieux traiter ce genre de matrice ligne par ligne. »
Je vais donc scanner les lignes pour additionner les valeurs. Un booléen associé à chaque colonne me dira si j'ai déjà rencontré un zéro dans cette colonne, auquel cas j'ignore la valeur.
En pratique je vais donc visiter des cases pour lesquelles je sais que les valeurs seront ignorées, que je n'aurais pas visitées en scannant par colonne, mais pour l'instant je pense que c'est pour le mieux.
De plus, peu avant de faire l'exercice j'avais visionné les présentations Parsing JSON Really Quickly: Lessons Learned (Daniel Lemire), Speed Is Found In The Minds of People (Andrei Alexandrescu) et Path Tracing Three Ways: A Study of C++ Style (Matt Godbolt) dans lesquelles les auteurs avaient amélioré les performances de manière remarquable en implémentant leurs algorithmes de manière branchless, c’est-à-dire sans conditions if qui amènent le prédicteur du processeur à faire des suppositions. Sans entrer dans les détails, il faut savoir qu'une prédiction a un coût d'annulation parfois plus grand que ce qui a été gagné par les prédictions justes. Un code sans conditions évite ce problème et se comporte de manière stable, mais il faut réussir à intégrer quelque chose correspondant à la condition dans les calculs.
Implémentation initiale
Assez de bla bla, allons dans le code. L'implémentation initiale (que tu peux retrouver dans ce dépôt sur GitHub) ressemble à cela :
int matrix_elements_sum_branchless(const std::vector<std::vector<int>>& matrix)
{
const int row_size(matrix[0].size());
std::vector<char> usable_column(row_size, 1);
int result(0);
for (const std::vector<int>& row : matrix)
for (int i(0); i != row_size; ++i)
{
const int v(row[i]);
result += v * usable_column[i];
usable_column[i] *= (v != 0);
}
return result;
}
Il n'y a pas beaucoup de lignes mais déjà beaucoup de questions, à commencer par « est-ce que c'est vraiment mieux qu'un scan de colonnes ? ». Regardons donc la solution la mieux notée sur CodeSignal.
int matrix_elements_sum_best_ratings(const std::vector<std::vector<int>>& m)
{
int r(0);
for (int j=0; j<(int)m[0].size(); j++)
for (int i=0; i<(int)m.size(); i++)
{
if (m[i][j]==0)
break;
r += m[i][j];
}
return r;
}
Bien, ça m'a l'air d'être un scan de colonnes justement ! Comparons les performances des deux implémentations à l'aide de Google Benchmark.
Le jugement
Pour les tests j'utilise une matrice de valeurs aléatoires, chaque cellule ayant une chance d'autant plus grande d'être zéro qu'elle est proche du bas de la matrice. Bien que l'énoncé indiquait une taille maximum de 10×10 pour la matrice, je teste aussi pour des tailles supérieures : 100×100, 1000×1000 et 10000×10000.
Run on (4 X 3100 MHz CPU s)
CPU Caches:
L1 Data 32 KiB (x2)
L1 Instruction 32 KiB (x2)
L2 Unified 256 KiB (x2)
L3 Unified 3072 KiB (x1)
-------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------
branchless/10 147 ns 147 ns 4624304
branchless/100 13613 ns 13612 ns 50630
branchless/1000 1363899 ns 1363210 ns 516
branchless/10000 134339389 ns 134330069 ns 5
best_ratings/10 40.5 ns 40.5 ns 17268039
best_ratings/100 1422 ns 1422 ns 485581
best_ratings/1000 49662 ns 49662 ns 14096
best_ratings/10000 3599890 ns 3599673 ns 204
Wow, la version la mieux notée est bien meilleure de la mienne ! Voyons si je peux améliorer ça. Déjà je me demande si l'implémentation branchless est vraiment mieux qu'une version avec branches. Comparons avec cette implémentation :
int matrix_elements_sum_branches(const std::vector<std::vector<int>>& matrix)
{
const int row_size(matrix[0].size());
std::vector<char> usable_column(row_size, 1);
int result(0);
for (const std::vector<int>& row : matrix)
for (int i(0); i != row_size; ++i)
if (usable_column[i])
{
const int v(row[i]);
if (v == 0)
usable_column[i] = 0;
else
result += v;
}
return result;
}
Nouveaux résultats :
-------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------
branchless/10 161 ns 161 ns 4315630
branchless/100 13757 ns 13757 ns 46364
branchless/1000 1352468 ns 1352375 ns 517
branchless/10000 134475422 ns 134475261 ns 5
branches/10 116 ns 116 ns 5764899
branches/100 7689 ns 7689 ns 84635
branches/1000 781502 ns 781443 ns 861
branches/10000 70365787 ns 70364814 ns 8
best_ratings/10 42.0 ns 42.0 ns 16691501
best_ratings/100 1091 ns 1091 ns 628554
best_ratings/1000 41888 ns 41886 ns 16697
best_ratings/10000 3454331 ns 3454125 ns 203
Mmmmh okay, donc la version branchless se comporte moins bien que la version avec branches. Décidémment c'est mal parti.
Des benchmarks et trop d'idées
Tiens, j'ai envie de tester avec un std::vector<bool>
pour usable_column
. Je doute que ça apporte un réel gain mais je me demande quel impact aurait l'implémentation bitset par rapport à la précédente. Ça commence à faire beaucoup de lignes dans l'output alors passons en mode graphique et profitons-en pour ajouter des tailles de matrices.
Apparemment mon ordinateur a un cache L1 de 32 ko, deux caches L2 de 256 ko et un cache L3 de 3072 ko. Cela signifie que sur des entiers de 32 bits les matrices en dessous de 64×64 tiennent dans un cache L1, puis dans L2 jusqu'à 128×128, puis L3 jusqu'à 512×512. Au delà il faudra aller chercher les données en RAM.
Bien, bien, bien… Les implémentations qui utilisent un std::vector<bool>
sont moins efficaces que les originales, et dans tous les cas c'est pire que la version en colonnes.
Et si je stockais les indices de la première et la dernière colonne encore pertinentes ? Ainsi le nombre de colonnes scannées devrait réduire au fur et à mesure que l'on descend dans la matrice. Ça donne quelque chose comme ça :
int matrix_elements_sum_branches_range
(const std::vector<std::vector<int>>& matrix)
{
const int row_size(matrix[0].size());
std::vector<char> usable_column(row_size, 1);
int first(0);
int last(row_size);
int result(0);
for (const std::vector<int>& row : matrix)
{
for (; (first != last)
&& ((usable_column[first] == 0) || (row[first] == 0));
++first);
int last_non_zero(first);
for (int i(first); i != last; ++i)
if (usable_column[i])
{
const int v(row[i]);
if (v == 0)
usable_column[i] = 0;
else
{
result += v;
last_non_zero = i;
}
}
last = last_non_zero + 1;
}
return result;
}
Et le benchmark :
Ha-HA ! Ça coupe enfin la ligne de l'algo naïf. Est-ce qu'il y a moyen de faire mieux ? Déjà l'approche est un peu limite car l'intervalle des colonnes ne réduit efficacement que si les extrêmes contiennent des zéros assez haut. Je vais tenter de n'itérer que sur les colonnes dont je sais qu'elles sont encore pertinentes de façon à ce que l'ordre n'ait plus d'importance. usable_columns
devient un vecteur d'indices de colonnes à traiter duquel je supprime les indices à chaque fois que je rencontre un zéro.
int matrix_elements_sum_indices(const std::vector<std::vector<int>>& matrix)
{
const int row_size(matrix[0].size());
std::vector<int> usable_columns(row_size);
const auto usable_columns_begin(usable_columns.begin());
auto usable_columns_end(usable_columns.end());
std::iota(usable_columns_begin, usable_columns_end, 0);
int result(0);
for (const std::vector<int>& row : matrix)
for (auto it(usable_columns_begin); it != usable_columns_end;)
{
const int i(*it);
const int v(row[i]);
if (v == 0)
{
--usable_columns_end;
*it = *usable_columns_end;
}
else
{
result += v;
++it;
}
}
return result;
}
Alors qu'est-ce que ça donne…
Wouhou ! Ça devient concret, dès 1024×1024 l'algorithme ci-dessus devient plus rapide que l'algo naïf. Quoi d'autre pour l'améliorer ?
La période sombre
Je n'aime pas trop la méthode utilisée pour retirer les indices de usable_columns
: l'indice à supprimer est échangé avec le dernier de la liste puis on décrémente le pointeur de fin pour l'ignorer. Je m'attends à ce qu'après quelques itérations les indices soient complètement désordonnés et donc les accès mémoire complètement aléatoires. Est-ce que les performances seraient meilleures en passant un peu de temps à maintenir la liste triée ? Un seul moyen de le savoir :
int matrix_elements_sum_indices_sort
(const std::vector<std::vector<int>>& matrix)
{
const int row_size(matrix[0].size());
std::vector<int> usable_columns(row_size);
const auto usable_columns_begin(usable_columns.begin());
auto usable_columns_end(usable_columns.end());
std::iota(usable_columns_begin, usable_columns_end, 0);
int result(0);
for (const std::vector<int>& row : matrix)
{
for (auto it(usable_columns_begin); it != usable_columns_end;)
{
const int i(*it);
const int v(row[i]);
if (v == 0)
{
--usable_columns_end;
*it = *usable_columns_end;
}
else
{
result += v;
++it;
}
}
// Cette ligne en plus.
std::sort(usable_columns_begin, usable_columns_end);
}
return result;
}
Non. Réponse égale non. Retrier les indices à chaque itération ne compense pas l'hypothétique perte de performances d'accès aléatoires en mémoire. Tentons la suppression de l'indice avec std::vector<>::erase(iterator)
et puis une autre implémentation en stockant les indices dans un std::set
et encore une autre dans std::unordered_set
. Je n'y crois pas trop mais ça me semble important pour la complétude.
Sans surprise, il n'y a aucun gain ici. Le second meilleur, après l'algo naïf, reste celui qui conserve la liste d'indices dans un std::vector
et qui échange les indices supprimés avec le dernier de la collection.
Et si l'algo retournait le résultat dès lors qu'il n'y a plus d'indices à traiter, c’est-à-dire la pratique du return early ? Tiens je vais essayer sur plusieurs des implémentations précédentes pour voir.
Bon, mauvaises piste. Ça fait partie du jeu de l'optimisation : dès fois on trouve et des fois on se perd. À part pour l'implémentation branchless il n'y a pas d'intérêt à quitter tôt. Cela dit c'est surprenant de voir que la version « indices » se comporte moins bien avec le return early. Est-ce le résultat de mauvaises prédictions du processeur ? Je ne saurais dire.
Continuons sur la version « indices ». La fonction commence par remplir un vecteur avec tous les indices de 1 à n par un appel de std::iota
. Est-ce qu'on ne pourrait pas faire mieux en générant un tableau d'indices jusqu'à la taille maximum des matrices dès le début du programme puis initialiser la variable usable_columns
à partir de ce tableau ? Nope, ça ne change rien. Crois-moi, j'ai testé.
Tiens, une autre approche classique de l'optimisation, je vais dérouler en partie la boucle la plus interne. L'implémentation commence à être un peu longue alors je vous invite à regarder le code sur GitHub. Il y a plusieurs implémentations :
indices_4_by_4
parcoursusable_columns
par segments de 4 colonnes à la fois et swappe ces indices en branchless de manière à mettre ceux qui ont abouti à zéro à la fin de ce segment, puis les remplace par les valeurs à la fin d'usable_columns
.indices_4_by_4_partition
fait la même chose mais utilisestd::partition
pour réordonner le segment.et
indices_8_by_8_partition
fait de même sur un segment de 8 colonnes.
Et le benchmark :
Bon, aucun succès à nouveau. Le compilateur se débrouille clairement mieux que moi pour optimiser la boucle, ce qui n'est pas vraiment surprenant.
Changement d'outils
Un peu à court d'idée je me demande ce que ça donnerait si la matrice était représentée en column-major plutôt que row-major. En d'autres termes, je me demande quel est l'impact de la structure de données sur les performances. Au niveau de l'algorithme ça serait assez direct:
int matrix_elements_sum_column_major(const std::vector<std::vector<int>>& m)
{
int r(0);
for (const std::vector<int>& column : m)
for (int v : column)
{
if (v == 0)
break;
r += v;
}
return r;
}
Et au niveau des performances :
Intéressant, avec une structure de données en column-major les performances sont les mêmes que la meilleure combinaison des algos en row-major vus jusque-là, et l'implémentation est simplissime.
Est-ce qu'un changement de compilateur change aussi les performances relatives des implémentations précédentes ? Essayons avec Clang 9 :
Ça ressemble pas mal aux résultats obtenus avec GCC 9, avec un clair gain pour la structure de données column-major pour les petites tailles. Par contre, cette structure de données perd face à indices
en row-major
des 256×256. C'est surprenant.
Allez pour finir comparons les performances relatives de Clang et GCC dans un seul graphe. Le programme est compilé avec les deux compilateurs puis lancé sur la seed 1234. Je ne prends que les meilleurs algorithmes vus précédemment :
Globalement c'est assez semblable même si Clang se débrouille légèrement mieux sur la version indices
et GCC l'emporte à peu de choses sur l'algorithme naïf. Sur la version column-major aucun deux compilateurs ne se démarque.
Il y a quatre ans j'avais effectué des benchmarks qui montraient une forte différence d'optimisations entre GCC et Clang . C'est agréable de voir que les performances des deux compilateurs sont maintenant plutôt proches.
Quand et quoi optimiser
Premature optimization is the root of all evil.
Il est coutume dans le monde des programmeurs d'accueillir par cette citation (amputée) toute tentative d'optimisation d'un code sans avoir d'abord montré son impact sur les performances. La version longue, moins souvent citée, ressemble à ça :
We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.
— Donald Knuth
Faut-il pour autant écrire les algorithmes les plus naïfs dès le départ comme pour prévenir toute sorte d'optimisation prématurée ? Eh bien ça dépend. L'algorithme naïf pour le problème considéré ici est performant tant que la matrice est très petite, puis s'écroule rapidement au fur et à mesure que les matrices traitées sont plus grandes. La question qu'il faut alors se poser est « Quel est l'ordre de grandeur des données que mon algo va réellement traiter ? » Autrement dit, est-ce qu'il y a absolument toutes sortes de tailles (ce n'est certainement pas le cas), est-ce que les matrices tiennent toujours dans le cache du processeur ?
Nous avons vu aussi que la structure de données elle-même a un fort impact sur les performances. En passant en column-major un algorithme natif se comporte aussi bien que les meilleurs cas de tous les autres de la version row-major. Ce qui amène à une autre question qui ouvre la porte à d'autres formes d'optimisations : ai-je la main sur la structure de données ? Comment puis-je la transformer pour simplifier mon implémentation et le travail du compilateur ?
En tant que développeur je pense qu'il est bon d'acquérir une certaine culture sur ce que peut faire le matériel qui fait tourner nos programmes ainsi que sur le coût relatif des traitements de nos algorithmes. Non pas pour optimiser tous les cas d'utilisations mais au moins pour ne pas surcharger de traitements inutiles des programmes déjà gourmands. Une sorte de preuve de bonne intention en quelque sorte.
Cette culture s'acquiert par la pratique ; c'est ce que nous apportent ces exercices de programmation. Notamment ici l'approche que je croyais solide (row-major -> scan par ligne) était complètement à la rue sur les petites tailles. On voit alors l'intérêt de pouvoir faire tenir les données dans le cache du processeur. De plus les versions branchless étaient moins performantes que les équivalents branchful (peut-être est-ce juste un manque de savoir faire pour le coup). Ce fut aussi l'occasion de tester un paquet d'implémentations et de constater, à nouveau, l'incroyable travail d'optimisation que font les compilateurs avec notre code.
Finalement ce petit exercice de programmation a amené beaucoup de questions et des réponses parfois surprenantes. J'ai appris des choses donc je pense que ça valait le coup. Nous verrons si les exercices suivants sont aussi enrichissants !
# Impact de la structure
Posté par n6p7 . Évalué à 6.
Merci pour cette (longue) lecture avec une analyse complète et intéressante.
Je reste curieux de la raison du choix du type la structure pour la matrice, un
std::vector<std::vector<int>>
plutôt qu'unstd::vector<int>
de dimensionlignes*colonnes
, peut-être était-ce un choix délibéré pour la simplicité d'accès aux lignes/colonnes. Ça reste acceptable vu la contraire initiale de la taille de matrice d'ordre 10×10. Cela dis, si on cherche les meilleures performance possible, ce point n'est probablement pas négligeable et cestd::vector<std::vector<int>>
me jette quelques grains de sable dans les yeux, pour plusieurs raisons :*
std::vector
est un conteneur qui stocke les données de façon contiguë dans une zone mémoire allouée dynamiquement. Grossièrement, la structure est composée de trois pointeurs : début/fin de la zone allouée, et première "case" libre (ça peut varier selon l'implémentation)*
std::vector<std::vector<int>>
est donc une structure qui pointe sur une zone allouée dynamiquement, qui elle contient desstd::vector<int>
qui chacun d'entre eux pointent sur d'autres zones allouées dynamiquement* Il y a déjà un impact à l'initialisation, puisqu'il faudra allouer autant de zones que les dimensions de la matrice le nécessite (mais c'est hors benchmark ici)
* Il n'y a aucune garantie que toutes ces "petites zones" soie allouée de façon contiguë (c'est plutôt improbable), donc on augmente le risque de cache-miss
* Je ne m'étends pas sur le surcoût d'utilisation de mémoire induit par la structure
Je serais curieux de comparer ces résultats à ceux exploitant une représentation de la matrice dans une seule zone continue (par ex avec un
std::vector<int>
). J'aurais bien voulu ajouter un comparatif à ce commentaire, mais je ne vais malheureusement pas avoir le loisir de m'amuser avec ce problème dans les prochain temps.[^] # Re: Impact de la structure
Posté par Benoît Sibaud (site web personnel) . Évalué à 5. Dernière modification le 12 février 2020 à 08:02.
C'est un cas intéressant maths vs informatique : des structures [ligne][colonne], [colonne][ligne] ou [ligne x colonne] sont mathématiquement équivalentes et informatiquement très différentes (en termes de performances mémoire ou disque, pas fonctionnellement).
[^] # Re: Impact de la structure
Posté par Shuba . Évalué à 3.
Mais
std::vector<std::vector<int>>
n'est pas du tout équivalent à une matrice, car la longueur de chaque "ligne" peut changer. Tu peux probablement montrer que pour toute structure de donnnées qui permet une longueur de "ligne" variable, il existe une structure à longueur de "ligne" fixe plus efficace.Du coup je ne vois pas ça comme une différence entre maths et info, mais plutôt comme une structure de donnée trop flexible par rapport à la représentation mathématique cherchée.
# Quitte à faire du branchless
Posté par serge_sans_paille (site web personnel) . Évalué à 4.
Je te propose cette légère amélioration :
Elle a l'avantage de se vectoriser très bien, et d'éviter le passage par une multiplication. Elle reste moins bonne que la version de référence, sauf dans les cas dégénérés (pas de zéro p.e.). On notera qu'on pourrait aussi passer la boucle interne sous OpenMP :-)
Cas dégénéré : http://quick-bench.com/PTIzGKHuheFGyKEVjkAPBwgDo4s
Cas normal : http://quick-bench.com/goZHQlQ10U4sDqMiEP0nMyyXUBs
Merci pour ce petit jeu d'esprit !
[^] # Re: Quitte à faire du branchless
Posté par Julien Jorge (site web personnel) . Évalué à 2.
Bien vu pour la version sans multiplication :) On voit bien la vectorisation et ça marche bien mieux :
[^] # Re: Quitte à faire du branchless
Posté par n6p7 . Évalué à 1.
Je me suis permis d'ajouter à ce bench l'implémentation que je proposais dans mon commentaire, pour mettre en évidence l'impact du choix de la structure de donnée sur les perfs :
http://quick-bench.com/lSIBctcUoFcxRNSssMJTMy93Zfg
On constate (pour les paramètres donnés) que l'intuition initiale du rédacteur sur la version branchless se retrouve dans ce cas. L'écart de performances de la version branchless "avec multiplication" par rapport au autres versions se réduit avec une structure contiguë en mémoire.
[^] # Re: Quitte à faire du branchless
Posté par Julien Jorge (site web personnel) . Évalué à 2.
Tiens, dis-moi, j'essaye le même genre de truc sur la version
indices
, qui maintient une liste les indices des colonnes pertinentes :Ça ne fonctionne pas très bien et je pense que c'est à cause de l'écriture dans le bloc d'ici à là. Si je mets à la place de ce bloc la condition suivante :
Alors ça fonctionne très bien.
Ma compréhension est grosso-modo que dans la première version on réécrit inconditionnellement dans la mémoire pointée par
it
, et bien qu'elle est probablement en cache (on vient juste d'y accéder) et que sa valeur ne change pas toujours, la ligne de cache devient toujours dirty et il faut la renvoyer en RAM. Du coup on paye une écriture à chaque itération, qui coûte bien plus cher qu'une misprediction occasionnelle. Qu'en penses tu ?# best-rating == column-major
Posté par YBoy360 (site web personnel) . Évalué à 2.
Je n'aurais pas appelé "branchless" un algorithme avec des boucles for. Pour
Ce n'est pas évident que ce branchement soit inliné, il aurait été bien d'avoir un extrait du code assembleur, et de comparer avec une version sans multiplication.
Pour faire vraiment de l'optimisation CPU, il faut presque obligatoirement un "profiler" pour observer l'impact des accès mémoire sur le binaire optimisé. Il faut donc faire un peu d'assembleur, c'est la partie la plus intéressante. Enfin, le C ou même le Fortran simplifie ce travail et offre plus de liberté par rapport au C++ (on peut toujours faire du C dans du C++ ??).
Le meilleur est celui qui accède à la mémoire de façon contiguë, en en faisant le moins. Comme les matrices sont de tailles fixes, on peut imaginer d'autres optimisations, du style en C, accéder à la prochaine adresse pour chauffer le cache sans rien faire, du prefetch, donner de la visibilité au compilateur en déroulant un peu ces boucles ou en utilisant des indices connus à la compilation.. , mais on navigue en aveugle sans un extrait du code désassemblé.
[^] # Re: best-rating == column-major
Posté par Julien Jorge (site web personnel) . Évalué à 2. Dernière modification le 12 février 2020 à 09:17.
Bien vu ! Voici une version sur Compiler Explorer où on peu voir le corps de la version branchless :
et celui avec les branches :
# Lisibilité
Posté par xulops (site web personnel) . Évalué à 10.
C'est un journal intéressant en recherche de performances pures, mais faut pas oublier un truc qui parfois fait gagner en performances globales : la lisibilité du code.
Je m'explique : un code super optimisé qui te fait gagner une seconde de traitement sur un script qui s'exécute tous les jours, ça fait gagner 3 minutes en 6 mois. Mais si au bout de ces 6 mois tu mets 3 heures à déchiffrer comment marche ce putain de code que tu veux modifier, tu as globalement perdu 2h57m (et encore, j'estime que mon temps de vie est plus important que le temps processeur perdu, chacun ses priorités).
Donc oui, c'est un bon exercice mental, c'est même extra de se pencher là dessus de manière aussi détaillée, … tant que ça reste un jeu de l'esprit.
En entreprise, en prod, on va essayer de trouver un équilibre, un compromis, entre performance et lisibilité, histoire de ne pas perdre des heures plus tard si le code doit être légèrement modifié. On a probablement tous haï des gars qui ont fait des codes trop tarabiscotés dans le but de gagner un pouillème.
[^] # Re: Lisibilité
Posté par cimcim . Évalué à 4.
Mais si c'est un code qui est exécuté sur des milliers de produits et/ou chez des milliers de clients, le rapport temps gagné/temps dépensé est nettement plus favorable à l'optimisation.
[^] # Re: Lisibilité
Posté par barmic 🦦 . Évalué à 5.
Jamais. J'ai haï les gens qui n'ont pas couverts leur code par des tests. J'ai aucun problème pour que quelqu'un ai pensé faire quelque chose d'un peu complexe, mais :
Si tu as ça, il n'y a aucun problème à déployer des trésors de subtilité.
Après oui la lisibilité est importante1 et il faut voir si optimiser est intéressant. Mais là il s'agit d'un exercice d'optimisation donc on va considérer que c'est utile ;)
par exemple je vais préférer utiliser
0xff_ff_ff_ff
ou~0
plutôt que -1 comme valeur d'entier quand il est utiliser comme bit patterns. ↩https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Lisibilité
Posté par xulops (site web personnel) . Évalué à 3.
Voilà pourquoi j'ai mis un "probablement" dans ma phrase, je me doutais qu'il existe des exceptions, chanceux va !
Je me souviens d'avoir du modifier des codes écrits en GAP3 (sur AS/400) écrits par un gars que je n'ai pas connu personnellement, mais que j'ai traité de tous les noms. Et puis j'ai un peu regretté quand j'ai appris que le gars s'était suicidé (avant que j'arrive, po ma faute, hein). La psychologie d'une personne influence-t-elle sa façon de coder ? Moi, j'y crois. Faire des usines à gaz pour gagner des queues de cerises, en comptant le temps qu'il a du y passer et le mien à m'arracher les cheveux, le jeu n'en valait pas la chandelle.
Après, il existe bien entendu des cas spéciaux où l'optimisation pure est LA priorité, mais c'est marginal.
Le sujet du journal, oui, c'est utile car l'optimisation pure est le but du jeu, jeu qui justement peut être utile dans ces cas spéciaux.
[^] # Re: Lisibilité
Posté par barmic 🦦 . Évalué à 1. Dernière modification le 12 février 2020 à 11:50.
Je pense que tu a raté ma remarque. J'ai râlé sur les même personnes, mais je considère que le problème n'est pas tant leur code que le fait qu'ils n'ont pas écrit de test.
Non, c'est une question de domaine. L'optimisation CPU de code n'est pas majoritaire, mais on peut difficilement dire que c'est marginal, mais la gestion de la performance (ce qui peut être très différent) n'est pas marginale du tout.
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Lisibilité
Posté par Meku (site web personnel) . Évalué à 2.
Ou bien l'inverse : la façon de coder influence sa psychologie.
Péter un câble car on n'arrive même plus à comprendre son propre code tout pourri.
--> []
[^] # Lisibilité… et optimisation là où c'est utile !
Posté par SpaceFox (site web personnel, Mastodon) . Évalué à 9. Dernière modification le 12 février 2020 à 11:43.
Personnellement je n'ai pas de problème à voir du code peu lisible pour des questions d'optimisation, à partir du moment où :
C'est bizarre, mais le point 4 est en fait assez rare. Je ne compte plus les trouzaines de micro-optimisations complètement inutiles alors que juste à côté il y a un énorme problème de performances facile à corriger.
J'ai l'impression que beaucoup de (mauvais ?) développeurs optimisent sur ce qu'ils pensent être problématique, au lieu de mesurer ce qui est réellement problématique. Et c'est comme ça qu'on se retrouve avec des trucs tordus pour gagner 0.5 ms sur une page qui met 15 secondes pour être calculée. Ou une astuce pour gagner quelques Ko de RAM, alors qu'une structure censée stocker des dizaines de millions d'entrée en RAM est tellement pétée qu'il lui faut 280 octets pour stocker un
double
(8 octets) (et donc, 10 millions d'entrées prennent 2,8 Go de RAM pour 80 Mo de données utiles) (je précise que la structure n'offre aucun avantage sur un tableau)…On a certes dépassé la grande époque de PHP et des conseils moisis à base de « Utilise des guillemets simples, c'est plus rapide que les guillemets doubles », mais c'est encore des problèmes que je rencontre souvent.
La connaissance libre : https://zestedesavoir.com
# projet euler
Posté par solsTiCe (site web personnel) . Évalué à 6.
moi j'ai testé mes limites en programmation et les limites de ma machine avec https://projecteuler.net/
La méthode bourrin ne marche jamais. Donc il faut trouver un algo approprié.
Donc c'est plus un exercice de math avec une recherche d'algo. Mais ca rejoint les besoins d'optimisation.
Je suis que niveau 2 /o\ sur le site ….
[^] # Re: projet euler
Posté par reno . Évalué à 3.
La méthode bourrin ne marche jamais sur les exo du project Euler, dans la vraie vie, faut voir..
# du pourquoi du comment
Posté par David Marec . Évalué à 4.
En fait, la situation est pire encore: au delà, on se paye des défaut de cache et on perd des centaines de cycles.
Reste à voir si les performances sont meilleures parce qu’on a supprimé les branches, ou parce que l'on a réussi à se coller pile poil à la taille des caches…
Après, ça devient compliqué puisque l'on optimise en fonction du hardware qui tourne en dessous.
[^] # Re: du pourquoi du comment
Posté par Philippe F (site web personnel) . Évalué à 5.
C'est vraiment la question que je me pose à chaque fois quand je lis ce genre d'optimisation. Combien sont spécifiques à la machine hôte, et combien vont encore fonctionner sur une machine avec une config hardware légèrement ou fondamentalement différente ?
# Et la version courte ?
Posté par Philippe F (site web personnel) . Évalué à 2.
J'avoue que sur ces exercices de programmation, j'adore la partie qui consiste à chercher le programme le plus court qui résout le problème. Surtout en Python, en jouant sur le langage, on arrive à gratter pas mal par rapport à la solution de base.
Par contre, s'il y a un codeur ruby dans le challenge, aucune chance. Ce langage est vraiment très concis!
[^] # Re: Et la version courte ?
Posté par Bruno Michel (site web personnel) . Évalué à 6.
Une version possible en Ruby :
[^] # Re: Et la version courte ?
Posté par n_e . Évalué à 3.
La même chose en Haskell :
# C++ boost
Posté par devnewton 🍺 (site web personnel) . Évalué à 4.
Peut être qu'avec un soupçon de boost, tu aurais des perfs équivalentes et un code plus élégant?
Le post ci-dessus est une grosse connerie, ne le lisez pas sérieusement.
[^] # Re: C++ boost
Posté par Julien Jorge (site web personnel) . Évalué à 3.
L'interface et l'API de ublas sont super mais malheureusement les perfs ne suivent pas :(
# benchmarks latence et bande passante CPU en 2020
Posté par Cyril Chaboisseau (Mastodon) . Évalué à 2.
Un autre benchmark qui fait suite à un article de 2012 souvent donné comme référence (Numbers Every Programmer Should Know) permet de voir comment la latence et la bande passante des processeurs a progressé en 8 ans.
https://www.forrestthewoods.com/blog/memory-bandwidth-napkin-math/
l'auteur note que l'article original donnait des chiffres qui était (au moins pour le premier d'entre eux) un peu sujets à caution :
Et que maintenant, sur un i7 il a pu en gros calculer la latence suivante
et pour la bande passante :
# Euuuhhhh....
Posté par calandoa . Évalué à -2.
C'est pas que je veux troller comme un sagouin, mais j'ai l'impression que tout ce journal et une bonne partie des commentaires n'est qu'une gigantesque branlette intellectuelle qui ne va nulle part. Je suis un peu désolé de dire ça de but en blanc, pour une fois qu'on a un journal qui met les mains dans le cambouis.
Je reprends la solution best rating et je remplace les entêtes des boucles
for
par ceux (+ ou -) de indices, et là, boum, j'ai un x10 sur des grosses matrices qui renvoie tous les concurrents chez leur mère respective :Je précise que je suis une grosse tanche en C++ (juste qq vagues connaissances en copier/coller), langage qui n'est lui même qu'une gigantesque branlette dont la syntaxe est en train d'évoluer vers un délire cabalisto-confusionniste et dont le but, comme ce journal le démontre, n'est que se répandre en débats intranchables car personne de normalement constitué n'est capable de comprendre ce que peut bien faire le compilo de tout ce gloubi-boulga indigeste (à part nous balancer des messages d'erreur encore moins compréhensible qu'un stack dump).
[^] # Re: Euuuhhhh....
Posté par Julien Jorge (site web personnel) . Évalué à 10.
Ta solution est sans doute très efficace, malheureusement elle ne répond pas à la question ! Le but est d'ignorer les valeurs pour lesquelles il existe un zéro plus haut dans la même colonne. Là ton algo ignore le reste de la ligne quand il rencontre un zéro.
Ça fonctionnerait si la matrice était en column-major, ce qui est d'ailleurs pris en considération dans le journal, mais ça demande de changer la structure de donnée. C'est un peu tricher par rapport à l'exercice. J'aurais bien voulu croire que tu considères que la matrice est en column-major mais comme tu as nommé ta variable
row
je ne doute pas que tu la considères en row-major.En tout cas merci pour les insultes et le mépris, c'est top ! <3
[^] # Re: Euuuhhhh....
Posté par calandoa . Évalué à 7.
Il n'y avait pas d'insulte contre toi, juste contre le C++… après c'est vrai le mépris a un peu débordé sur le journal, et je reconnais après coup que c'était totalement déplacé : effectivement, le remplacement que j'ai opéré change complètement l'algo, et je me suis auto-abusé en imaginant c'était le compilo qui optimisait simplement une syntaxe mieux qu'une autre, perdu que j'étais à essayer de comprendre ce n-ième style de C++.
Après essai, en essayant d'optimiser le
m[i][j]
, il n'y a quasiment pas de différence. Je n'ai pas essayé en changeant le sens de la matrice, et j'imagine qu'il n'y a plus d'intérêt vu que la rapidité de indices tient à la manière dont le cache charge simultanément les lignes de la matrice.Donc je réitère, je te présente toutes mes excuses pour cet exercice qui est en fait assez intéressant quant au fonctionnement de la mémoire. Et donc logiquement, il ne m'en reste plus à présenter au C++, quel dommage !
# Comment lancer les tests en C++ sur son propre ordinateur sous Linux ?
Posté par SpaceFox (site web personnel, Mastodon) . Évalué à 2.
Hello,
Je ne connais à peu près rien au C++, mais j'aimerais lancer ces tests sur mon propre ordinateur. J'ai un système Linux sous la main avec les outils de dev installés et j'ai récupéré le code, mais là je ne sais pas quoi faire.
Parce j'ai écrit une version Java du benchmark, à la fois pour tester le système de microbenchmark de Java (JMH), et pour voir quel différence j'avais avec la version C++.
Or, sur les grosse matrices (10 000 x 10 000) je trouve un facteur 10 en faveur de Java, et reproductible, ce qui me semble franchement bizarre… du coup je voulais lancer la version C++ pour voir ce que ça donnait chez moi :)
La connaissance libre : https://zestedesavoir.com
[^] # Re: Comment lancer les tests en C++ sur son propre ordinateur sous Linux ?
Posté par SpaceFox (site web personnel, Mastodon) . Évalué à 3.
Bon, j'ai trouvé ce qui n'allait pas, c'était une erreur dans ma compréhension du fonctionnement du générateur de nombres aléatoires en C++, qui faisait que ma version Java avait trop de 0 (et donc allait sensiblement plus vite).
La connaissance libre : https://zestedesavoir.com
[^] # Re: Comment lancer les tests en C++ sur son propre ordinateur sous Linux ?
Posté par barmic 🦦 . Évalué à 5.
Je pense que pour tester des langages différents il vaut mieux générer des matrices séparément et les donner en entrée aux deux programmes, non ?
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.