¿Evitando la declaración if dentro de un bucle for?

116

Tengo una clase llamada Writerque tiene una función writeVectorcomo esta:

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

Estoy tratando de no tener un código duplicado, sin dejar de preocuparme por el rendimiento. En la función, estoy haciendo la if (index)verificación en cada ronda de mi forbucle, aunque el resultado es siempre el mismo. Esto va en contra de "preocuparse por el rendimiento".

Podría evitarlo fácilmente colocando el cheque fuera de mi forbucle. Sin embargo, obtendré un montón de código duplicado:

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

Así que estas son dos soluciones "malas" para mí. Lo que he estado pensando son dos funciones privadas, una de ellas sale del índice y luego llama a la otra. El otro solo supera el valor. Sin embargo, no puedo averiguar cómo usarlo con mi programa, todavía necesitaría el ifcheque para ver a cuál llamar ...

Según el problema, el polimorfismo parece una solución correcta. Pero no veo cómo debería usarlo aquí. ¿Cuál sería la forma preferida de resolver este tipo de problema?

Este no es un programa real, solo estoy interesado en aprender cómo se debe resolver este tipo de problema.

Skamah Uno
fuente
8
@JonathonReinhart ¿Quizás algunas personas quieran aprender a programar y sienten curiosidad por saber cómo resolver problemas?
Skamah One
9
He dado +1 a esta pregunta. Este tipo de optimización puede no ser necesario a menudo, pero en primer lugar, señalar este hecho puede ser parte de la respuesta y, en segundo lugar, los tipos raros de optimización siguen siendo muy relevantes para la programación.
jogojapan
31
La pregunta es sobre un buen diseño que evite la duplicación de código y la lógica complicada dentro del ciclo. Es una buena pregunta, no es necesario que la rechace.
Ali
5
Es una pregunta interesante, por lo general, los pases de transformación de bucle en el compilador resolverán esto de manera muy eficiente. si la función es lo suficientemente pequeña como esta, el inliner se encargará de ella y lo más probable es que elimine la rama por completo. Prefiero cambiar el código hasta que el inliner esté felizmente incluido en el código que resolver esto con plantillas.
Alex
5
@JonathonReinhart: ¿Eh? La primera revisión de la pregunta es prácticamente idéntica a esta. Tu "¿por qué te importa?" El comentario es 100% irrelevante para todas las revisiones. En cuanto a reprenderlo públicamente, no es solo usted, es mucha gente aquí la que causa este problema. Cuando el título es "evitar declaraciones if dentro de un bucle for" , debería ser bastante obvio que la pregunta es genérica, y el ejemplo es solo para ilustración . No estás ayudando a nadie cuando ignoras la pregunta y haces que el OP parezca estúpido debido al ejemplo ilustrativo particular que usó.
user541686

Respuestas:

79

Pase en el cuerpo del bucle como un funtor. Se inserta en tiempo de compilación, sin penalización de rendimiento.

La idea de pasar lo que varía es omnipresente en la biblioteca estándar de C ++. Se llama patrón de estrategia.

Si se le permite usar C ++ 11, puede hacer algo como esto:

#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 no es perfecto pero entiendes la idea.

En el antiguo C ++ 98 se ve así:

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

Una vez más, el código está lejos de ser perfecto, pero te da la idea.

Ali
fuente
4
for(int i=0;i<100;i++){cout<<"Thank you!"<<endl;}: D Este es el tipo de solución que estaba buscando, funciona como un encanto :) Podrías mejorarlo con pocos comentarios (tuve problemas para entenderlo al principio), pero lo obtuve, así que no hay problema :)
Skamah One
1
¡Me alegro de que haya ayudado! Verifique mi actualización con el código C ++ 11, está menos hinchado en comparación con la versión C ++ 98.
Ali
3
Nitpick: esto está bien en el caso de ejemplo de OP porque el cuerpo del bucle es muy pequeño, pero si fuera más grande (imagina una docena de líneas de código en lugar de solo una cout << e << "\n";) todavía habría bastante duplicación de código.
syam
3
¿Por qué se utilizan las estructuras y la sobrecarga de operadores en el ejemplo de C ++ 03? ¿Por qué no hacer dos funciones y pasarles los indicadores?
Malcolm
2
@Malcolm Inlining. Si son estructuras, es probable que las llamadas a funciones se puedan insertar. Si pasa un puntero de función, es probable que esas llamadas no se puedan insertar.
Ali
40

En la función, estoy haciendo la verificación if (index) en cada ronda de mi bucle for, aunque el resultado es siempre el mismo. Esto va en contra de "preocuparse por el rendimiento".

Si este es realmente el caso, el predictor de rama no tendrá problemas para predecir el resultado (constante). Como tal, esto solo causará una leve sobrecarga por errores de predicción en las primeras iteraciones. No hay nada de qué preocuparse en términos de rendimiento.

En este caso, abogo por mantener la prueba dentro del ciclo para mayor claridad.

