Considérez le code suivant (p
est de type unsigned char*
et bitmap->width
est d'un type entier, qui est exactement inconnu et dépend de la version de la bibliothèque externe que nous utilisons):
for (unsigned x = 0; x < static_cast<unsigned>(bitmap->width); ++x)
{
*p++ = 0xAA;
*p++ = 0xBB;
*p++ = 0xCC;
}
Vaut-il l'optimisation [..]
Pourrait-il y avoir un cas où cela pourrait donner des résultats plus efficaces en écrivant:
unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0; x < width; ++x)
{
*p++ = 0xAA;
*p++ = 0xBB;
*p++ = 0xCC;
}
... ou est-ce trivial pour le compilateur à optimiser?
Que considéreriez-vous comme un "meilleur" code?
Note de l'éditeur (Ike): pour ceux qui s'interrogent sur le texte barré, la question d'origine, telle qu'elle est formulée, était dangereusement proche d'un territoire hors sujet et était très près d'être fermée malgré des commentaires positifs. Cependant, ne punissez pas les personnes qui ont répondu à ces sections de la question.
À première vue, je pensais que le compilateur pouvait générer un assemblage équivalent pour les deux versions avec des drapeaux d'optimisation activés. Quand je l'ai vérifié, j'ai été surpris de voir le résultat:
unoptimized.cpp
note: ce code n'est pas destiné à être exécuté.
struct bitmap_t
{
long long width;
} bitmap;
int main(int argc, char** argv)
{
for (unsigned x = 0 ; x < static_cast<unsigned>(bitmap.width) ; ++x)
{
argv[x][0] = '\0';
}
return 0;
}
optimized.cpp
note: ce code n'est pas destiné à être exécuté.
struct bitmap_t
{
long long width;
} bitmap;
int main(int argc, char** argv)
{
const unsigned width = static_cast<unsigned>(bitmap.width);
for (unsigned x = 0 ; x < width ; ++x)
{
argv[x][0] = '\0';
}
return 0;
}
$ g++ -s -O3 unoptimized.cpp
$ g++ -s -O3 optimized.cpp
.file "unoptimized.cpp"
.text
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
movl bitmap(%rip), %eax
testl %eax, %eax
je .L2
xorl %eax, %eax
.p2align 4,,10
.p2align 3
.L3:
mov %eax, %edx
addl $1, %eax
movq (%rsi,%rdx,8), %rdx
movb $0, (%rdx)
cmpl bitmap(%rip), %eax
jb .L3
.L2:
xorl %eax, %eax
ret
.cfi_endproc
.LFE0:
.size main, .-main
.globl bitmap
.bss
.align 8
.type bitmap, @object
.size bitmap, 8
bitmap:
.zero 8
.ident "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-16)"
.section .note.GNU-stack,"",@progbits
.file "optimized.cpp"
.text
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
movl bitmap(%rip), %eax
testl %eax, %eax
je .L2
subl $1, %eax
leaq 8(,%rax,8), %rcx
xorl %eax, %eax
.p2align 4,,10
.p2align 3
.L3:
movq (%rsi,%rax), %rdx
addq $8, %rax
cmpq %rcx, %rax
movb $0, (%rdx)
jne .L3
.L2:
xorl %eax, %eax
ret
.cfi_endproc
.LFE0:
.size main, .-main
.globl bitmap
.bss
.align 8
.type bitmap, @object
.size bitmap, 8
bitmap:
.zero 8
.ident "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-16)"
.section .note.GNU-stack,"",@progbits
$ diff -uN unoptimized.s optimized.s
--- unoptimized.s 2015-11-24 16:11:55.837922223 +0000
+++ optimized.s 2015-11-24 16:12:02.628922941 +0000
@@ -1,4 +1,4 @@
- .file "unoptimized.cpp"
+ .file "optimized.cpp"
.text
.p2align 4,,15
.globl main
@@ -10,16 +10,17 @@
movl bitmap(%rip), %eax
testl %eax, %eax
je .L2
+ subl $1, %eax
+ leaq 8(,%rax,8), %rcx
xorl %eax, %eax
.p2align 4,,10
.p2align 3
.L3:
- mov %eax, %edx
- addl $1, %eax
- movq (%rsi,%rdx,8), %rdx
+ movq (%rsi,%rax), %rdx
+ addq $8, %rax
+ cmpq %rcx, %rax
movb $0, (%rdx)
- cmpl bitmap(%rip), %eax
- jb .L3
+ jne .L3
.L2:
xorl %eax, %eax
ret
L'assembly généré pour la version optimisée charge réellement ( lea
) la constante width
contrairement à la version non optimisée qui calcule l'offset width
à chaque itération ( movq
).
Quand j'aurai le temps, j'afficherai finalement des références à ce sujet. Bonne question.
Il y a en fait suffisamment d'informations de votre extrait de code pour pouvoir le dire, et la seule chose à laquelle je peux penser est l'alias. De notre point de vue, il est assez clair que vous ne voulez pas que p
et bitmap
pointent vers le même emplacement en mémoire, mais le compilateur ne le sait pas et (car p
est de type char*
) le compilateur doit faire fonctionner ce code même si p
et bitmap
se chevauchent.
Cela signifie dans ce cas que si la boucle change bitmap->width
à travers le pointeur p
alors cela doit être vu lors de la relecture bitmap->width
plus tard, ce qui signifie que son stockage dans une variable locale serait illégal.
Cela étant dit, je crois que certains compilateurs généreront parfois deux versions du même code (j'en ai vu des preuves circonstancielles, mais je n'ai jamais directement recherché d'informations sur ce que fait le compilateur dans ce cas), et vérifie rapidement si les pointeurs alias et exécutez le code plus rapide s'il détermine qu'il est correct.
Cela étant dit, je maintiens mon commentaire sur la simple mesure des performances des deux versions, mon argent est de ne voir aucune différence de performances cohérente entre les deux versions du code.
À mon avis, des questions comme celles-ci sont correctes si votre but est d'en apprendre davantage sur les théories et techniques d'optimisation du compilateur, mais c'est une perte de temps (une micro-optimisation inutile) si votre objectif final est de rendre le programme plus rapide.
D'autres réponses ont souligné que hisser l'opération de pointeur hors de la boucle peut changer le comportement défini en raison de règles d'alias qui permettent à char d'alias quoi que ce soit et n'est donc pas une optimisation autorisée pour un compilateur même si dans la plupart des cas, il est évidemment correct pour un humain programmeur.
Ils ont également souligné que le levage de l'opération hors de la boucle est généralement, mais pas toujours, une amélioration du point de vue des performances et est souvent négatif du point de vue de la lisibilité.
Je voudrais souligner qu'il existe souvent une "troisième voie". Plutôt que de compter jusqu'au nombre d'itérations souhaité, vous pouvez compter jusqu'à zéro. Cela signifie que le nombre d'itérations n'est nécessaire qu'une seule fois au début de la boucle, il n'a pas besoin d'être stocké après cela. Mieux encore au niveau de l'assembleur, il élimine souvent la nécessité d'une comparaison explicite car l'opération de décrémentation définit généralement des indicateurs qui indiquent si le compteur était nul à la fois avant (indicateur de transfert) et après (indicateur de zéro) la décrémentation.
for (unsigned x = static_cast<unsigned>(bitmap->width);x > 0; x--)
{
*p++ = 0xAA;
*p++ = 0xBB;
*p++ = 0xCC;
}
Notez que cette version de la boucle donne des valeurs x dans la plage 1..largeur plutôt que dans la plage 0 .. (largeur-1). Cela n'a pas d'importance dans votre cas, car vous n'utilisez en fait x pour rien, mais c'est quelque chose dont vous devez être conscient. Si vous voulez une boucle de compte à rebours avec des valeurs x dans la plage 0 .. (largeur-1), vous pouvez le faire.
for (unsigned x = static_cast<unsigned>(bitmap->width); x-- > 0;)
{
*p++ = 0xAA;
*p++ = 0xBB;
*p++ = 0xCC;
}
Vous pouvez également vous débarrasser des transtypages dans les exemples ci-dessus si vous le souhaitez sans vous soucier de son impact sur les règles de comparaison, car tout ce que vous faites avec bitmap-> width est de l'affecter directement à une variable.
Ok, les gars, donc j'ai mesuré, avec GCC -O3
(en utilisant GCC 4.9 sur Linux x64).
Il s'avère que la deuxième version est 54% plus rapide!
Donc, je suppose que le repliement est la chose, je n'y avais pas pensé.
[Modifier]
J'ai réessayé la première version avec tous les pointeurs définis avec __restrict__
, et les résultats sont les mêmes. Bizarre .. Soit l'aliasing n'est pas le problème, soit, pour une raison quelconque, le compilateur ne l'optimise pas bien même avec __restrict__
.
[Modifier 2]
Ok, je pense que j'ai été à peu près en mesure de prouver que l'alias est le problème. J'ai répété mon test d'origine, cette fois en utilisant un tableau plutôt qu'un pointeur:
const std::size_t n = 0x80000000ull;
bitmap->width = n;
static unsigned char d[n*3];
std::size_t i=0;
for (unsigned x = 0; x < static_cast<unsigned>(bitmap->width); ++x)
{
d[i++] = 0xAA;
d[i++] = 0xBB;
d[i++] = 0xCC;
}
Et mesuré (a dû utiliser "-mcmodel = large" pour le lier). J'ai ensuite essayé:
const std::size_t n = 0x80000000ull;
bitmap->width = n;
static unsigned char d[n*3];
std::size_t i=0;
unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0; x < width; ++x)
{
d[i++] = 0xAA;
d[i++] = 0xBB;
d[i++] = 0xCC;
}
Les résultats des mesures étaient les mêmes - il semble que le compilateur ait pu l'optimiser par lui-même.
J'ai ensuite essayé les codes originaux (avec un pointeur p
), cette fois quand p
est de type std::uint16_t*
. Encore une fois, les résultats étaient les mêmes - en raison d'un aliasing strict. Ensuite, j'ai essayé de construire avec "-fno-strict-aliasing", et j'ai encore vu une différence de temps.
La seule chose ici qui peut empêcher l'optimisation est la règle d'aliasing stricte . En bref :
"Un alias strict est une hypothèse, faite par le compilateur C (ou C++), que le déréférencement de pointeurs vers des objets de différents types ne fera jamais référence au même emplacement de mémoire (c'est-à-dire un alias entre eux.)"
[…]
L'exception à la règle est un
char*
, Qui est autorisé à pointer vers n'importe quel type.
L'exception s'applique également aux pointeurs unsigned
et signed
char
.
C'est le cas dans votre code: vous modifiez *p
À p
qui est un unsigned char*
, Donc le compilateur must suppose que il pourrait pointer vers bitmap->width
. Par conséquent, la mise en cache de bitmap->width
Est une optimisation non valide. Ce comportement empêchant l'optimisation est indiqué dans réponse de YSC .
Si et seulement si p
pointait vers un type nonchar
et non_decltype(bitmap->width)
, la mise en cache serait-elle une optimisation possible.
La question posée à l'origine:
Vaut-il l'optimisation?
Et ma réponse à cela (recueillant un bon mélange de votes haut et bas ..)
Laissez le compilateur s'en préoccuper.
Le compilateur fera presque certainement un meilleur travail que vous. Et il n'y a aucune garantie que votre "optimisation" soit meilleure que le code "évident" - l'avez-vous mesuré ??
Plus important encore, avez-vous la preuve que le code que vous optimisez a un impact sur les performances de votre programme?
Malgré les votes négatifs (et maintenant voir le problème d'alias), je suis toujours satisfait de cela comme une réponse valide. Si vous ne savez pas s'il vaut la peine d'optimiser quelque chose, ce n'est probablement pas le cas.
Une question assez différente, bien sûr, serait la suivante:
Comment savoir s'il vaut la peine d'optimiser un fragment de code?
Premièrement, votre application ou bibliothèque doit-elle s'exécuter plus rapidement qu'elle ne le fait actuellement? L'utilisateur attend-il trop longtemps? Votre logiciel prévoit-il la météo d'hier plutôt que celle de demain?
Vous seul pouvez vraiment le dire, en fonction de la fonction de votre logiciel et des attentes de vos utilisateurs.
En supposant que votre logiciel nécessite une optimisation, la prochaine chose à faire est de commencer à mesurer. Les profileurs vous diront où votre code passe, il est temps. Si votre fragment ne s'affiche pas comme un goulot d'étranglement, il vaut mieux le laisser seul. Les profileurs et autres outils de mesure vous indiqueront également si vos modifications ont fait une différence. Il est possible de passer des heures à essayer d'optimiser le code, pour constater que vous n'avez fait aucune différence perceptible.
Que voulez-vous dire par "optimisation" de toute façon?
Si vous n'écrivez pas de code "optimisé", votre code doit être aussi clair, propre et concis que possible. L'argument "L'optimisation prématurée est mauvaise" n'est pas une excuse pour un code bâclé ou inefficace.
Le code optimisé sacrifie normalement certains des attributs ci-dessus pour les performances. Cela pourrait impliquer l'introduction de variables locales supplémentaires, avoir des objets avec une portée plus large que prévu ou même inverser l'ordre des boucles normales. Tout cela peut être moins clair ou concis, alors documentez le code (brièvement!) Sur la raison pour laquelle vous faites cela.
Mais souvent, avec un code "lent", ces micro-optimisations sont le dernier recours. Le premier endroit à regarder est les algorithmes et les structures de données. Y a-t-il un moyen d'éviter du tout de faire le travail? Les recherches linéaires peuvent-elles être remplacées par des recherches binaires? Une liste chaînée serait-elle plus rapide ici qu'un vecteur? Ou une table de hachage? Puis-je mettre en cache les résultats? Prendre de bonnes décisions "efficaces" ici peut souvent affecter les performances d'un ordre de grandeur ou plus!
J'utilise le modèle suivant dans une situation comme celle-ci. Il est presque aussi court que le premier cas du vôtre, et meilleur que le deuxième cas, car il conserve la variable temporaire locale à la boucle.
for (unsigned int x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
{
*p++ = 0xAA;
*p++ = 0xBB;
*p++ = 0xCC;
}
Ce sera plus rapide avec un compilateur moins intelligent, une version de débogage ou certains indicateurs de compilation.
Edit1: Placer une opération constante en dehors d'une boucle est un bon modèle de programmation. Il montre la compréhension des bases du fonctionnement de la machine, en particulier en C/C++. Je dirais que l'effort de faire leurs preuves devrait être sur les personnes qui ne suivent pas cette pratique. Si le compilateur punit pour un bon modèle, c'est un bogue dans le compilateur.
Edit2:: J'ai mesuré ma suggestion par rapport au code d'origine sur vs2013, j'ai obtenu une amélioration de% 1. Pouvons-nous faire mieux? Une optimisation manuelle simple donne une amélioration de 3 fois par rapport à la boucle d'origine sur une machine x64 sans recourir à des instructions exotiques. Le code ci-dessous suppose un petit système endian et un bitmap correctement aligné. TEST 0 est d'origine (9 sec), TEST 1 est plus rapide (3 sec). Je parie que quelqu'un pourrait rendre cela encore plus rapide, et le résultat du test dépendrait de la taille du bitmap. Très prochainement, le compilateur sera en mesure de produire du code toujours plus rapide. J'ai peur que ce soit l'avenir quand le compilateur sera aussi un programmeur AI, donc nous serions sans travail. Mais pour l'instant, il suffit d'écrire du code qui montre que vous savez qu'une opération supplémentaire dans la boucle n'est pas nécessaire.
#include <memory>
#include <time.h>
struct Bitmap_line
{
int blah;
unsigned int width;
Bitmap_line(unsigned int w)
{
blah = 0;
width = w;
}
};
#define TEST 0 //define 1 for faster test
int main(int argc, char* argv[])
{
unsigned int size = (4 * 1024 * 1024) / 3 * 3; //makes it divisible by 3
unsigned char* pointer = (unsigned char*)malloc(size);
memset(pointer, 0, size);
std::unique_ptr<Bitmap_line> bitmap(new Bitmap_line(size / 3));
clock_t told = clock();
#if TEST == 0
for (int iter = 0; iter < 10000; iter++)
{
unsigned char* p = pointer;
for (unsigned x = 0; x < static_cast<unsigned>(bitmap->width); ++x)
//for (unsigned x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
{
*p++ = 0xAA;
*p++ = 0xBB;
*p++ = 0xCC;
}
}
#else
for (int iter = 0; iter < 10000; iter++)
{
unsigned char* p = pointer;
unsigned x = 0;
for (const unsigned n = static_cast<unsigned>(bitmap->width) - 4; x < n; x += 4)
{
*(int64_t*)p = 0xBBAACCBBAACCBBAALL;
p += 8;
*(int32_t*)p = 0xCCBBAACC;
p += 4;
}
for (const unsigned n = static_cast<unsigned>(bitmap->width); x < n; ++x)
{
*p++ = 0xAA;
*p++ = 0xBB;
*p++ = 0xCC;
}
}
#endif
double ms = 1000.0 * double(clock() - told) / CLOCKS_PER_SEC;
printf("time %0.3f\n", ms);
{
//verify
unsigned char* p = pointer;
for (unsigned x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
{
if ((*p++ != 0xAA) || (*p++ != 0xBB) || (*p++ != 0xCC))
{
printf("EEEEEEEEEEEEERRRRORRRR!!!\n");
abort();
}
}
}
return 0;
}
Il y a deux choses à considérer.
A) À quelle fréquence l'optimisation sera-t-elle exécutée?
Si la réponse n'est pas très fréquente, comme seulement lorsqu'un utilisateur clique sur un bouton, ne vous embêtez pas si cela rend votre code illisible. Si la réponse est 1000 fois par seconde, vous voudrez probablement aller avec l'optimisation. Si c'est même un peu complexe, assurez-vous de mettre un commentaire pour expliquer ce qui se passe pour aider le prochain gars qui se présente.
B) Est-ce que cela rendra le code plus difficile à entretenir/dépanner?
Si vous ne constatez pas un énorme gain de performances, rendre votre code cryptique simplement pour enregistrer quelques tics d'horloge n'est pas une bonne idée. Beaucoup de gens vous diront que tout bon programmeur devrait pouvoir regarder le code et comprendre ce qui se passe. C'est vrai. Le problème est que dans le monde des affaires, le temps supplémentaire nécessaire pour comprendre cela coûte de l'argent. Donc, si vous pouvez le rendre plus joli à lire, faites-le. Vos amis vous en remercieront.
Cela dit, j'utiliserais personnellement l'exemple B.
La comparaison est incorrecte car les deux extraits de code
for (unsigned x = 0; x < static_cast<unsigned>(bitmap->width); ++x)
et
unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0; x<width ; ++x)
ne sont pas équivalents
Dans le premier cas, width
est dépendant et non const, et on ne peut pas supposer qu'il ne peut pas changer entre les itérations suivantes. Il ne peut donc pas être optimisé, mais doit être vérifié à chaque boucle.
Dans votre cas optimisé, une variable locale se voit attribuer la valeur de bitmap->width
à un moment donné pendant l'exécution du programme. Le compilateur peut vérifier que cela ne change pas réellement.
Avez-vous pensé au multi-threading, ou peut-être que la valeur peut dépendre de l'extérieur, de sorte que sa valeur est volatile. Comment peut-on s'attendre à ce que le compilateur comprenne toutes ces choses si vous ne le dites pas?
Le compilateur ne peut faire aussi bien que votre code le permet.
En règle générale, laissez le compilateur faire l'optimisation pour vous jusqu'à ce que vous décidiez de prendre le relais. La logique de cela n'a rien à voir avec les performances, mais plutôt avec la lisibilité humaine. Dans la grande majorité des cas, la lisibilité de votre programme est plus importante que ses performances. Vous devez viser à écrire du code plus facile à lire pour un être humain, et ne vous soucier de l'optimisation que lorsque vous êtes convaincu que les performances sont plus importantes que la maintenabilité de votre code.
Une fois que vous voyez que les performances sont importantes, vous devez exécuter un profileur sur le code pour déterminer quelles boucles sont inefficaces et les optimiser individuellement. Il peut en effet y avoir des cas où vous souhaitez faire cette optimisation (surtout si vous migrez vers C++, où les conteneurs STL s'impliquent), mais le coût en termes de lisibilité est important.
De plus, je peux penser à des situations pathologiques où cela pourrait réellement ralentir le code. Par exemple, considérons le cas où le compilateur n'a pas pu prouver que bitmap->width
a été constant tout au long du processus. En ajoutant la variable width
, vous forcez le compilateur à conserver une variable locale dans cette étendue. Si, pour une raison spécifique à la plate-forme, cette variable supplémentaire a empêché une certaine optimisation de l'espace de pile, elle peut avoir à réorganiser la façon dont elle émet des bytecodes et produire quelque chose de moins efficace.
Par exemple, sous Windows x64, on est obligé d'appeler un appel API spécial, __chkstk
dans le préambule de la fonction si la fonction utilisera plus d'une page de variables locales. Cette fonction permet aux fenêtres de gérer les pages de garde qu'elles utilisent pour étendre la pile en cas de besoin. Si votre variable supplémentaire pousse l'utilisation de la pile de moins de 1 page à au moins 1 page, votre fonction est désormais obligée d'appeler __chkstk
à chaque saisie. Si vous deviez optimiser cette boucle sur un chemin lent, vous pourriez en fait ralentir le chemin rapide plus que vous n'en avez enregistré sur le chemin lent!
Bien sûr, c'est un peu pathologique, mais le but de cet exemple est que vous pouvez réellement ralentir le compilateur. Cela montre simplement que vous devez profiler votre travail pour déterminer où vont les optimisations. En attendant, veuillez ne sacrifier en aucune façon la lisibilité pour une optimisation qui peut ou non avoir de l'importance.
Le compilateur est capable d'optimiser beaucoup de choses. Pour votre exemple, vous devriez opter pour la lisibilité, la maintenabilité et ce qui suit la norme de votre code. Pour plus d'informations sur ce qui peut être optimisé (avec GCC), voir cet article de blog .
À moins que vous ne sachiez exactement comment le compilateur optimise le code, il est préférable de faire vos propres optimisations en conservant la lisibilité et la conception du code. Pratiquement, il est difficile de vérifier le code Assembly pour chaque fonction que nous écrivons pour les nouvelles versions du compilateur.
Le compilateur ne peut pas optimiser bitmap->width
Car la valeur de width
peut être modifiée entre les itérations. Il y a quelques raisons les plus courantes:
iterator::end()
ou container::size()
il est donc difficile de prédire si elle retournera toujours le même résultat.Pour résumer (mon avis personnel) pour les endroits qui nécessitent un haut niveau d'optimisation, vous devez le faire vous-même, dans d'autres endroits, laissez-le, le compilateur peut l'optimiser ou non, s'il n'y a pas de grande différence de lisibilité du code est la cible principale.