Vermeiden Sie if-Anweisungen in einer for-Schleife?

116

Ich habe eine Klasse namens Writer, die eine Funktion writeVectorwie folgt hat:

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";
    }
}

Ich versuche, keinen doppelten Code zu haben, während ich mir immer noch Sorgen um die Leistung mache. In der Funktion if (index)überprüfe ich jede Runde meiner forSchleife, obwohl das Ergebnis immer das gleiche ist. Dies ist gegen "Sorgen um die Leistung".

Ich könnte dies leicht vermeiden, indem ich den Scheck außerhalb meiner forSchleife platziere. Ich werde jedoch jede Menge doppelten Codes erhalten:

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

Das sind also beide "schlechte" Lösungen für mich. Was ich gedacht habe, sind zwei private Funktionen, von denen eine den Index verlässt und dann die andere aufruft. Der andere übertrifft nur den Wert. Ich kann jedoch nicht herausfinden, wie ich es mit meinem Programm verwenden soll. Ich würde immer noch die ifPrüfung benötigen, um zu sehen, welches ich aufrufen soll ...

Entsprechend dem Problem scheint Polymorphismus eine richtige Lösung zu sein. Aber ich kann nicht sehen, wie ich es hier verwenden soll. Was wäre der bevorzugte Weg, um diese Art von Problem zu lösen?

Dies ist kein richtiges Programm, ich bin nur daran interessiert zu lernen, wie diese Art von Problem gelöst werden sollte.

Skamah Eins
quelle
8
@ JonathonReinhart Vielleicht möchten einige Leute Programmieren lernen und sind neugierig, wie man Probleme löst?
Skamah One
9
Ich habe diese Frage +1 gegeben. Diese Art der Optimierung ist möglicherweise nicht oft erforderlich, aber erstens kann es Teil der Antwort sein, auf diese Tatsache hinzuweisen, und zweitens sind seltene Optimierungsarten für die Programmierung immer noch von hoher Relevanz.
Jogojapan
31
Die Frage betrifft ein gutes Design, das Codeduplizierungen und komplizierte Logik innerhalb der Schleife vermeidet. Es ist eine gute Frage, die nicht abgelehnt werden muss.
Ali
5
Es ist eine interessante Frage, normalerweise lösen die Schleifentransformationsdurchläufe im Compiler dies sehr effizient. Wenn die Funktion wie diese ausreichend klein ist, kümmert sich der Inliner darum und tötet den Zweig höchstwahrscheinlich vollständig ab. Ich würde den Code lieber ändern, bis der Inliner den Code glücklich einfügt, als dies mit Vorlagen zu lösen.
Alex
5
@ JonathonReinhart: Huh? Die erste Überarbeitung der Frage ist praktisch identisch mit dieser. Dein "Warum kümmert es dich?" Kommentar ist 100% irrelevant für alle Revisionen. Was die öffentliche Rüge angeht - es sind nicht nur Sie, sondern viele Menschen hier, die dieses Problem verursachen. Wenn der Titel "Vermeiden von if-Anweisungen in einer for-Schleife" lautet , sollte es ziemlich offensichtlich sein, dass die Frage generisch ist und das Beispiel nur zur Veranschaulichung dient . Sie helfen niemandem, wenn Sie die Frage ignorieren und das OP aufgrund des von ihm verwendeten anschaulichen Beispiels dumm aussehen lassen.
user541686

Antworten:

79

Übergeben Sie den Körper der Schleife als Funktor. Es wird zur Kompilierungszeit eingefügt, keine Leistungseinbußen.

Die Idee, Unterschiede weiterzugeben, ist in der C ++ - Standardbibliothek allgegenwärtig. Es wird das Strategiemuster genannt.

Wenn Sie C ++ 11 verwenden dürfen, können Sie Folgendes tun:

#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'; });
}

Dieser Code ist nicht perfekt, aber Sie bekommen die Idee.

In altem C ++ 98 sieht es so aus:

#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;
}

Auch hier ist der Code alles andere als perfekt, aber er gibt Ihnen die Idee.

