J'ai revendiqué à un collègue qui if (i < input.size() - 1) print(0);
serait optimisé dans cette boucle afin que ce input.size()
ne soit pas lu à chaque itération, mais il s'avère que ce n'est pas le cas!
void print(int x) {
std::cout << x << std::endl;
}
void print_list(const std::vector<int>& input) {
int i = 0;
for (size_t i = 0; i < input.size(); i++) {
print(input[i]);
if (i < input.size() - 1) print(0);
}
}
Selon l' explorateur du compilateur avec les options gcc, -O3 -fno-exceptions
nous lisons actuellement input.size()
chaque itération et utilisons lea
pour effectuer une soustraction!
movq 0(%rbp), %rdx
movq 8(%rbp), %rax
subq %rdx, %rax
sarq $2, %rax
leaq -1(%rax), %rcx
cmpq %rbx, %rcx
ja .L35
addq $1, %rbx
Fait intéressant, dans Rust, cette optimisation se produit. Il semble que i
soit remplacé par une variable j
qui est décrémentée à chaque itération, et le test i < input.size() - 1
est remplacé par quelque chose comme j > 0
.
fn print(x: i32) {
println!("{}", x);
}
pub fn print_list(xs: &Vec<i32>) {
for (i, x) in xs.iter().enumerate() {
print(*x);
if i < xs.len() - 1 {
print(0);
}
}
}
Dans l' explorateur du compilateur, l'assembly correspondant ressemble à ceci:
cmpq %r12, %rbx
jae .LBB0_4
J'ai vérifié et je suis sûr que r12
c'est xs.len() - 1
et rbx
c'est le comptoir. Plus tôt, il y a un add
pour rbx
et un mov
extérieur dans la boucle r12
.
Pourquoi est-ce? Il semble que si GCC est en mesure d'aligner le size()
et operator[]
comme il l'a fait, il devrait pouvoir savoir que size()
cela ne change pas. Mais peut-être que l'optimiseur de GCC juge qu'il ne vaut pas la peine de le retirer dans une variable? Ou peut-être y a-t-il un autre effet secondaire possible qui rendrait cela dangereux - quelqu'un le sait-il?
println
probablement une méthode complexe, le compilateur peut avoir du mal à prouver queprintln
cela ne mute pas le vecteur.cout.operator<<()
. Le compilateur ne sait pas que cette fonction de boîte noire n'obtient pas de référence àstd::vector
partir d'un global.println
ouoperator<<
est la clé.Réponses:
L'appel de fonction non en ligne à
cout.operator<<(int)
est une boîte noire pour l'optimiseur (car la bibliothèque est simplement écrite en C ++ et tout ce que l'optimiseur voit est un prototype; voir la discussion dans les commentaires). Il doit supposer que toute mémoire qui pourrait éventuellement être pointée par une variable globale a été modifiée.(Ou l'
std::endl
appel. BTW, pourquoi forcer un flush de cout à ce point au lieu d'imprimer simplement un'\n'
?)par exemple pour tout ce qu'il sait,
std::vector<int> &input
est une référence à une variable globale, et l'un de ces appels de fonction modifie cette variable globale . (Ou il y a un globalvector<int> *ptr
quelque part, ou il y a une fonction qui renvoie un pointeur vers unstatic vector<int>
dans une autre unité de compilation, ou d'une autre manière qu'une fonction pourrait obtenir une référence à ce vecteur sans que nous lui transmettions une référence.Si vous aviez une variable locale dont l'adresse n'avait jamais été prise, le compilateur pouvait supposer que les appels de fonction non en ligne ne pouvaient pas la muter. Parce qu'il n'y aurait aucun moyen pour une variable globale de contenir un pointeur sur cet objet. ( C'est ce qu'on appelle l' analyse d'échappement ). C'est pourquoi le compilateur peut conserver
size_t i
un registre à travers les appels de fonction. (int i
peut simplement être optimisé car il est masqué parsize_t i
et non utilisé autrement).Il pourrait faire de même avec un
vector
pointeur local (c'est-à-dire pour les pointeurs base, end_size et end_capacity.)ISO C99 a une solution à ce problème:
int *restrict foo
. De nombreux C ++ prennent en charge compilesint *__restrict foo
promettre que la mémoire pointée parfoo
est uniquement accessible via ce pointeur. Le plus souvent utile dans les fonctions qui prennent 2 tableaux, et vous voulez promettre au compilateur qu'elles ne se chevauchent pas. Il peut donc auto-vectoriser sans générer de code pour vérifier cela et exécuter une boucle de secours.Le PO commente:
Cela explique pourquoi Rust peut effectuer cette optimisation mais pas C ++.
Optimiser votre C ++
Évidemment, vous devez utiliser
auto size = input.size();
une fois en haut de votre fonction pour que le compilateur sache que c'est un invariant de boucle. Les implémentations C ++ ne résolvent pas ce problème pour vous, vous devez donc le faire vous-même.Vous devrez peut-être également
const int *data = input.data();
hisser des charges du pointeur de données dustd::vector<int>
"bloc de contrôle". Il est regrettable que l'optimisation puisse nécessiter des changements de source très non idiomatiques.Rust est un langage beaucoup plus moderne, conçu après que les développeurs de compilateurs ont appris ce qui était possible dans la pratique pour les compilateurs. Il montre vraiment d'autres façons, aussi, y compris l' exposition portably certains des processeurs cool stuff peuvent faire via
i32.count_ones
, rotate, balayage bits, etc. Il est vraiment stupide que l' ISO C ++ ne fonctionne toujours pas exposer l' une de ces portably, à l' exceptionstd::bitset::count()
.la source
operator<<
pour ces types d'opérandes; donc en Standard C ++ ce n'est pas une boîte noire et le compilateur peut supposer qu'il fait ce que dit la documentation. Peut-être qu'ils veulent soutenir les développeurs de bibliothèques en ajoutant un comportement non standard ...cout
permet à un objet d'une classe définie par l'utilisateur dérivé destreambuf
d'être associé au flux utilisantcout.rdbuf
. De même, un objet dérivé deostream
peut être associé àcout.tie
.this
pointeur est implicitement passé. Cela pourrait se produire dans la pratique dès le constructeur. Considérez cette boucle simple - je n'ai vérifié que la boucle principale de gcc (deL34:
àjne L34
), mais elle se comporte définitivement comme si les membres du vecteur s'étaient échappés (en les chargeant de la mémoire à chaque itération).