¿Puede C ++ moderno obtener rendimiento gratis?

205

A veces se afirma que C ++ 11/14 puede darle un aumento de rendimiento incluso cuando solo compila código C ++ 98. La justificación suele estar en la línea de la semántica de movimiento, ya que en algunos casos los constructores de valores r se generan automáticamente o ahora forman parte del STL. Ahora me pregunto si estos casos ya fueron manejados previamente por RVO u optimizaciones de compiladores similares.

Mi pregunta es si podría darme un ejemplo real de un fragmento de código C ++ 98 que, sin modificaciones, se ejecute más rápido utilizando un compilador que admita las nuevas características del lenguaje. Entiendo que no se requiere un compilador conforme estándar para hacer la elisión de la copia y solo por esa razón la semántica de movimiento podría generar velocidad, pero me gustaría ver un caso menos patológico, si lo desea.

EDITAR: Solo para que quede claro, no estoy preguntando si los nuevos compiladores son más rápidos que los antiguos, sino que si hay un código que agregue -std = c ++ 14 a los indicadores de mi compilador, se ejecutará más rápido (evite copias, pero si puede llegar a cualquier otra cosa además de mover la semántica, también me interesaría)

un gran
fuente
3
Recuerde que la elisión de copia y la optimización del valor de retorno se realizan al construir un nuevo objeto usando un constructor de copia. Sin embargo, en un operador de asignación de copia, no hay una elisión de copia (cómo puede ser, ya que el compilador no sabe qué hacer con un objeto ya construido que no es temporal). Por lo tanto, en ese caso, C ++ 11/14 gana a lo grande, al darle la posibilidad de utilizar un operador de asignación de movimiento. Sin embargo, sobre su pregunta, no creo que el código C ++ 98 deba ser más rápido si es compilado por un compilador C ++ 11/14, tal vez sea más rápido porque el compilador es más nuevo.
vsoftco
27
Además, el código que usa la biblioteca estándar es potencialmente más rápido, incluso si lo hace totalmente compatible con C ++ 98, porque en C ++ 11/14 la biblioteca subyacente usa semántica de movimiento interno cuando es posible. Por lo tanto, el código que se ve idéntico en C ++ 98 y C ++ 11/14 será (posiblemente) más rápido en este último caso, siempre que utilice los objetos de biblioteca estándar como vectores, listas, etc. y la semántica de movimiento marca la diferencia.
vsoftco
1
@vsoftco, ese es el tipo de situación a la que me refería, pero no pude encontrar un ejemplo: por lo que recuerdo si tengo que definir el constructor de copia, el constructor de movimiento no se generará automáticamente, lo que nos deja con clases muy simples donde RVO, creo, siempre funciona. Una excepción podría ser algo en conjunción con los contenedores STL, donde los constructores rvalue son generados por el implementador de la biblioteca (lo que significa que no tendría que cambiar nada en el código para que use movimientos).
alarge
Las clases no necesitan ser simples para no tener un constructor de copia. C ++ prosperar en la semántica de valores, y el constructor de copia, operador de asignación, destructor, etc. debería ser la excepción.
sp2danny
1
@Eric Gracias por el enlace, fue interesante. Sin embargo, después de haberlo examinado rápidamente, las ventajas de velocidad parecen provenir principalmente de agregar std::movey mover constructores (lo que requeriría modificaciones al código existente). Lo único realmente relacionado con mi pregunta fue la frase "Obtiene ventajas inmediatas de velocidad simplemente al volver a compilar", que no está respaldado por ningún ejemplo (menciona STL en la misma diapositiva, como lo hice en mi pregunta, pero nada específico ) Estaba pidiendo algunos ejemplos. Si estoy leyendo las diapositivas mal, hágamelo saber.
alarge

Respuestas:

221

Conozco 5 categorías generales en las que recompilar un compilador de C ++ 03 ya que C ++ 11 puede causar aumentos de rendimiento ilimitados que prácticamente no están relacionados con la calidad de la implementación. Estas son todas las variaciones de la semántica de movimiento.

std::vector redistribuir