Ali
quelle
4
for(int i=0;i<100;i++){cout<<"Thank you!"<<endl;}: D Dies ist die Art von Lösung, nach der ich gesucht habe, sie funktioniert wie ein Zauber :) Sie könnten sie mit wenigen Kommentaren verbessern (hatten zuerst Probleme, sie zu verstehen), aber ich habe sie bekommen, also kein Problem :)
Skamah One
1
Ich bin froh, dass es geholfen hat! Bitte überprüfen Sie mein Update mit C ++ 11-Code, es ist im Vergleich zur C ++ 98-Version weniger aufgebläht.
Ali
3
Nitpick: Dies ist im Beispielfall von OP in Ordnung, da der Schleifenkörper so klein ist, aber wenn er größer wäre (stellen Sie sich ein Dutzend Codezeilen statt nur einer einzigen vor cout << e << "\n";), würde es immer noch einige Codeduplikationen geben.
Syam
3
Warum werden im C ++ 03-Beispiel die Strukturen und die Operatorüberladung verwendet? Warum nicht einfach zwei Funktionen erstellen und die Zeiger an sie übergeben?
Malcolm
2
@ Malcolm Inlining. Wenn es sich um Strukturen handelt, können die Funktionsaufrufe wahrscheinlich eingebunden werden. Wenn Sie einen Funktionszeiger übergeben, können diese Aufrufe wahrscheinlich nicht eingebunden werden.
Ali
40

In der Funktion führe ich die if (Index) -Prüfung für jede Runde meiner for-Schleife durch, obwohl das Ergebnis immer das gleiche ist. Dies ist gegen "Sorgen um die Leistung".

Wenn dies tatsächlich der Fall ist, hat der Verzweigungsprädiktor kein Problem bei der Vorhersage des (konstanten) Ergebnisses. Dies führt nur zu einem geringen Overhead für Fehlvorhersagen in den ersten Iterationen. In Bezug auf die Leistung besteht kein Grund zur Sorge

In diesem Fall empfehle ich, den Test aus Gründen der Klarheit in der Schleife zu halten.

Marc Claesen
quelle
3
Es ist nur ein Beispiel, ich bin hier, um zu erfahren, wie diese Art von Problem gelöst werden soll. Ich bin nur neugierig und erstelle nicht einmal ein richtiges Programm. Hätte es in der Frage erwähnen sollen.
Skamah One
40
Denken Sie in diesem Fall daran, dass vorzeitige Optimierung die Wurzel allen Übels ist . Konzentrieren Sie sich beim Programmieren immer auf die Lesbarkeit des Codes und stellen Sie sicher, dass andere verstehen, was Sie tun möchten. Berücksichtigen Sie Mikrooptimierungen und verschiedene Hacks erst, nachdem Sie Ihr Programm profiliert und Hotspots identifiziert haben . Sie sollten niemals Optimierungen in Betracht ziehen, ohne die Notwendigkeit dafür festzustellen. Sehr oft sind Leistungsprobleme nicht dort, wo Sie sie erwarten.
Marc Claesen
3
Und in diesem speziellen Beispiel (ok, verstanden, dies ist nur ein Beispiel) ist es sehr wahrscheinlich, dass die Zeit, die für die Schleifensteuerung aufgewendet wird, und wenn der Test neben der Zeit, die für die E / A aufgewendet wird, nahezu unsichtbar ist. Dies ist bei C ++ häufig ein Problem: Wählen Sie zwischen Lesbarkeit auf Kosten der Wartung und (hypothetischer) Effizienz.
kriss
8
Sie gehen davon aus, dass der Code auf einem Prozessor ausgeführt wird, der zunächst eine Verzweigungsvorhersage hat. Die meisten Systeme, auf denen C ++ ausgeführt wird, tun dies nicht. (Obwohl wahrscheinlich die Mehrheit der Systeme mit einem nützlichen std::couttun)
Ben Voigt
2
-1. Ja, die Verzweigungsvorhersage funktioniert hier gut. Ja, die Bedingung wird möglicherweise vom Compiler außerhalb der Schleife angehoben. Ja, POITROAE. Aber Zweige innerhalb einer Schleife sind eine gefährliche Sache, die häufig Auswirkungen auf die Leistung hat, und ich denke nicht, dass es ein guter Rat ist, diese zu entlassen, indem man nur "Verzweigungsvorhersage" sagt, wenn sich jemand wirklich um die Leistung kümmert. Das bemerkenswerteste Beispiel ist, dass ein Vektorisierungs-Compiler eine Prädikation benötigt , um dies zu handhaben, wodurch weniger effizienter Code erzeugt wird als für schleifenlose Schleifen.
Eiche
35

Um Alis Antwort zu erweitern, die vollkommen korrekt ist, aber dennoch Code dupliziert (Teil des Schleifenkörpers, dies ist bei Verwendung des Strategiemusters leider kaum zu vermeiden) ...

