Unikasz instrukcji if wewnątrz pętli for?

116

Mam klasę o nazwie, Writerktóra ma taką funkcję writeVector:

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

Staram się nie mieć duplikatu kodu, jednocześnie martwiąc się o wydajność. W funkcji if (index)sprawdzam każdą rundę mojej for-loop, mimo że wynik jest zawsze taki sam. To jest przeciwko „martwieniu się o wydajność”.

Mogę łatwo tego uniknąć, umieszczając czek poza my for-loop. Jednak otrzymam mnóstwo zduplikowanego kodu:

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

Są to więc dla mnie „złe” rozwiązania. To, o czym myślałem, to dwie funkcje prywatne, jedna z nich wychodzi poza indeks, a następnie wywołuje drugą. Drugi tylko przekracza wartość. Jednak nie mogę dowiedzieć się, jak go używać z moim programem, nadal potrzebowałbym ifsprawdzenia, aby zobaczyć, do którego zadzwonić ...

Zgodnie z problemem polimorfizm wydaje się być właściwym rozwiązaniem. Ale nie widzę, jak mam go tutaj używać. Jaki byłby preferowany sposób rozwiązania tego rodzaju problemu?

To nie jest prawdziwy program, interesuje mnie tylko dowiedzieć się, jak rozwiązać tego rodzaju problem.

Skamah One
źródło
8
@JonathonReinhart Może niektórzy chcą nauczyć się programowania i są ciekawi, jak rozwiązywać problemy?
Skamah One
9
Dałem temu pytaniu +1. Ten rodzaj optymalizacji często nie jest konieczny, ale po pierwsze, zwrócenie uwagi na ten fakt może być częścią odpowiedzi, a po drugie, rzadkie typy optymalizacji są nadal bardzo istotne dla programowania.
jogojapan
31
Pytanie dotyczy dobrego projektu, który pozwala uniknąć powielania kodu i skomplikowanej logiki wewnątrz pętli. To dobre pytanie, nie trzeba go odrzucać.
Ali
5
To interesujące pytanie, zwykle transformacja pętli w kompilatorze rozwiązuje to bardzo skutecznie. jeśli funkcja jest wystarczająco mała, jak ta, inliner zajmie się tym i najprawdopodobniej całkowicie zabije gałąź. Wolałbym zmienić kod, dopóki inliner z radością nie wstawi kodu, niż rozwiązuje to za pomocą szablonów.
Alex
5
@JonathonReinhart: Hę? Pierwsza wersja pytania jest praktycznie identyczna z tą. Twoje "dlaczego cię to obchodzi?" komentarz jest w 100% nieistotny dla wszystkich wersji. Jeśli chodzi o upominanie cię publicznie - to nie tylko ty, to wielu ludzi powoduje ten problem. Gdy w tytule chodzi o „unikanie instrukcji if wewnątrz pętli for” , powinno być całkiem oczywiste, że pytanie jest ogólne, a przykład służy tylko do ilustracji . Nie pomagasz nikomu, gdy ignorujesz pytanie i sprawiasz, że PO wygląda głupio z powodu konkretnego ilustracyjnego przykładu, którego użył.
user541686

Odpowiedzi:

79

Podaj treść pętli jako funktor. Jest wstawiany w czasie kompilacji, bez spadku wydajności.

Idea przekazywania tego, co się zmienia, jest wszechobecna w bibliotece standardowej C ++. Nazywa się to wzorcem strategii.

Jeśli możesz używać C ++ 11, możesz zrobić coś takiego:

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

Ten kod nie jest doskonały, ale masz pomysł.

W starym C ++ 98 wygląda to tak:

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

Ponownie, kod jest daleki od doskonałości, ale daje ci pomysł.