Marc Claesen
fuente
3
Es solo un ejemplo, estoy aquí para aprender cómo se debe resolver este tipo de problema. Solo tengo curiosidad, ni siquiera estoy creando un programa real. Debería haberlo mencionado en la pregunta.
Skamah One
40
En ese caso, tenga en cuenta que la optimización prematura es la raíz de todos los males . Al programar, céntrese siempre en la legibilidad del código y asegúrese de que los demás comprendan lo que está intentando hacer. Solo considere las microoptimizaciones y varios trucos después de perfilar su programa e identificar los puntos de acceso . Nunca debe considerar las optimizaciones sin establecer la necesidad de ellas. Muy a menudo, los problemas de rendimiento no están donde usted espera que estén.
Marc Claesen
3
Y en este ejemplo en particular (ok, entendido, esto es solo un ejemplo) es muy probable que el tiempo dedicado al control de bucle y si la prueba sea casi invisible además del tiempo dedicado a IO. Esto suele ser un problema con C ++: elegir entre legibilidad a costa del mantenimiento y eficiencia (hipotética).
kriss
8
Está asumiendo que el código se está ejecutando en un procesador que tiene una predicción de rama para empezar. La mayoría de los sistemas que ejecutan C ++ no lo hacen. (Aunque, probablemente la mayoría de los sistemas con una función útil std::cout)
Ben Voigt
2
-1. Sí, la predicción de ramas funcionará bien aquí. Sí, el compilador podría sacar la condición del bucle. Sí, POITROAE. Pero las ramas dentro de un ciclo son algo peligroso que a menudo tiene un impacto en el rendimiento, y no creo que descartarlas simplemente diciendo "predicción de ramas" sea un buen consejo si alguien realmente se preocupa por el rendimiento. El ejemplo más notable es que un compilador de vectorización necesitará una predicación para manejar esto, produciendo un código menos eficiente que para los bucles sin ramificaciones.
Oak
35

Para ampliar la respuesta de Ali, que es perfectamente correcta pero aún duplica algo de código (parte del cuerpo del bucle, desafortunadamente, esto es difícilmente evitable cuando se usa el patrón de estrategia) ...

Concedido en este caso particular, la duplicación de código no es mucha, pero hay una manera de reducirla aún más, lo cual es útil si el cuerpo de la función es más grande que unas pocas instrucciones .

La clave es utilizar la capacidad del compilador para realizar una eliminación constante de plegado / código muerto . Podemos hacer eso asignando manualmente el valor de tiempo de ejecución de indexa un valor de tiempo de compilación (fácil de hacer cuando hay solo un número limitado de casos, dos en este caso) y usar un argumento de plantilla que no sea de tipo que se conoce en la compilación -hora:

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 esta manera terminamos con un código compilado que es equivalente a su segundo ejemplo de código (externo if/ interno for) pero sin duplicar el código nosotros mismos. Ahora podemos hacer que la versión de la plantilla sea writeVectortan complicada como queramos, siempre habrá una sola pieza de código para mantener.

Observe cómo la versión de la plantilla (que toma una constante de tiempo de compilación en forma de un argumento de plantilla sin tipo) y la versión sin plantilla (que toma una variable de tiempo de ejecución como argumento de función) están sobrecargadas. Esto le permite elegir la versión más relevante en función de sus necesidades, teniendo una sintaxis bastante similar y fácil de recordar en ambos 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
fuente
2
Tenga en cuenta que eliminó la duplicación de código a expensas de complicar la lógica dentro del ciclo. No lo veo ni mejor ni peor que lo que propuse para este sencillo ejemplo en particular. +1 de todos modos!
Ali
1
Me gusta tu propuesta porque muestra otra posible optimización. Es muy posible que el índice sea una plantilla constante desde el principio. En este caso, podría ser reemplazado por una constante de tiempo de ejecución por el llamador de writeVector y writeVector cambiado a alguna plantilla. Evitando cualquier cambio adicional al código original.
kriss
1
@kriss: En realidad, mi solución anterior ya permitía que si llamabas doWriteVectordirectamente, pero estoy de acuerdo en que el nombre era desafortunado. Lo acabo de cambiar para que tenga dos writeVectorfunciones sobrecargadas (una plantilla, la otra una función regular) para que el resultado sea más homogéneo. Gracias por la sugerencia. ;)
syam
4
En mi opinión, esta es la mejor respuesta. +1
user541686
1
@Mehrdad Excepto que no responde a la pregunta original ¿ Evitar la declaración if dentro de un bucle for? Sin embargo, sí responde cómo evitar la penalización de rendimiento. En cuanto a la "duplicación", se necesitaría un ejemplo más realista con casos de uso para ver cuál es la mejor manera de descartarla. Como dije antes, voté a favor de esta respuesta.
Ali
0

En la mayoría de los casos, su código ya es bueno para el rendimiento y la legibilidad. Un buen compilador es capaz de detectar invariantes de bucle y realizar las optimizaciones adecuadas. Considere el siguiente ejemplo que está muy cerca de su 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));
}

Estoy usando la siguiente línea de comando para 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

Entonces, descarguemos el ensamblaje:

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

El resultado ensamblado de write_vectores:

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 al inicio de la función, verificamos el valor y saltamos a uno de los dos bucles posibles:

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

Por supuesto, esto solo funciona si un compilador es capaz de detectar que una condición es invariante real. Por lo general, funciona perfectamente para banderas y funciones en línea simples. Pero si la condición es "compleja", considere usar enfoques de otras respuestas.

ivaigult
fuente