Éviter l'instruction if dans une boucle for?


116

J'ai une classe appelée Writerqui a une fonction writeVectorcomme ceci:

void Drawer::writeVector(vector<T> vec, bool index=true)
{
    for (unsigned int i = 0; i < vec.size(); i++) {
        if (index) {
            cout << i << "\t";
        }
        cout << vec[i] << "\n";
    }
}

J'essaie de ne pas avoir de code en double, tout en me souciant des performances. Dans la fonction, je if (index)vérifie à chaque tour de ma forboucle, même si le résultat est toujours le même. C'est contre "s'inquiéter de la performance".

Je pourrais facilement éviter cela en plaçant le chèque en dehors de ma forboucle. Cependant, j'obtiendrai beaucoup de code en double:

void Drawer::writeVector(...)
{
    if (index) {
        for (...) {
            cout << i << "\t" << vec[i] << "\n";
        }
    }
    else {
        for (...) {
            cout << vec[i] << "\n";
        }
    }
}

Ce sont donc deux «mauvaises» solutions pour moi. Ce que j'ai pensé, ce sont deux fonctions privées, l'une d'elles sort de l'index et appelle l'autre. L'autre dépasse seulement la valeur. Cependant, je ne peux pas comprendre comment l'utiliser avec mon programme, j'aurais quand même besoin de la ifvérification pour voir lequel appeler ...

Selon le problème, le polymorphisme semble être une solution correcte. Mais je ne vois pas comment l'utiliser ici. Quelle serait la meilleure façon de résoudre ce genre de problème?

Ce n'est pas un vrai programme, je suis juste intéressé à apprendre comment ce genre de problème devrait être résolu.


8
@JonathonReinhart Peut-être que certaines personnes veulent apprendre la programmation et sont curieuses de savoir comment résoudre des problèmes?
Skamah One

9
J'ai donné +1 à cette question. Ce type d'optimisation peut ne pas être souvent nécessaire, mais d'une part, le fait de souligner que ce fait peut faire partie de la réponse, et d'autre part, de rares types d'optimisation sont toujours très pertinents pour la programmation.
jogojapan

31
La question concerne une bonne conception qui évite la duplication de code et la logique compliquée à l'intérieur de la boucle. C'est une bonne question, pas besoin de la rejeter.
Ali

5
C'est une question intéressante, généralement les passes de transformation en boucle dans le compilateur résoudront cela très efficacement. si la fonction est suffisamment petite comme celle-ci, l'inliner s'en occupera et tuera probablement complètement la branche. Je préfère changer le code jusqu'à ce que l'inliner intègre joyeusement le code plutôt que de résoudre ce problème avec des modèles.
Alex

5
@JonathonReinhart: Hein? La première révision de la question est pratiquement identique à celle-ci. Votre "pourquoi vous souciez-vous?" le commentaire est à 100% sans rapport avec toutes les révisions. Quant à vous réprimander publiquement - ce n'est pas seulement vous, ce sont beaucoup de gens ici qui causent ce problème. Lorsque le titre est "éviter les instructions if dans une boucle for" , il devrait être assez évident que la question est générique, et l'exemple est juste pour l'illustration . Vous n'aidez personne lorsque vous ignorez la question et donnez à l'OP un aspect stupide à cause de l'exemple illustratif particulier qu'il a utilisé.
user541686

Réponses:


79

Passez le corps de la boucle en tant que foncteur. Il est intégré au moment de la compilation, aucune pénalité de performance.

L'idée de transmettre ce qui varie est omniprésente dans la bibliothèque standard C ++. Cela s'appelle le modèle de stratégie.

Si vous êtes autorisé à utiliser C ++ 11, vous pouvez faire quelque chose comme ceci:

#include <iostream>
#include <set>
#include <vector>

template <typename Container, typename Functor, typename Index = std::size_t>
void for_each_indexed(const Container& c, Functor f, Index index = 0) {

    for (const auto& e : c)
        f(index++, e);
}

int main() {

    using namespace std;

    set<char> s{'b', 'a', 'c'};

    // indices starting at 1 instead of 0
    for_each_indexed(s, [](size_t i, char e) { cout<<i<<'\t'<<e<<'\n'; }, 1u);

    cout << "-----" << endl;

    vector<int> v{77, 88, 99};

    // without index
    for_each_indexed(v, [](size_t , int e) { cout<<e<<'\n'; });
}

