programar - ¿Puede el uso del ''auto'' de C++ 11 mejorar el rendimiento?
programar en go (4)
Como
auto
deduce 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
cmp2
y
cmp3
, todo el algoritmo puede alinear la llamada de comparación, mientras que si construye un objeto
std::function
, no solo la llamada no puede estar en línea, sino que también tiene que pasar por la búsqueda polimórfica en el interior de tipo borrado 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, es posible que se vea obligado a construir un nuevo objeto (tal vez 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 valor privalente).
Puedo ver por qué el tipo
auto
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 mejorar
auto
rendimiento? - ¿Alguien puede dar un ejemplo?
Hay dos categorias.
auto
puede evitar el borrado de tipo.
Hay tipos no innombrables (como lambdas), y tipos casi innombrables (como el resultado de
std::bind
u otras plantillas de expresiones similares).
Sin
auto
, termina teniendo que borrar los datos a algo como
std::function
.
El borrado de tipo tiene costos.
std::function<void()> task1 = []{std::cout << "hello";};
auto task2 = []{std::cout << " world/n";};
task1
tiene sobrecarga de borrado de tipo: una posible asignación de montón, dificultad para incluirla y sobrecarga de invocación de la tabla de funciones virtuales.
task2
no tiene ninguno.
Las lambdas
necesitan
deducción automática 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 la
expression()
devuelve
Bar const&
o
Bar
o incluso
Bar&
, donde
Foo
se puede construir a partir de
Bar
.
Se creará un
Foo
temporal, luego se unirá a
f
, y su vida útil se extenderá hasta que
f
desaparezca.
El programador puede haber querido decir
Bar const& f
y no tiene la intención de hacer una copia allí, pero se hace una copia independientemente.
El ejemplo más común es el tipo de
*std::map<A,B>::const_iterator
, que es
std::pair<A const, B> const&
not
std::pair<A,B> const&
, pero el error es una categoría de errores que silenciosamente cuestan rendimiento.
Puede construir un
std::pair<A, B>
partir de un
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.
Las tres respuestas existentes dan ejemplos en los que el uso de
auto
ayudas
auto
"hace que sea menos probable que se pesen involuntariamente"
efectivamente, lo que lo hace "mejorar el rendimiento".
Hay un reverso de la moneda.
Usar
auto
con 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
auto
dio 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 de
auto
para evitar la pesimismo involuntaria podría clasificarse como optimización prematura, o al menos incorrecta;).
auto
puede 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_type
es
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 una
const Key
en una
Key
, pero tenemos que construir una temporal del nuevo tipo para permitir eso.
Por lo tanto, efectivamente nuestro bucle 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 objeto
__tmp
, solo está ahí para ilustración, en realidad el temporal sin nombre solo está vinculado al
item
durante toda su vida útil).
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.