Evitando a instrução if dentro de um loop for?

116

Eu tenho uma classe chamada Writerque tem uma função writeVectorassim:

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

Estou tentando não ter um código duplicado, mas ainda me preocupo com o desempenho. Na função, estou fazendo a if (index)verificação em cada rodada do meu for-loop, embora o resultado seja sempre o mesmo. Isso é contra "se preocupar com o desempenho".

Eu poderia facilmente evitar isso colocando o cheque fora do meu for-loop. No entanto, vou obter muitos códigos duplicados:

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

Portanto, essas duas soluções são "ruins" para mim. O que estive pensando são duas funções privadas, uma delas sai do índice e depois chama a outra. O outro apenas supera o valor. No entanto, não consigo descobrir como usá-lo com meu programa, ainda preciso ifverificar para ver qual chamar ...

De acordo com o problema, o polimorfismo parece a solução correta. Mas não consigo ver como devo usá-lo aqui. Qual seria a forma preferida de resolver esse tipo de problema?

Este não é um programa real, estou apenas interessado em aprender como esse tipo de problema deve ser resolvido.

Skamah One
fonte
8
@JonathonReinhart Talvez algumas pessoas queiram aprender programação e estejam curiosas para saber como resolver problemas?
Skamah One
9
Eu dei a esta pergunta +1. Muitas vezes, esse tipo de otimização pode não ser necessário, mas, em primeiro lugar, apontar esse fato pode ser parte da resposta e, em segundo lugar, tipos raros de otimização ainda são altamente relevantes para a programação.
jogojapan 01 de
31
A questão é sobre um bom design que evita a duplicação de código e lógica complicada dentro do loop. É uma boa pergunta, não há necessidade de votar contra ela.
Ali
5
É uma questão interessante, geralmente a transformação de loop passa no compilador resolverá isso de forma muito eficiente. se a função for suficientemente pequena como esta, o inliner cuidará dela e provavelmente eliminará o branch completamente. Prefiro alterar o código até que o inliner esteja in-line o código do que resolver isso com modelos.
Alex
5
@JonathonReinhart: Hein? A primeira revisão da questão é virtualmente idêntica a esta. Seu "por que você se importa?" o comentário é 100% irrelevante para todas as revisões. Quanto a repreendê-lo publicamente - não é só você, são muitas pessoas aqui que causam esse problema. Quando o título é "evitando instruções if dentro de um loop for" , deve ser bastante óbvio que a questão é genérica e o exemplo é apenas para ilustração . Você não está ajudando ninguém quando ignora a pergunta e faz o OP parecer estúpido por causa do exemplo ilustrativo específico que ele usou.
user541686 01 de

Respostas:

79

Passe o corpo do loop como um functor. Ele fica embutido em tempo de compilação, sem penalidade de desempenho.

A ideia de passar o que varia é onipresente na Biblioteca Padrão C ++. É o chamado padrão de estratégia.

Se você tem permissão para usar C ++ 11, pode fazer algo assim:

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

Este código não é perfeito, mas essa é a ideia.

No antigo C ++ 98, era assim:

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

Novamente, o código está longe de ser perfeito, mas dá uma ideia.

Todos
fonte
4
for(int i=0;i<100;i++){cout<<"Thank you!"<<endl;}: D Este é o tipo de solução que eu estava procurando, funciona perfeitamente :) Você poderia melhorá-la com alguns comentários (tive problemas para entendê-la no início), mas eu consegui sem problemas :)
Skamah One
1
Estou feliz por ter ajudado! Por favor, verifique minha atualização com o código C ++ 11, é menos inchado em comparação com a versão C ++ 98.
Ali
3
Nitpick: isso é bom no caso de exemplo do OP porque o corpo do loop é muito pequeno, mas se fosse maior (imagine uma dúzia de linhas de código em vez de apenas uma cout << e << "\n";), ainda haveria bastante duplicação de código.
syam 01 de
3
Por que as estruturas e a sobrecarga de operador são usadas no exemplo C ++ 03? Por que não criar duas funções e passar os ponteiros para elas?
Malcolm
2
@Malcolm Inlining. Se forem estruturas, é provável que as chamadas de função possam ser sequenciais. Se você passar um ponteiro de função, é provável que essas chamadas não possam ser sequenciais.
Ali
40

Na função, estou fazendo a verificação if (index) em cada rodada do meu loop for, mesmo que o resultado seja sempre o mesmo. Isso é contra "se preocupar com o desempenho".

Se este for realmente o caso, o preditor de ramificação não terá problemas em prever o resultado (constante). Como tal, isso só causará uma pequena sobrecarga para previsões erradas nas primeiras iterações. Não há nada com que se preocupar em termos de desempenho

Nesse caso, eu defendo manter o teste dentro do loop para maior clareza.