Ce code n'est pas parfait mais vous voyez l'idée.

Dans l'ancien C ++ 98, cela ressemble à ceci:

#include <iostream>
#include <vector>
using namespace std;

struct with_index {
  void operator()(ostream& out, vector<int>::size_type i, int e) {
    out << i << '\t' << e << '\n';
  }
};

struct without_index {
  void operator()(ostream& out, vector<int>::size_type i, int e) {
    out << e << '\n';
  }
};


template <typename Func>
void writeVector(const vector<int>& v, Func f) {
  for (vector<int>::size_type i=0; i<v.size(); ++i) {
    f(cout, i, v[i]);
  }
}

int main() {

  vector<int> v;
  v.push_back(77);
  v.push_back(88);
  v.push_back(99);

  writeVector(v, with_index());

  cout << "-----" << endl;

  writeVector(v, without_index());

  return 0;
}

Encore une fois, le code est loin d'être parfait mais il vous donne l'idée.


4
for(int i=0;i<100;i++){cout<<"Thank you!"<<endl;}: D C'est le genre de solution que je cherchais, ça marche comme un charme :) Vous pourriez l'améliorer avec quelques commentaires (j'ai eu du mal à la comprendre au début), mais je l'ai donc pas de problème :)
Skamah One

1
Je suis content que cela ait aidé! Veuillez vérifier ma mise à jour avec du code C ++ 11, elle est moins gonflée par rapport à la version C ++ 98.
Ali

