¿Puede el uso del 'auto' de C ++ 11 mejorar el rendimiento?

230

Puedo ver por qué el autotipo en C ++ 11 mejora la corrección y la facilidad de mantenimiento. He leído que también puede mejorar el rendimiento ( Casi siempre automático de Herb Sutter), pero echo de menos una buena explicación.

  • ¿Cómo puede automejorar el rendimiento?
  • ¿Alguien puede dar un ejemplo?
DaBrain
fuente
55
Vea hierbasutter.com/2013/06/13/... que habla sobre evitar conversiones implícitas accidentales, por ejemplo, de gadget a widget. No es un problema común.
Jonathan Wakely
42
¿Aceptan "hace menos probable que se pesen involuntariamente" como una mejora del rendimiento?
5gon12eder
1
Rendimiento del código de limpieza solo en el futuro, tal vez
Croll
Necesitamos una respuesta corta: no, si eres bueno. Puede evitar errores 'novatos'. C ++ tiene una curva de aprendizaje que mata a aquellos que no lo logran después de todo.
Alec Teal

Respuestas:

309

autopuede ayudar al rendimiento al evitar conversiones implícitas silenciosas . Un ejemplo que encuentro convincente es el siguiente.

std::map<Key, Val> m;
// ...

for (std::pair<Key, Val> const& item : m) {
    // do stuff
}

¿Ves el error? Aquí estamos, pensando que estamos tomando elegantemente cada elemento del mapa por referencia constante y usando la nueva expresión de rango para aclarar nuestra intención, pero en realidad estamos copiando cada elemento. Esto se debe a que std::map<Key, Val>::value_typees std::pair<const Key, Val>, no std::pair<Key, Val>. Por lo tanto, cuando (implícitamente) tenemos:

std::pair<Key, Val> const& item = *iter;

En lugar de tomar una referencia a un objeto existente y dejarlo así, tenemos que hacer una conversión de tipo. Se le permite tomar una referencia constante a un objeto (o temporal) de un tipo diferente siempre que haya una conversión implícita disponible, por ejemplo:

int const& i = 2.0; // perfectly OK

La conversión de tipo es una conversión implícita permitida por la misma razón por la que puede convertir a const Keya Key, pero tenemos que construir una temporal del nuevo tipo para permitir eso. Por lo tanto, efectivamente nuestro ciclo hace:

std::pair<Key, Val> __tmp = *iter;       // construct a temporary of the correct type
std::pair<Key, Val> const& item = __tmp; // then, take a reference to it

(Por supuesto, en realidad no hay un __tmpobjeto, solo está allí para ilustración, en realidad, el temporal sin nombre está destinado a itemdurar toda la vida).

Solo cambiando a:

for (auto const& item : m) {
    // do stuff
}

simplemente nos ahorró una tonelada de copias; ahora el tipo de referencia coincide con el tipo de inicializador, por lo que no es necesario realizar una conversión temporal, solo podemos hacer una referencia directa.

Barry
fuente
19
@Barry ¿Puede explicar por qué el compilador felizmente haría copias en lugar de quejarse de tratar de tratar un std::pair<const Key, Val> const &como un std::pair<Key, Val> const &? Nuevo en C ++ 11, no estoy seguro de cómo es el rango y autojuega en esto.
Agop
@Barry Gracias por la explicación. Esa es la pieza que me faltaba, por alguna razón, pensé que no podías tener una referencia constante a un temporal. Pero, por supuesto, puede hacerlo: dejará de existir al final de su alcance.
Agop
@barry Te entiendo, pero el problema es que no hay una respuesta que cubra todas las razones para usar autoque aumentan el rendimiento. Así que lo escribiré en mis propias palabras a continuación.
Yakk - Adam Nevraumont
38
Todavía no creo que esto sea una prueba de que " automejora el rendimiento". Es solo un ejemplo que " autoayuda a prevenir errores del programador que destruyen el rendimiento". Sostengo que hay una distinción sutil pero importante entre los dos. Aún así, +1.
Carreras ligeras en órbita el
70

Como autodeduce el tipo de la expresión de inicialización, no hay conversión de tipo involucrada. Combinado con algoritmos con plantilla, esto significa que puede obtener un cálculo más directo que si fuera a inventar un tipo usted mismo, ¡especialmente cuando se trata de expresiones cuyo tipo no puede nombrar!

Un ejemplo típico proviene de (ab) usando std::function:

std::function<bool(T, T)> cmp1 = std::bind(f, _2, 10, _1);  // bad
auto cmp2 = std::bind(f, _2, 10, _1);                       // good
auto cmp3 = [](T a, T b){ return f(b, 10, a); };            // also good

std::stable_partition(begin(x), end(x), cmp?);

Con cmp2y cmp3, todo el algoritmo puede alinear la llamada de comparación, mientras que si construye un std::functionobjeto, no solo no se puede alinear la llamada, sino que también tiene que pasar por la búsqueda polimórfica en el interior borrado de tipo del contenedor de funciones.

Otra variante de este tema es que puedes decir:

auto && f = MakeAThing();