Ali
źródło
4
for(int i=0;i<100;i++){cout<<"Thank you!"<<endl;}: D To jest rozwiązanie, którego szukałem, działa jak urok :) Można to poprawić kilkoma komentarzami (na początku miałem problemy ze zrozumieniem), ale dostałem, więc nie ma problemu :)
Skamah One
1
Cieszę się, że to pomogło! Proszę sprawdzić moją aktualizację z kodem C ++ 11, jest mniej rozdęta w porównaniu z wersją C ++ 98.
Ali
3
Nitpick: jest to w porządku w przykładzie OP, ponieważ treść pętli jest tak mała, ale gdyby była większa (wyobraź sobie kilkanaście wierszy kodu zamiast tylko jednego cout << e << "\n";), nadal występowałoby pewne powielenie kodu.
syam
3
Dlaczego struktury i przeciążenia operatorów są używane w przykładzie C ++ 03? Dlaczego po prostu nie utworzyć dwóch funkcji i nie przekazać im wskazówek?
Malcolm
2
@Malcolm Inlining. Jeśli są to struktury, są szanse, że wywołania funkcji mogą być wbudowane. Jeśli przekażesz wskaźnik funkcji, są szanse, że te wywołania nie mogą być wstawione.
Ali
40

W funkcji sprawdzam if (index) w każdej rundzie mojej pętli for, mimo że wynik jest zawsze taki sam. To jest przeciwko „martwieniu się o wydajność”.

Jeśli tak jest w rzeczywistości, predyktor gałęzi nie będzie miał problemu z przewidywaniem (stałego) wyniku. W związku z tym spowoduje to jedynie niewielkie obciążenie za błędne przewidywania w pierwszych kilku iteracjach. Nie ma się czym martwić, jeśli chodzi o wydajność

W tym przypadku zalecam trzymanie testu w pętli dla przejrzystości.

Marc Claesen
źródło
3
To tylko przykład, jestem tutaj, aby dowiedzieć się, jak rozwiązać tego rodzaju problem. Jestem po prostu ciekawy, nawet nie tworzę prawdziwego programu. Powinienem był wspomnieć o tym w pytaniu.
Skamah One
40
W takim przypadku należy pamiętać, że źródłem wszelkiego zła jest przedwczesna optymalizacja . Podczas programowania zawsze skupiaj się na czytelności kodu i upewnij się, że inni rozumieją, co próbujesz zrobić. Rozważ tylko mikro-optymalizacje i różne hacki po profilowaniu programu i zidentyfikowaniu hotspotów . Nigdy nie należy rozważać optymalizacji bez określenia ich potrzeby. Bardzo często problemy z wydajnością nie są tam, gdzie się spodziewasz.
Marc Claesen
3
W tym konkretnym przykładzie (ok, rozumiem, to tylko przykład) jest bardzo prawdopodobne, że czas spędzony na sterowaniu pętlą i czy test jest prawie niewidoczny poza czasem spędzonym na IO. Jest to często problem z C ++: wybór między czytelnością kosztem utrzymania a (hipotetyczną) wydajnością.
kriss
8
Zakładasz, że kod działa na procesorze, który ma na początku przewidywanie rozgałęzień. Większość systemów z C ++ nie. (Chociaż, prawdopodobnie większość systemów z przydatnym std::coutdo)
Ben Voigt
2
-1. Tak, tutaj dobrze sprawdzi się przewidywanie gałęzi. Tak, warunek może faktycznie zostać podniesiony poza pętlę przez kompilator. Tak, POITROAE. Ale gałęzie w pętli niebezpieczną rzeczą, która często ma wpływ na wydajność, i nie sądzę, aby odrzucanie ich po prostu mówiąc „przewidywanie gałęzi” to dobra rada, jeśli komuś naprawdę zależy na wydajności. Najbardziej godnym uwagi przykładem jest to, że kompilator wektoryzujący będzie potrzebował predykcji, aby sobie z tym poradzić, tworząc mniej wydajny kod niż w przypadku pętli bez gałęzi.
Oak
35

Aby rozwinąć odpowiedź Alego, która jest całkowicie poprawna, ale wciąż powiela jakiś kod (część treści pętli, jest to niestety trudne do uniknięcia przy użyciu wzorca strategii) ...