3
Nitpick: c'est bien dans le cas de l'exemple d'OP parce que le corps de la boucle est si petit, mais s'il était plus grand (imaginez une douzaine de lignes de code au lieu d'une seule cout << e << "\n";), il y aurait encore une certaine duplication de code.
syam

3
Pourquoi les structures et la surcharge d'opérateurs sont-elles utilisées dans l'exemple C ++ 03? Pourquoi ne pas simplement créer deux fonctions et leur transmettre les pointeurs?
Malcolm

2
@Malcolm Inlining. S'il s'agit de structures, il y a de fortes chances que les appels de fonction puissent être insérés. Si vous passez un pointeur de fonction, il est probable que ces appels ne puissent pas être insérés.
Ali

40

Dans la fonction, je fais la vérification if (index) à chaque tour de ma boucle for, même si le résultat est toujours le même. C'est contre "s'inquiéter de la performance".

Si tel est effectivement le cas, le prédicteur de branche n'aura aucun problème à prédire le résultat (constant). En tant que tel, cela n'entraînera qu'une légère surcharge pour les erreurs de prédiction dans les premières itérations. Il n'y a rien à craindre en termes de performances

Dans ce cas, je préconise de garder le test à l'intérieur de la boucle pour plus de clarté.


3
C'est juste un exemple, je suis ici pour savoir comment résoudre ce genre de problème. Je suis juste curieux de ne même pas créer un vrai programme. J'aurais dû le mentionner dans la question.
Skamah One

40
Dans ce cas, gardez à l'esprit que l' optimisation prématurée est la racine de tout mal . Lors de la programmation, concentrez-vous toujours sur la lisibilité du code et assurez-vous que les autres comprennent ce que vous essayez de faire. Ne considérez les micro-optimisations et divers hacks qu'après avoir profilé votre programme et identifié les points chauds . Vous ne devriez jamais envisager des optimisations sans en établir la nécessité. Très souvent, les problèmes de performances ne sont pas là où vous vous attendez.
Marc Claesen

3
Et dans cet exemple particulier (ok, compris, ce n'est qu'un exemple), il est très probable que le temps passé pour le contrôle de boucle et si le test soit presque invisible à côté du temps passé pour les E / S. C'est souvent un problème avec C ++: choisir entre la lisibilité au prix de la maintenance et l'efficacité (hypothétique).
kriss

8
Vous supposez que le code s'exécute sur un processeur qui a une prédiction de branche pour commencer. La majorité des systèmes exécutant C ++ ne le font pas. (Bien que, probablement la majorité des systèmes avec un std::coutdo utile )
Ben Voigt

2
-1. Oui, la prédiction de branche fonctionnera bien ici. Oui, la condition peut en fait être hissée hors de la boucle par le compilateur. Oui, POITROAE. Mais les branches dans une boucle sont une chose dangereuse qui a souvent un impact sur les performances, et je ne pense pas que les rejeter en disant simplement "prédiction de branche" est un bon conseil si quelqu'un se soucie vraiment des performances. L'exemple le plus notable est qu'un compilateur de vectorisation aura besoin d'une prédication pour gérer cela, produisant du code moins efficace que pour les boucles sans branche.
Oak

35

Pour développer la réponse d'Ali, qui est parfaitement correcte mais duplique quand même du code (une partie du corps de la boucle, c'est malheureusement difficilement évitable lors de l'utilisation du modèle de stratégie) ...

Certes, dans ce cas particulier, la duplication de code n'est pas beaucoup, mais il existe un moyen de la réduire encore plus, ce qui est pratique si le corps de la fonction est plus grand que quelques instructions .

La clé est d'utiliser la capacité du compilateur à effectuer une élimination constante de pliage / code mort . Nous pouvons le faire en mappant manuellement la valeur d'exécution de indexà une valeur au moment de la compilation (facile à faire lorsqu'il n'y a qu'un nombre limité de cas - deux dans ce cas) et en utilisant un argument de modèle non-type qui est connu à la compilation -temps:

template<bool index = true>
//                  ^^^^^^ note: the default value is now part of the template version
//                         see below to understand why
void writeVector(const vector<int>& vec) {
    for (size_t i = 0; i < vec.size(); ++i) {
        if (index) { // compile-time constant: this test will always be eliminated
            cout << i << "\t"; // this will only be kept if "index" is true
        }
        cout << vec[i] << "\n";
    }
}

void writeVector(const vector<int>& vec, bool index)
//                                            ^^^^^ note: no more default value, otherwise
//                                            it would clash with the template overload
{
    if (index) // runtime decision
        writeVector<true>(vec);
        //          ^^^^ map it to a compile-time constant
    else
        writeVector<false>(vec);
}

De cette façon, nous nous retrouvons avec du code compilé qui est équivalent à votre deuxième exemple de code (externe if/ interne for) mais sans dupliquer le code nous-mêmes. Maintenant, nous pouvons rendre la version du modèle writeVectoraussi compliquée que nous le voulons, il y aura toujours un seul morceau de code à maintenir.

Notez comment la version du modèle (qui prend une constante de compilation sous la forme d'un argument de modèle non-type) et la version non-modèle (qui prend une variable d'exécution comme argument de fonction) sont surchargées. Cela vous permet de choisir la version la plus pertinente en fonction de vos besoins, ayant une syntaxe assez similaire et facile à retenir dans les deux cas:

writeVector<true>(vec);   // you already know at compile-time which version you want
                          // no need to go through the non-template runtime dispatching

writeVector(vec, index);  // you don't know at compile-time what "index" will be
                          // so you have to use the non-template runtime dispatching

writeVector(vec);         // you can even use your previous syntax using a default argument
                          // it will call the template overload directly

2
Veuillez garder à l'esprit que vous avez supprimé la duplication de code au détriment de la logique à l'intérieur de la boucle plus compliquée. Je ne vois ni mieux ni pire que ce que j'ai proposé pour cet exemple simple. +1 de toute façon!
Ali

1
J'aime votre proposition car elle montre une autre optimisation possible. Il est très possible que l'index puisse être une constante de modèle depuis le début. Dans ce cas, il pourrait être remplacé par une constante d'exécution par l'appelant de writeVector et writeVector changé en un modèle. Éviter toute autre modification du code d'origine.
kriss

1
@kriss: En fait, ma solution précédente permettait déjà que si vous appeliez doWriteVectordirectement mais je suis d'accord que le nom était malheureux. Je viens de le changer pour avoir deux writeVectorfonctions surchargées (un modèle, l'autre une fonction régulière) afin que le résultat soit plus homogène. Merci pour la suggestion. ;)
syam

4
OMI c'est la meilleure réponse. +1
user541686