struct bar{
  std::vector<int> data;
};
std::vector<bar> foo(1);
foo.back().data.push_back(3);
foo.reserve(10); // two allocations and a delete occur in C++03

cada vez que el foobuffer 's se reasigna en C ++ 03 se copian todos los vectoren bar.

En C ++ 11, en cambio, mueve el bar::datas, que es básicamente libre.

En este caso, esto se basa en optimizaciones dentro del stdcontenedor vector. En todos los casos a continuación, el uso de stdcontenedores es solo porque son objetos C ++ que tienen una movesemántica eficiente en C ++ 11 "automáticamente" cuando actualiza su compilador. Los objetos que no lo bloquean y que contienen un stdcontenedor también heredan los moveconstructores mejorados automáticos .

Fallo de NRVO

Cuando NRVO (optimización del valor de retorno denominado) falla, en C ++ 03 vuelve a copiarse, en C ++ 11 vuelve a caer en movimiento. Las fallas de NRVO son fáciles:

std::vector<int> foo(int count){
  std::vector<int> v; // oops
  if (count<=0) return std::vector<int>();
  v.reserve(count);
  for(int i=0;i<count;++i)
    v.push_back(i);
  return v;
}

o incluso:

std::vector<int> foo(bool which) {
  std::vector<int> a, b;
  // do work, filling a and b, using the other for calculations
  if (which)
    return a;
  else
    return b;
}

Tenemos tres valores: el valor de retorno y dos valores diferentes dentro de la función. Elision permite que los valores dentro de la función se 'fusionen' con el valor de retorno, pero no entre sí. Ambos no pueden fusionarse con el valor de retorno sin fusionarse entre sí.

El problema básico es que la elisión de NRVO es frágil, y el código con cambios que no están cerca del returnsitio de repente puede tener reducciones masivas de rendimiento en ese punto sin emitir diagnóstico. En la mayoría de los casos de falla NRVO, C ++ 11 termina con a move, mientras que C ++ 03 termina con una copia.

Devolver un argumento de función

Elision también es imposible aquí:

std::set<int> func(std::set<int> in){
  return in;
}

en C ++ 11 esto es barato: en C ++ 03 no hay forma de evitar la copia. Los argumentos de las funciones no se pueden elidir con el valor de retorno, ya que el código de llamada administra la vida útil y la ubicación del parámetro y el valor de retorno.

Sin embargo, C ++ 11 puede moverse de uno a otro. (En un ejemplo de menos juguete, se podría hacer algo al set).

push_back o insert

Finalmente, la elisión en contenedores no ocurre: pero C ++ 11 sobrecarga los operadores de inserción de movimiento de valor, lo que guarda copias.

struct whatever {
  std::string data;
  int count;
  whatever( std::string d, int c ):data(d), count(c) {}
};
std::vector<whatever> v;
v.push_back( whatever("some long string goes here", 3) );

en C ++ 03 whateverse crea un temporal , luego se copia en el vector v. std::stringSe asignan 2 buffers, cada uno con datos idénticos, y uno se descarta.

En C ++ 11 whateverse crea un temporal . La whatever&& push_backsobrecarga entonces movees temporal en el vector v. Se std::stringasigna un búfer y se traslada al vector. Se std::stringdescarta un vacío .

Asignación

Robado de la respuesta de @ Jarod42 a continuación.

Elision no puede ocurrir con la asignación, pero se puede mover desde.

std::set<int> some_function();

std::set<int> some_value;

// code

some_value = some_function();

aquí some_functiondevuelve un candidato para eludir, pero debido a que no se utiliza para construir un objeto directamente, no se puede elidir. En C ++ 03, lo anterior hace que se copie el contenido de lo temporal some_value. En C ++ 11, se traslada a some_value, que básicamente es gratuito.


Para obtener el efecto completo de lo anterior, necesita un compilador que sintetice los constructores de movimiento y la asignación por usted.

MSVC 2013 implementa constructores de movimiento en stdcontenedores, pero no sintetiza constructores de movimiento en sus tipos.

Por lo tanto, los tipos que contienen std::vectorsy similares no obtienen tales mejoras en MSVC2013, pero comenzarán a obtenerlas en MSVC2015.