Esto siempre es una referencia, vinculada al valor de la expresión de llamada de función, y nunca construye ningún objeto adicional. Si no conocía el tipo del valor devuelto, podría verse obligado a construir un nuevo objeto (quizás como temporal) a través de algo como T && f = MakeAThing(). (Además, auto &&incluso funciona cuando el tipo de retorno no es móvil y el valor de retorno es un prvalue).

Kerrek SB
fuente
Por lo tanto, esta es la razón de "evitar el borrado de tipo" auto. Su otra variante es "evitar copias accidentales", pero necesita adornos; ¿Por qué autote da velocidad simplemente escribiendo el tipo allí? (Creo que la respuesta es "te equivocas y se convierte silenciosamente") Lo que lo convierte en un ejemplo menos explicado de la respuesta de Barry, ¿no? Es decir, hay dos casos básicos: automático para evitar el borrado de tipos y automático para evitar errores de tipo silencioso que se convierten accidentalmente, los cuales tienen un costo de tiempo de ejecución.
Yakk - Adam Nevraumont
2
"no solo la llamada no puede estar en línea", ¿por qué entonces? ¿Quiere decir que, en principio, algo impide que la llamada se desvirtualice después del análisis del flujo de datos si las especializaciones relevantes de std::bind, std::functiony std::stable_partitiontodas han sido alineadas? ¿O simplemente que en la práctica ningún compilador de C ++ se alineará de forma lo suficientemente agresiva como para resolver el desastre?
Steve Jessop
@SteveJessop: Principalmente el último: después de pasar por el std::functionconstructor, será muy complejo ver a través de la llamada real, especialmente con optimizaciones de función pequeña (por lo que no desea la desvirtualización). Por supuesto, en principio todo es como si ...
Kerrek SB
41

Hay dos categorias.

autoPuede evitar el borrado de tipo. Hay tipos no innombrables (como lambdas), y tipos casi innombrables (como el resultado de std::bindu otra plantilla de expresión como cosas).

Sin auto, terminas teniendo que borrar los datos a algo así std::function. El borrado de tipo tiene costos.

std::function<void()> task1 = []{std::cout << "hello";};
auto task2 = []{std::cout << " world\n";};

task1tiene sobrecarga de borrado de tipo: una posible asignación de montón, dificultad para alinearlo y sobrecarga de invocación de tabla de funciones virtuales. task2no tiene ninguno Lambdas necesita auto u otras formas de deducción de tipo para almacenar sin borrado de tipo; otros tipos pueden ser tan complejos que solo lo necesitan en la práctica.

En segundo lugar, puede obtener tipos incorrectos. En algunos casos, el tipo incorrecto funcionará aparentemente perfectamente, pero causará una copia.

Foo const& f = expression();

se compilará si se expression()devuelve Bar const&o Barincluso Bar&, desde dónde Foose puede construir Bar. Se Foocreará un temporal , luego se vinculará f, y su vida útil se extenderá hasta que fdesaparezca.

El programador puede haber querido Bar const& fhacer una copia allí, y no lo hizo, pero se hace una copia independientemente.

El ejemplo más común es el tipo de *std::map<A,B>::const_iterator, que std::pair<A const, B> const&no lo es std::pair<A,B> const&, pero el error es una categoría de errores que silenciosamente cuestan rendimiento. Puedes construir un std::pair<A, B>de a std::pair<const A, B>. (La clave en un mapa es constante, porque editarlo es una mala idea)

Tanto @Barry como @KerrekSB ilustraron por primera vez estos dos principios en sus respuestas. Esto es simplemente un intento de resaltar los dos problemas en una sola respuesta, con una redacción que apunta al problema en lugar de centrarse en el ejemplo.

Yakk - Adam Nevraumont
fuente
9

Las tres respuestas existentes dan ejemplos en los que el uso de las autoayudas “hace que sea menos probable que se pesen involuntariamente” efectivamente, lo que lo hace "mejorar el rendimiento".

Hay un reverso de la moneda. El uso autocon objetos que tienen operadores que no devuelven el objeto básico puede dar como resultado un código incorrecto (aún compilable y ejecutable). Por ejemplo, esta pregunta pregunta cómo usar autodio resultados diferentes (incorrectos) usando la biblioteca Eigen, es decir , las siguientes líneas

const auto    resAuto    = Ha + Vector3(0.,0.,j * 2.567);
const Vector3 resVector3 = Ha + Vector3(0.,0.,j * 2.567);

std::cout << "resAuto = " << resAuto <<std::endl;
std::cout << "resVector3 = " << resVector3 <<std::endl;

resultó en una salida diferente. Es cierto que esto se debe principalmente a la evaluación perezosa de Eigens, pero ese código es / debería ser transparente para el usuario (de la biblioteca).

Si bien el rendimiento no se ha visto muy afectado aquí, el uso autopara evitar pesimismo involuntario podría clasificarse como optimización prematura, o al menos incorrecta;).

Avi Ginsburg
fuente
1
Se agregó la pregunta opuesta: stackoverflow.com/questions/38415831/…
Leon