1
@Mehrdad Sauf qu'il ne répond pas à la question d'origine. Éviter l'instruction if dans une boucle for? Il explique cependant comment éviter la pénalité de performance. En ce qui concerne la "duplication", un exemple plus réaliste avec des cas d'utilisation serait nécessaire pour voir comment elle est au mieux prise en compte. Comme je l'ai déjà dit, j'ai voté pour cette réponse.
Ali

0

Dans la plupart des cas, votre code est déjà bon pour les performances et la lisibilité. Un bon compilateur est capable de détecter les invariants de boucle et d'effectuer les optimisations appropriées. Prenons l'exemple suivant qui est très proche de votre code:

#include <cstdio>
#include <iterator>

void write_vector(int* begin, int* end, bool print_index = false) {
    unsigned index = 0;
    for(int* it = begin; it != end; ++it) {
        if (print_index) {
            std::printf("%d: %d\n", index, *it);
        } else {
            std::printf("%d\n", *it);
        }
        ++index;
    }
}

int my_vector[] = {
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
};


int main(int argc, char** argv) {
    write_vector(std::begin(my_vector), std::end(my_vector));
}

J'utilise la ligne de commande suivante pour le compiler:

g++ --version
g++ (GCC) 4.9.1
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
g++ -O3 -std=c++11 main.cpp

Ensuite, vidons l'assemblage:

objdump -d a.out | c++filt > main.s

L'assemblage de résultat de write_vectorest:

00000000004005c0 <write_vector(int*, int*, bool)>:
  4005c0:   48 39 f7                cmp    %rsi,%rdi
  4005c3:   41 54                   push   %r12
  4005c5:   49 89 f4                mov    %rsi,%r12
  4005c8:   55                      push   %rbp
  4005c9:   53                      push   %rbx
  4005ca:   48 89 fb                mov    %rdi,%rbx
  4005cd:   74 25                   je     4005f4 <write_vector(int*, int*, bool)+0x34>
  4005cf:   84 d2                   test   %dl,%dl
  4005d1:   74 2d                   je     400600 <write_vector(int*, int*, bool)+0x40>
  4005d3:   31 ed                   xor    %ebp,%ebp
  4005d5:   0f 1f 00                nopl   (%rax)
  4005d8:   8b 13                   mov    (%rbx),%edx
  4005da:   89 ee                   mov    %ebp,%esi
  4005dc:   31 c0                   xor    %eax,%eax
  4005de:   bf a4 06 40 00          mov    $0x4006a4,%edi
  4005e3:   48 83 c3 04             add    $0x4,%rbx
  4005e7:   83 c5 01                add    $0x1,%ebp
  4005ea:   e8 81 fe ff ff          callq  400470 <printf@plt>
  4005ef:   49 39 dc                cmp    %rbx,%r12
  4005f2:   75 e4                   jne    4005d8 <write_vector(int*, int*, bool)+0x18>
  4005f4:   5b                      pop    %rbx
  4005f5:   5d                      pop    %rbp
  4005f6:   41 5c                   pop    %r12
  4005f8:   c3                      retq   
  4005f9:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)
  400600:   8b 33                   mov    (%rbx),%esi
  400602:   31 c0                   xor    %eax,%eax
  400604:   bf a8 06 40 00          mov    $0x4006a8,%edi
  400609:   48 83 c3 04             add    $0x4,%rbx
  40060d:   e8 5e fe ff ff          callq  400470 <printf@plt>
  400612:   49 39 dc                cmp    %rbx,%r12
  400615:   75 e9                   jne    400600 <write_vector(int*, int*, bool)+0x40>
  400617:   eb db                   jmp    4005f4 <write_vector(int*, int*, bool)+0x34>
  400619:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)

Nous pouvons voir qu'au début de la fonction, nous vérifions la valeur et sautons à l'une des deux boucles possibles:

  4005cf:   84 d2                   test   %dl,%dl
  4005d1:   74 2d                   je     400600 <write_vector(int*, int*, bool)+0x40>

Bien sûr, cela ne fonctionne que si un compilateur est capable de détecter qu'une condition est un invariant réel. Habituellement, cela fonctionne parfaitement pour les indicateurs et les fonctions en ligne simples. Mais si la condition est «complexe», envisagez d'utiliser des approches d'autres réponses.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.