Zugegeben, in diesem speziellen Fall ist die Codeduplizierung nicht viel, aber es gibt eine Möglichkeit, sie noch weiter zu reduzieren. Dies ist praktisch, wenn der Funktionskörper größer als nur ein paar Anweisungen ist .

Der Schlüssel besteht darin, die Fähigkeit des Compilers zu nutzen, eine konstante Faltung / Eliminierung von totem Code durchzuführen . Wir können dies tun, indem wir den Laufzeitwert von manuell einem Wert indexzur Kompilierungszeit zuordnen (dies ist einfach, wenn nur eine begrenzte Anzahl von Fällen vorliegt - in diesem Fall zwei) und ein beim Kompilieren bekanntes Vorlagenargument ohne Typ verwenden -Zeit:

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);
}

Auf diese Weise erhalten wir kompilierten Code, der Ihrem zweiten Codebeispiel (außen if/ innen for) entspricht, ohne den Code selbst zu duplizieren. Jetzt können wir die Vorlagenversion writeVectorso kompliziert gestalten, wie wir möchten. Es muss immer nur ein einziger Code verwaltet werden.

Beachten Sie, wie die Vorlagenversion (die eine Kompilierungszeitkonstante in Form eines Vorlagenarguments ohne Typ verwendet) und die Nichtvorlagenversion (die eine Laufzeitvariable als Funktionsargument verwendet) überladen sind. Auf diese Weise können Sie je nach Ihren Anforderungen die relevanteste Version auswählen und in beiden Fällen eine ähnliche, leicht zu merkende Syntax verwenden:

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
Syam
quelle
2
Beachten Sie bitte, dass Sie die Codeduplizierung auf Kosten einer komplizierteren Logik in der Schleife entfernt haben. Ich sehe es weder besser noch schlechter als das, was ich für dieses einfache Beispiel vorgeschlagen habe. +1 sowieso!
Ali
1
Ich mag Ihren Vorschlag, weil er eine weitere mögliche Optimierung zeigt. Es ist sehr wahrscheinlich, dass der Index von Anfang an eine Vorlagenkonstante ist. In diesem Fall könnte es durch den Aufrufer von writeVector durch eine Laufzeitkonstante ersetzt und writeVector in eine Vorlage geändert werden. Vermeiden Sie weitere Änderungen am Originalcode.
kriss
1
@kriss: Eigentlich hat meine vorherige Lösung das schon erlaubt, wenn du doWriteVectordirekt anrufst, aber ich stimme zu, dass der Name unglücklich war. Ich habe es gerade so geändert, dass es zwei überladene writeVectorFunktionen hat (eine Vorlage, die andere eine reguläre Funktion), damit das Ergebnis homogener ist. Danke für den Vorschlag. ;)
Syam
4
IMO das ist die beste Antwort. +1
user541686
1
@Mehrdad Außer, dass es die ursprüngliche Frage nicht beantwortet. Vermeiden, wenn Anweisung in einer for-Schleife? Es wird jedoch beantwortet, wie die Leistungseinbußen vermieden werden können. Für die "Vervielfältigung" wäre ein realistischeres Beispiel mit Anwendungsfällen erforderlich, um zu sehen, wie es am besten herausgerechnet wird. Wie ich bereits sagte, habe ich diese Antwort positiv bewertet.
Ali
0

In den meisten Fällen ist Ihr Code bereits gut für die Leistung und Lesbarkeit. Ein guter Compiler kann Schleifeninvarianten erkennen und entsprechende Optimierungen vornehmen. Betrachten Sie das folgende Beispiel, das Ihrem Code sehr nahe kommt:

#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));
}

Ich benutze die folgende Befehlszeile, um es zu kompilieren:

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

Dann lassen Sie uns die Assembly entleeren:

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

Die Ergebnisanordnung von write_vectorist:

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)

Wir können sehen, dass wir zu Beginn der Funktion den Wert überprüfen und zu einer von zwei möglichen Schleifen springen:

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

Dies funktioniert natürlich nur, wenn ein Compiler erkennen kann, dass eine Bedingung tatsächlich invariant ist. Normalerweise funktioniert es perfekt für Flags und einfache Inline-Funktionen. Wenn die Bedingung jedoch "komplex" ist, sollten Sie Ansätze aus anderen Antworten verwenden.

ivaigult
quelle