Evitare l'istruzione if all'interno di un ciclo for?


116

Ho una classe chiamata Writerche ha una funzione in questo writeVectormodo:

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

Sto cercando di non avere un codice duplicato, pur continuando a preoccuparmi delle prestazioni. Nella funzione sto if (index)controllando ogni round del mio forloop, anche se il risultato è sempre lo stesso. Questo è contro "preoccuparsi per le prestazioni".

Potrei facilmente evitarlo posizionando il segno di spunta fuori dal mio forloop. Tuttavia, riceverò un sacco di codice duplicato:

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

Quindi queste sono entrambe soluzioni "cattive" per me. Quello che stavo pensando, sono due funzioni private, una di loro supera l'indice e poi chiama l'altra. L'altro supera solo il valore. Tuttavia, non riesco a capire come usarlo con il mio programma, avrei comunque bisogno del ifcontrollo per vedere quale chiamare ...

Secondo il problema, il polimorfismo sembra una soluzione corretta. Ma non riesco a vedere come dovrei usarlo qui. Quale sarebbe il modo migliore per risolvere questo tipo di problema?

Questo non è un vero programma, mi interessa solo imparare come risolvere questo tipo di problema.


8
@JonathonReinhart Forse alcune persone vogliono imparare a programmare e sono curiose di sapere come risolvere i problemi?
Skamah One

9
Ho dato +1 a questa domanda. Questo tipo di ottimizzazione può non essere spesso necessario, ma in primo luogo, sottolineare questo fatto può essere parte della risposta e, in secondo luogo, rari tipi di ottimizzazione sono ancora altamente rilevanti per la programmazione.
jogojapan

31
La domanda riguarda un buon design che eviti la duplicazione del codice e la logica complicata all'interno del ciclo. È una buona domanda, non c'è bisogno di sottovalutarla.
Ali

5
È una domanda interessante, di solito i passaggi di trasformazione del ciclo nel compilatore risolveranno questo in modo molto efficiente. se la funzione è sufficientemente piccola come questa, l'inliner si prenderà cura di essa e molto probabilmente ucciderà completamente il ramo. Preferisco cambiare il codice fino a quando l'inliner non inserisce felicemente il codice piuttosto che risolverlo con i modelli.
Alex

5
@JonathonReinhart: eh? La prima revisione della domanda è praticamente identica a questa. Il tuo "perché ti interessa?" il commento è irrilevante al 100% per tutte le revisioni. Per quanto riguarda il rimprovero pubblico, non sei solo tu, sono molte persone qui che causano questo problema. Quando il titolo è "evitare istruzioni if ​​all'interno di un ciclo for" , dovrebbe essere abbastanza ovvio che la domanda è generica e l'esempio è solo a scopo illustrativo . Non stai aiutando nessuno quando ignori la domanda e fai sembrare stupido l'OP a causa del particolare esempio illustrativo che ha usato.
user541686

Risposte:


79

Passa nel corpo del ciclo come funtore. Viene integrato in fase di compilazione, nessuna penalizzazione delle prestazioni.

L'idea di trasmettere ciò che varia è onnipresente nella libreria standard C ++. Si chiama pattern strategico.

Se ti è permesso usare C ++ 11, puoi fare qualcosa del genere:

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

Questo codice non è perfetto ma hai un'idea.

Nel vecchio C ++ 98 sembra così:

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

Anche in questo caso, il codice è lungi dall'essere perfetto ma ti dà l'idea.


4
for(int i=0;i<100;i++){cout<<"Thank you!"<<endl;}: D Questo è il tipo di soluzione che stavo cercando, funziona come un incantesimo :) Potresti migliorarlo con pochi commenti (ho avuto problemi a capirlo all'inizio), ma l'ho preso quindi nessun problema :)
Skamah One

1
Sono contento che abbia aiutato! Si prega di controllare il mio aggiornamento con il codice C ++ 11, è meno gonfio rispetto alla versione C ++ 98.
Ali

3
Nitpick: questo va bene nel caso di esempio di OP perché il corpo del loop è così piccolo, ma se fosse più grande (immagina una dozzina di righe di codice invece di una sola cout << e << "\n";) ci sarebbe ancora una certa duplicazione del codice.
syam

3
Perché le strutture e l'overloading degli operatori vengono utilizzati nell'esempio C ++ 03? Perché non creare solo due funzioni e passare loro i puntatori?
Malcolm

2
@Malcolm Inlining. Se sono strutture, è probabile che le chiamate di funzione possano essere inline. Se si passa un puntatore a funzione, è probabile che quelle chiamate non possano essere inline.
Ali

40

Nella funzione, sto facendo il controllo if (index) su ogni round del mio ciclo for, anche se il risultato è sempre lo stesso. Questo è contro "preoccuparsi per le prestazioni".

Se questo è effettivamente il caso, il predittore di ramo non avrà problemi a prevedere il risultato (costante). In quanto tale, ciò causerà solo un leggero sovraccarico per previsioni errate nelle prime iterazioni. Non c'è niente di cui preoccuparsi in termini di prestazioni

In questo caso, propongo di mantenere il test all'interno del ciclo per maggiore chiarezza.


3
È solo un esempio, sono qui per imparare come risolvere questo tipo di problema. Sono solo curioso, non sto nemmeno creando un vero programma. Avrei dovuto menzionarlo nella domanda.
Skamah One

