2012-03-19 21 views
19

En https://doc-snapshots.qt.io/qtcreator-extending/coding-style.html se recomienda escribir para bucles como los siguientes:Puede terminar() ser una operación costosa para contenedores STL

Container::iterator end = large.end(); 
for (Container::iterator it = large.begin(); it != end; ++it) { 
     //...; 
} 

en lugar de

for (Container::iterator it = large.begin(); it != large.end(); ++it) { 
     //...; 
} 

Dado que pocas veces he visto este estilo en cualquier código, me gustaría saber si la llamada consecutiva de end() realmente agrega una sobrecarga de tiempo de ejecución notable para bucles grandes sobre contenedores stl o si los compiladores ya optimizan dichos casos.

Editar: Como muchos de los comentarios a muy buenos señaló: Esta pregunta sólo es válida si el código dentro del bucle no no modifica el iterador final. De lo contrario, por supuesto, la llamada repetida de finalización es obligatoria.

+12

Como acotación al margen: Incluso iría para 'para (contenedor :: iterador it = large.begin(), end = large.end(); it! = end; ++ it) {...} 'para limitar el alcance de la variable' end' a solo for-loop. – haffax

+0

C# y Java devs escriben este tipo de bucles para permitir que el JITer lo optimice (un cheque menos por iteración). Parece que no es el caso de C++. –

+4

Desarrolladores de C++ simplemente escriba 'for_each (begin (c), end (c), []() {});' Loops son para escritores de librerías: P – MSalters

Respuesta

17

El estándar C++ 11 (§ 23.2.1) establece que end tiene O (1) complejidad, por lo que una implementación conforme tendría las mismas características de rendimiento para ambas versiones.

Dicho esto, a menos que el compilador puede probar que el valor de retorno de end nunca cambiará luego tirando end fuera del circuito podría ser más rápido por alguna cantidad constante (como comentarios Steve Jessop, hay un montón de variables que pueden influencia si esto es cierto o no).

Aún así, incluso si en un caso particular no hay absolutamente ninguna diferencia en el rendimiento, es un buen hábito sacar esas pruebas del circuito. Un aún mejor es entrar en es utilizar algoritmos estándar como dice @pmr, lo que evita el problema por completo.

+7

Incluso si el compilador no puede demostrar que el valor de 'end' es invariante de bucle, alzarlo todavía podría no ser más rápido. La razón es que, incluso si se iza, el valor todavía tiene que almacenarse en una variable, que bien podría estar en la pila. El contenedor también podría estar en la pila, en cuyo caso leer su 'extremo' de algún miembro de datos en el contenedor probablemente sea exactamente la misma operación que leer el 'extremo' alzado de una variable. Si el contenedor se pasó por referencia, podría haber una indirección adicional y eso podría ser un poco más lento. –

+0

@SteveJessop: Gracias por el perspicaz comentario, cambié la redacción para que tal vez no deje la impresión equivocada. Aparte de eso, en mi humilde opinión nosotros (los humanos) realmente no deberíamos diseccionar esto tan delgado porque es muy poco probable que las diferencias de rendimiento de una magnitud tan pequeña puedan predecirse. – Jon

+2

Estoy de acuerdo en que no podemos predecir estas pequeñas diferencias. Realmente no estoy de acuerdo con que levantar esas pruebas sea un buen hábito. Para algunas personas, podría mejorar la legibilidad (2 líneas más cortas en lugar de 1 más), de lo contrario se trata de una optimización especulativa y realmente no vale la pena saturar el código para IMO. Si el polipasto reduce la complejidad de toda la operación, o si claramente va a ser una proporción significativa del trabajo realizado en el ciclo, entonces lo haría de forma especulativa. De lo contrario, no lo haría, aunque tampoco me opondría a tomarlo "gratis" cuando use un algoritmo o un rango para. –

-3

Depende de la implementación, pero no creo que end() ofrezca una gran demora en el rendimiento.

3

Si planea modificar la colección mientras itera, debe hacerlo de la 2da manera (el extremo puede cambiar); de lo contrario, la primera es teóricamente una fracción más rápida. Sin embargo, dudo que sea notable.

7

Esto es menos acerca de end es más costoso y más acerca de la capacidad de un compilador para ver que end no cambiará a través de un efecto secundario en el cuerpo del bucle (que es un bucle invariante).

La complejidad de end es constante. Consulte la tabla 96 en N3337 en 23.2.1.

El uso de algoritmos de biblioteca estándar elude todo el dilema.

2

De hecho, el método end() está en línea. El segundo no lo llame cada vez, no creo que end() otorgue ningún retraso en el rendimiento.

0

std :: vector.end() (for example) devuelve un iterador por valor. En el segundo ciclo, crea un objeto en cada ciclo. El estándar de codificación le indica que no cree un objeto si no lo necesita. El compilador puede ser inteligente y optimizar el código para usted, sin embargo, esto no es garantía. Una solución mucho mejor es usar algoritmos stl. Ya están optimizados y tu código será más legible. Tenga en cuenta que los dos bucles son equivalentes solo si no modifica la colección.

P.S.es muy probable que la diferencia en el rendimiento sea muy mínima

+1

Es probable que la llamada a 'end()' esté en línea, como lo es la llamada a 'operator! ='. En ese nivel estamos comparando dos punteros. Tener una copia separada de uno de ellos no es probable que aumente el rendimiento. –

+0

Estoy de acuerdo en que el resultado final en el rendimiento no va a ser muy diferente, sin embargo, me gusta la idea de prestar atención a la proliferación de objetos. –

0

Para cualquiera que lea esto ahora, la pregunta se ha convertido en algo discutible con C++ 11.

No estaba seguro de si esta respuesta califica como respuesta, porque en realidad no aborda el punto de la pregunta. Pero sí creo que es válido señalar que el problema planteado aquí rara vez se encontrará en la práctica para un programador de C++ 11, y ciertamente habría encontrado esta respuesta útil hace unos años. Por lo tanto, esta respuesta está dirigida al lector que simplemente quiere saber mejor forma de iterar a través de todos los elementos en un contenedor STL (vector, list, deque, etc.)).

Suponiendo que el PO quería tener acceso a cada elemento en el contenedor, que puede eludir fácilmente toda la cuestión de si la definición end es lo suficientemente rápido que llamar Container::end() escribiendo un range-based for loop:

Container container; // my STL container that has been filled with stuff 

// (note that you can replace Container::value_type with the value in the container) 

// the standard way 
for (Container::value_type element : container) { 
    // access each element by 'element' rather than by '*it' 
} 

// or, if Container::value_type is large 
Container container; // fill it with something 
for (Container::value_type& element : container) { 
    // 
} 

// if you're lazy 
Container container; // fill it with something 
for (auto element : container) { 
    // 
} 

El PO ha pedido si la compensación entre la brevedad de simplemente comparar it a Container::end() en cada iteración y el rendimiento de declarar una variable end y compararla en cada paso vale la pena. Dado que los bucles for basados ​​en rangos proporcionan una alternativa simple, fácil de escribir y fácil de leer que también ocurre, internamente, declarar un iterador end en lugar de llamar al método Container::end() en cada paso, el número de casos en los que necesitamos detenernos en este la pregunta se ha reducido a un número limitado de casos.

Según cppreference.com, el bucle for-basada gama producirá código con los mismos efectos secundarios como los siguientes:

{ 
    auto && __range = range_expression ; 
    for (auto __begin = begin_expr, 
     __end = end_expr; 
     __begin != __end; ++__begin) { 
    range_declaration = *__begin; 
    loop_statement 
    } 
} 
Cuestiones relacionadas