Marc Claesen
fonte
3
É apenas um exemplo, estou aqui para aprender como esse tipo de problema deve ser resolvido. Estou apenas curioso, nem mesmo estou criando um programa real. Deveria ter mencionado isso na pergunta.
Skamah One
40
Nesse caso, lembre-se de que a otimização prematura é a raiz de todos os males . Ao programar, sempre foque na legibilidade do código e certifique-se de que os outros entendam o que você está tentando fazer. Considere apenas micro-otimizações e vários hacks após criar o perfil de seu programa e identificar pontos de acesso . Você nunca deve considerar otimizações sem estabelecer a necessidade delas. Muitas vezes, os problemas de desempenho não estão onde você espera.
Marc Claesen
3
E neste exemplo particular (ok, entendido, este é apenas um exemplo) é muito provável que o tempo gasto para controle de loop e se teste seja quase invisível ao lado do tempo gasto para IO. Isso costuma ser um problema com C ++: escolher entre a legibilidade ao custo de manutenção e a eficiência (hipotética).
Kriss
8
Você está assumindo que o código está sendo executado em um processador que tem previsão de ramificação para começar. A maioria dos sistemas executando C ++ não. (Embora, provavelmente a maioria dos sistemas com uma função útil std::cout)
Ben Voigt
2
-1. Sim, a previsão de branch funcionará bem aqui. Sim, a condição pode realmente ser içada fora do loop pelo compilador. Sim, POITROAE. Mas branches dentro de um loop são coisas perigosas que geralmente têm impacto no desempenho, e não acho que descartá-los apenas dizendo "previsão de branch" seja um bom conselho se alguém realmente se preocupa com o desempenho. O exemplo mais notável é que um compilador de vetorização precisará de predicação para lidar com isso, produzindo código menos eficiente do que para loops sem ramificação.
Oak
35

Para expandir a resposta de Ali, que é perfeitamente correta, mas ainda assim duplica algum código (parte do corpo do loop, infelizmente isso é dificilmente evitável ao usar o padrão de estratégia) ...

Concedido, neste caso específico, a duplicação de código não é muito, mas há uma maneira de reduzi-la ainda mais, o que é útil se o corpo da função for maior do que apenas algumas instruções .

A chave é usar a capacidade do compilador de realizar dobragem constante / eliminação de código morto . Podemos fazer isso mapeando manualmente o valor de tempo de execução de indexpara um valor de tempo de compilação (fácil de fazer quando há apenas um número limitado de casos - dois neste caso) e usar um argumento de modelo não-tipo que é conhecido na compilação -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);
}

Desta forma, terminamos com o código compilado que é equivalente ao seu segundo exemplo de código (externo if/ interno for), mas sem duplicar o código. Agora podemos tornar a versão do modelo writeVectortão complicada quanto quisermos, sempre haverá um único pedaço de código para manter.

Observe como a versão do modelo (que usa uma constante de tempo de compilação na forma de um argumento de modelo não-tipo) e a versão não-modelo (que usa uma variável de tempo de execução como um argumento de função) estão sobrecarregadas. Isso permite que você escolha a versão mais relevante de acordo com suas necessidades, tendo uma sintaxe bastante semelhante e fácil de lembrar em ambos os casos:

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
fonte
2
Lembre-se de que você removeu a duplicação de código à custa de tornar a lógica dentro do loop mais complicada. Não vejo nem melhor nem pior do que o que propus para este exemplo simples em particular. +1 de qualquer maneira!
Ali
1
Gosto da sua proposta porque mostra outra otimização possível. É muito possível que o índice seja uma constante do modelo desde o início. Nesse caso, ele poderia ser substituído por uma constante de tempo de execução pelo chamador de writeVector e writeVector alterado para algum modelo. Evitando qualquer alteração adicional no código original.
Kriss
1
@kriss: Na verdade, minha solução anterior já permitia isso se você ligasse doWriteVectordiretamente, mas concordo que o nome era lamentável. Acabei de alterá-lo para ter duas writeVectorfunções sobrecarregadas (um modelo, o outro uma função regular) para que o resultado seja mais homogêneo. Obrigado pela sugestão. ;)
syam 01 de
4
IMO esta é a melhor resposta. +1
user541686
1
@Mehrdad Exceto que não responde à pergunta original Evitando a instrução if dentro de um loop for? Porém, responde como evitar a penalidade de desempenho. Quanto à "duplicação", um exemplo mais realista com casos de uso seria necessário para ver como é melhor fatorado. Como eu disse antes, votei a favor dessa resposta.
Ali
0

Na maioria dos casos, seu código já é bom para desempenho e legibilidade. Um bom compilador é capaz de detectar invariantes de loop e fazer otimizações apropriadas. Considere o exemplo a seguir, que é muito próximo ao seu código:

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

Estou usando a seguinte linha de comando para compilá-lo:

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

Então, vamos despejar a montagem:

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

O resultado da montagem 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)

Podemos ver que no início da função, verificamos o valor e saltamos para um dos dois loops possíveis:

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

Claro, isso só funciona se um compilador for capaz de detectar que uma condição é invariante real. Normalmente, funciona perfeitamente para sinalizadores e funções embutidas simples. Mas se a condição for "complexa", considere usar abordagens de outras respostas.

ivaigult
fonte