40
In tal caso, tieni presente che l'ottimizzazione prematura è la radice di tutti i mali . Durante la programmazione, concentrati sempre sulla leggibilità del codice e assicurati che gli altri capiscano cosa stai cercando di fare. Considera solo micro-ottimizzazioni e vari hack dopo aver profilato il tuo programma e aver identificato gli hotspot . Non dovresti mai considerare le ottimizzazioni senza stabilirne la necessità. Molto spesso, i problemi di prestazioni non sono dove ti aspetti che siano.
Marc Claesen

3
E in questo particolare esempio (ok, capito, questo è solo un esempio) è molto probabile che il tempo speso per il controllo del loop e se il test è quasi invisibile oltre al tempo speso per IO. Questo è spesso un problema con C ++: scegliere tra leggibilità al costo di manutenzione e efficienza (ipotetica).
kriss

8
Si presume che il codice sia in esecuzione su un processore con cui iniziare la previsione del ramo. La maggior parte dei sistemi che eseguono C ++ non lo fanno. (Anche se, probabilmente, la maggior parte dei sistemi con un utile std::coutfare)
Ben Voigt

2
-1. Sì, la previsione del ramo funzionerà bene qui. Sì, la condizione potrebbe effettivamente essere sollevata al di fuori del ciclo dal compilatore. Sì, POITROAE. Ma i rami all'interno di un ciclo sono una cosa pericolosa che spesso ha un impatto sulle prestazioni, e non penso che ignorarli semplicemente dicendo "previsione dei rami" sia un buon consiglio se qualcuno si preoccupa davvero delle prestazioni. L'esempio più notevole è che un compilatore vettorizzatore avrà bisogno di una previsione per gestirlo, producendo codice meno efficiente rispetto ai cicli senza rami.
Quercia

35

Per espandere la risposta di Ali, che è perfettamente corretta ma duplica ancora del codice (parte del corpo del ciclo, questo è sfortunatamente difficilmente evitabile quando si utilizza il modello strategico) ...

Certo in questo caso particolare la duplicazione del codice non è molto, ma c'è un modo per ridurla ancora di più, il che è utile se il corpo della funzione è più grande di poche istruzioni .

La chiave è utilizzare la capacità del compilatore di eseguire costantemente l'eliminazione del codice pieghevole / inattivo . Possiamo farlo mappando manualmente il valore di runtime di indexsu un valore in fase di compilazione (facile da fare quando ci sono solo un numero limitato di casi - due in questo caso) e utilizzare un argomento di modello non di tipo noto in fase di compilazione -tempo:

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

In questo modo si finisce con il codice compilato che è equivalente al secondo esempio di codice (esterno if/ interno for) ma senza duplicare il codice noi stessi. Ora possiamo rendere la versione del modello writeVectorcomplicata quanto vogliamo, ci sarà sempre un singolo pezzo di codice da mantenere.

Notare come la versione del modello (che accetta una costante del tempo di compilazione sotto forma di un argomento di modello non di tipo) e la versione non di modello (che accetta una variabile di runtime come argomento di funzione) sono sovraccariche. Ciò consente di scegliere la versione più pertinente in base alle proprie esigenze, avendo una sintassi piuttosto simile, facile da ricordare in entrambi i casi:

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
Tieni presente che hai rimosso la duplicazione del codice a scapito di rendere più complicata la logica all'interno del ciclo. Non lo vedo né meglio né peggio di quello che ho proposto per questo particolare semplice esempio. +1 comunque!
Ali

1
Mi piace la tua proposta perché mostra un'altra possibile ottimizzazione. È molto probabile che index possa essere una costante del modello dall'inizio. In questo caso potrebbe essere sostituito da una costante di runtime dal chiamante di writeVector e writeVector modificato in un modello. Evitando qualsiasi ulteriore modifica al codice originale.
kriss

1
@kriss: In realtà la mia soluzione precedente lo consentiva già se chiamavi doWriteVectordirettamente ma sono d'accordo che il nome fosse sfortunato. L'ho appena cambiato per avere due writeVectorfunzioni sovraccaricate (un modello, l'altra una funzione regolare) in modo che il risultato sia più omogeneo. Grazie per il suggerimento. ;)
syam

4
IMO questa è la migliore risposta. +1
user541686

1
@ Mehrdad Tranne che non risponde alla domanda originale Evitando l'istruzione if all'interno di un ciclo for? Tuttavia, risponde a come evitare la penalizzazione delle prestazioni. Per quanto riguarda la "duplicazione", sarebbe necessario un esempio più realistico con casi d'uso per vedere come è meglio fattorizzarla. Come ho detto prima, ho votato positivamente questa risposta.
Ali

0

Nella maggior parte dei casi, il codice è già buono per le prestazioni e la leggibilità. Un buon compilatore è in grado di rilevare invarianti di ciclo e di eseguire ottimizzazioni appropriate. Considera il seguente esempio che è molto vicino al tuo codice:

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

Sto usando la seguente riga di comando per compilarlo:

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

Quindi, eseguiamo il dump dell'assembly:

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

Il risultato dell'assemblaggio di write_vectorè:

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)

Possiamo vedere, che all'inizio della funzione, controlliamo il valore e saltiamo a uno dei due possibili cicli:

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

Naturalmente, questo funziona solo se un compilatore è in grado di rilevare che una condizione è effettivamente invariante. Di solito funziona perfettamente per flag e semplici funzioni inline. Ma se la condizione è "complessa", considera l'utilizzo di approcci da altre risposte.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.