W tym konkretnym przypadku powielanie kodu to niewiele, ale istnieje sposób, aby go jeszcze bardziej zmniejszyć, co jest przydatne, jeśli treść funkcji jest większa niż tylko kilka instrukcji .

Kluczem jest wykorzystanie zdolności kompilatora do ciągłej eliminacji zwijanego / martwego kodu . Możemy to zrobić, ręcznie mapując wartość czasu wykonywania na wartość czasu indexkompilacji (łatwe do zrobienia, gdy jest tylko ograniczona liczba przypadków - w tym przypadku dwa) i używając argumentu szablonu innego niż typ, który jest znany podczas kompilacji -czas:

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

W ten sposób otrzymujemy skompilowany kod, który jest odpowiednikiem twojego drugiego przykładu kodu (zewnętrzny if/ wewnętrzny for), ale bez jego duplikowania. Teraz możemy uczynić wersję szablonu writeVectortak skomplikowaną, jak chcemy, zawsze będzie jeden kawałek kodu do utrzymania.

Zwróć uwagę, jak wersja szablonu (która przyjmuje stałą czasu kompilacji w postaci argumentu szablonu innego niż typ) i wersja bez szablonu (która przyjmuje zmienną czasu wykonywania jako argument funkcji) są przeciążone. Dzięki temu możesz wybrać najbardziej odpowiednią wersję w zależności od potrzeb, mając dość podobną, łatwą do zapamiętania składnię w obu przypadkach:

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
źródło
2
Pamiętaj, że usunąłeś duplikację kodu kosztem skomplikowania logiki wewnątrz pętli. Nie widzę tego ani lepiej, ani gorzej niż to, co zaproponowałem dla tego prostego przykładu. +1 mimo wszystko!
Ali
1
Podoba mi się twoja propozycja, ponieważ pokazuje inną możliwą optymalizację. Jest bardzo możliwe, że indeks od początku może być stałą szablonową. W tym przypadku można by go zastąpić stałą czasu wykonywania przez obiekt wywołujący writeVector i zmieniony writeVector na jakiś szablon. Unikanie dalszych zmian w oryginalnym kodzie.
kriss
1
@kriss: Właściwie moje poprzednie rozwiązanie już pozwalało na to, jeśli dzwoniłeś doWriteVectorbezpośrednio, ale zgadzam się, że nazwa była niefortunna. Po prostu zmieniłem go, aby miał dwie przeciążone writeVectorfunkcje (jeden szablon, drugi zwykłą funkcję), aby wynik był bardziej jednorodny. Dzieki za sugestie. ;)
syam
4
IMO to najlepsza odpowiedź. +1
użytkownik541686
1
@Mehrdad Z wyjątkiem tego, że nie odpowiada na pierwotne pytanie. Unikanie instrukcji if w pętli for? Odpowiada jednak, jak uniknąć spadku wydajności. Jeśli chodzi o „powielanie”, potrzebny byłby bardziej realistyczny przykład przypadków użycia, aby zobaczyć, jak najlepiej je rozłożyć. Jak powiedziałem wcześniej, głosowałem za tą odpowiedzią.
Ali
0

W większości przypadków kod jest już dobry pod względem wydajności i czytelności. Dobry kompilator jest w stanie wykryć niezmienniki pętli i dokonać odpowiednich optymalizacji. Rozważ następujący przykład, który jest bardzo zbliżony do twojego kodu:

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

Do kompilacji używam następującego wiersza poleceń:

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

Następnie zrzućmy zbiór:

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

Wynik montażu write_vectorto:

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)

Widzimy, że na początku funkcji sprawdzamy wartość i przeskakujemy do jednej z dwóch możliwych pętli:

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

Oczywiście działa to tylko wtedy, gdy kompilator jest w stanie wykryć, że warunek jest faktycznie niezmienny. Zwykle doskonale sprawdza się w przypadku flag i prostych funkcji wbudowanych. Ale jeśli warunek jest „złożony”, rozważ zastosowanie innych odpowiedzi.

ivaigult
źródło