clang y gcc han implementado desde hace mucho tiempo constructores de movimientos implícitos. El compilador 2013 de Intel admitirá la generación implícita de constructores de movimientos si aprueba -Qoption,cpp,--gen_move_operations(no lo hacen de forma predeterminada en un esfuerzo por ser compatibles con MSVC2013).

Yakk - Adam Nevraumont
fuente
1
@alarge si. Pero para que un constructor de movimientos sea muchas veces más eficiente que un constructor de copias, generalmente tiene que mover recursos en lugar de copiarlos. Sin escribir sus propios constructores de movimientos (y simplemente recompilar un programa C ++ 03), todos los stdcontenedores de la biblioteca se actualizarán con moveconstructores "gratis" y (si no lo bloqueó) construcciones que usan dichos objetos ( y dichos objetos) comenzarán a obtener construcción de movimiento libre en una serie de situaciones. Muchas de esas situaciones están cubiertas por elisión en C ++ 03: no todas.
Yakk - Adam Nevraumont
55
Esa es una mala implementación del optimizador, entonces, debido a que los objetos con nombres diferentes que se devuelven no tienen una vida útil superpuesta, RVO es teóricamente posible.
Ben Voigt
2
@alarge Hay lugares donde la elisión falla, como cuando dos objetos con vidas superpuestas se pueden eludir en un tercero, pero no el uno al otro. Luego se requiere mover en C ++ 11 y copiar en C ++ 03 (ignorando como si). Elision es a menudo frágil en la práctica. El uso de los stdcontenedores anteriores se debe principalmente a que son baratos de mover de forma exagerada para copiar el tipo que se obtiene 'gratis' en C ++ 11 al volver a compilar C ++ 03. El vector::resizees una excepción: se usa moveen C ++ 11.
Yakk - Adam Nevraumont
27
Solo veo 1 categoría general que es semántica de movimiento, y 5 casos especiales de eso.
Johannes Schaub - litb
3
@sebro Entiendo que no considera que "hace que los programas no asignen muchos miles de asignaciones de kilobytes, y en su lugar mueve los punteros" para que sean suficientes. Quieres resultados cronometrados. Las microbenchmarks no son más pruebas de mejoras de rendimiento que pruebas de que básicamente está haciendo menos. Menos de unas 100 aplicaciones del mundo real en una amplia variedad de industrias que se perfilan con perfiles de tareas del mundo real no es realmente una prueba. Tomé vagas afirmaciones sobre el "rendimiento libre" y les hice hechos específicos sobre las diferencias en el comportamiento del programa en C ++ 03 y C ++ 11.
Yakk - Adam Nevraumont
46

si tienes algo como:

std::vector<int> foo(); // function declaration.
std::vector<int> v;

// some code

v = foo();

Tienes una copia en C ++ 03, mientras que tienes una asignación de movimiento en C ++ 11. entonces tienes optimización gratis en ese caso.

Jarod42
fuente
44
@Yakk: ¿Cómo se produce la elisión de copia en la asignación?
Jarod42
2
@ Jarod42 También creo que la elisión de copia no es posible en una tarea, ya que el lado izquierdo ya está construido y no hay una forma razonable para que un compilador sepa qué hacer con los datos "antiguos" después de robar los recursos de la derecha lado. Pero tal vez me equivoque, me encantaría encontrar una vez y para siempre la respuesta. Copiar elisión tiene sentido cuando copia la construcción, ya que el objeto es "nuevo" y no hay ningún problema para decidir qué hacer con los datos antiguos. Hasta donde yo sé, la única excepción es esta: "Las asignaciones solo se pueden eludir en función de la regla as-if"
vsoftco
44
El buen código C ++ 03 ya hizo un movimiento en este caso, a través defoo().swap(v);
Ben Voigt
@BenVoigt seguro, pero no todo el código está optimizado, y no todos los lugares donde esto sucede es fácil de alcanzar.
Yakk - Adam Nevraumont
La copia de la elisión puede funcionar en una tarea, como dice @BenVoigt. Mejor término es RVO (optimización del valor de retorno) y solo funciona si foo () se ha implementado así.
DrumM