¿Puede C ++ moderno obtener rendimiento de forma gratuita?

11 minutos de lectura

avatar de usuario
un gran

A veces se afirma que C++ 11/14 puede aumentar el rendimiento incluso cuando solo se 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 rvalue se generan automáticamente o ahora forman parte de la STL. Ahora me pregunto si estos casos ya fueron manejados anteriormente por RVO u optimizaciones de compilador similares.

Entonces, mi pregunta es si podría darme un ejemplo real de una pieza de código C++98 que, sin modificaciones, se ejecute más rápido usando un compilador compatible con las características del nuevo lenguaje. Entiendo que no se requiere un compilador conforme al estándar para hacer la elisión de copia y solo por esa razón, la semántica de movimiento puede generar velocidad, pero me gustaría ver un caso menos patológico, por así decirlo.

EDITAR: Para que quede claro, no estoy preguntando si los compiladores nuevos son más rápidos que los compiladores antiguos, sino si hay un código mediante el cual agregar -std = c ++ 14 a las banderas de mi compilador se ejecutaría más rápido (evite las copias, pero si usted puede pensar en cualquier otra cosa además de la semántica de movimiento, también me interesaría)

  • Recuerde que la elisión de copias y la optimización del valor de retorno se realizan cuando se construye un nuevo objeto usando un constructor de copias. Sin embargo, en un operador de asignación de copia, no hay 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 brindarle la posibilidad de usar 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 lo compila un compilador C ++ 11/14, tal vez sea más rápido porque el compilador es más nuevo.

    – vsoftco

    22 de diciembre de 2014 a las 1:13

  • 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 la semántica de movimiento interno cuando es posible. Por lo tanto, el código que parece idéntico en C++ 98 y C++ 11/14 será (posiblemente) más rápido en el último caso, cada vez que use los objetos de biblioteca estándar, como vectores, listas, etc., y mueva la semántica hace la diferencia.

    – vsoftco

    22 de diciembre de 2014 a las 1:22


  • @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).

    – un gran

    22 de diciembre de 2014 a las 1:26

  • las clases no necesitan ser simples para no tener un constructor de copia. C ++ prospera con la semántica de valores, y el constructor de copias, el operador de asignación, el destructor, etc. deberían ser la excepción.

    – sp2danny

    22 de diciembre de 2014 a las 1:50

  • @Eric Gracias por el enlace, fue interesante. Sin embargo, después de haberlo revisado rápidamente, las ventajas de velocidad parecen provenir principalmente de agregar std::move y mover constructores (lo que requeriría modificaciones al código existente). Lo único realmente relacionado con mi pregunta fue la oración “Obtiene ventajas de velocidad inmediatas simplemente recompilando”, que no está respaldada por ningún ejemplo (sí menciona STL en la misma diapositiva, como lo hice en mi pregunta, pero nada específico ). Estaba pidiendo algunos ejemplos. Si estoy leyendo mal las diapositivas, házmelo saber.

    – un gran

    23 de diciembre de 2014 a las 11:25


avatar de usuario
Yakk – Adam Nevraumont

Conozco 5 categorías generales en las que volver a compilar un compilador C++03 como C++11 puede causar aumentos de rendimiento ilimitados que prácticamente no están relacionados con la calidad de la implementación. Todas estas son 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 fooEl búfer se reasigna en C++ 03, se copia cada vector en bar.

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

En este caso, esto se basa en optimizaciones dentro del std envase vector. En todos los casos a continuación, el uso de std contenedores es solo porque son objetos C++ que tienen eficiente move semántica en C++ 11 “automáticamente” cuando actualiza su compilador. Objetos que no lo bloqueen que contengan un std El contenedor también hereda la mejora automática. move constructores

falla de NRVO

Cuando falla NRVO (denominado optimización del valor de retorno), en C++03 recurre a la copia, en C++11 recurre al 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 devuelto, pero no entre sí. Ambos no se pueden fusionar con el valor devuelto sin fusionarse entre sí.

El problema básico es que la elisión de NRVO es frágil y el código con cambios no está cerca del return el sitio puede tener repentinamente reducciones masivas de rendimiento en ese lugar sin que se emita ningún diagnóstico. En la mayoría de los casos de falla de NRVO, C ++ 11 termina con un movemientras que C++03 termina con una copia.

Devolver un argumento de función

La elisión 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, porque 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 pasar de uno a otro. (En un ejemplo menos de 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 rvalue, 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 un temporal whatever se crea, luego se copia en el vector v. 2 std::string se asignan búferes, cada uno con datos idénticos, y uno se descarta.

En C++ 11 un temporal whatever es creado. los whatever&& push_back sobrecarga entonces moves que temporal en el vector v. Una std::string se asigna el búfer y se mueve al vector. Un vacío std::string se descarta.

Asignación

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

La elisión no puede ocurrir con la asignación, pero sí puede ocurrir el traslado.

std::set<int> some_function();

std::set<int> some_value;

// code

some_value = some_function();

aquí some_function devuelve un candidato para elidir, pero debido a que no se usa para construir un objeto directamente, no se puede elidir. En C++03, lo anterior da como resultado que el contenido del temporal se copie en some_value. En C++ 11, se mueve a some_valueque básicamente es gratis.


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 std contenedores, pero no sintetiza constructores de movimiento en sus tipos.

Así que los tipos que contienen std::vectors y similares no obtienen dichas 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 de Intel de 2013 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 compatible con MSVC2013).

  • @alarge si. Pero para que un constructor de movimiento sea muchas veces más eficiente que un constructor de copia, generalmente tiene que mover recursos en lugar de copiarlos. Sin escribir sus propios constructores de movimiento (y simplemente recompilar un programa C++03), el std todos los contenedores de la biblioteca se actualizarán con move los constructores “gratis” y (si no lo bloqueó) las construcciones que usan dichos objetos (y dichos objetos) comenzarán a tener construcción de movimiento libre en varias situaciones. Muchas de esas situaciones están cubiertas por elisión en C++03: no todas.

    – Yakk – Adam Nevraumont

    22 de diciembre de 2014 a las 4:08

  • 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

    22 de diciembre de 2014 a las 5:46

  • @alarge Hay lugares donde la elisión falla, como cuando dos objetos con vidas superpuestas se pueden elidir en un tercero, pero no entre sí. Luego se requiere mover en C ++ 11 y copiar en C ++ 03 (ignorando como si). La elisión es a menudo frágil en la práctica. El uso de std Los contenedores anteriores se deben principalmente a que son económicos de mover y exoensivos para copiar el tipo que obtiene ‘gratis’ en C++ 11 al volver a compilar C++ 03. los vector::resize es una excepción: utiliza move en C++11.

    – Yakk – Adam Nevraumont

    22 de diciembre de 2014 a las 13:48

  • Solo veo 1 categoría general que es semántica de movimiento, y 5 casos especiales de eso.

    – Johannes Schaub – litb

    27 de diciembre de 2014 a las 12:29

  • @sebro Entiendo, no considera que “hace que los programas no asignen muchos miles de muchas asignaciones de kilobytes, y en su lugar mueva los punteros” como suficiente. Quieres resultados cronometrados. Los microbenchmarks no son más prueba de mejoras en el rendimiento que la prueba de que fundamentalmente está haciendo menos. Aparte de unas 100 aplicaciones del mundo real en una amplia variedad de industrias que se perfilan con tareas del mundo real, la creación de perfiles no es realmente una prueba. Tomé afirmaciones vagas sobre el “desempeño libre” y les hice hechos específicos sobre las diferencias en el comportamiento del programa bajo C++03 y C++11.

    – Yakk – Adam Nevraumont

    14 de enero de 2019 a las 11:47

avatar de usuario
jarod42

si tienes algo como:

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

// some code

v = foo();

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

  • @Yakk: ¿Cómo ocurre la elisión de copia en la asignación?

    – Jarod42

    22 de diciembre de 2014 a las 1:41

  • @ Jarod42 También creo que la elisión de copia no es posible en una asignación, 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 saber de una vez por todas la respuesta. La elisión de copia tiene sentido cuando copia la construcción, ya que el objeto es “nuevo” y no hay problema de decidir qué hacer con los datos antiguos. Hasta donde yo sé, la única excepción es esta: “Las asignaciones solo se pueden eliminar en función de la regla como si”

    – vsoftco

    22 de diciembre de 2014 a las 1:47


  • El buen código C ++ 03 ya hizo un movimiento en este caso, a través de foo().swap(v);

    – Ben Voigt

    22 de diciembre de 2014 a las 5:47

  • @BenVoigt seguro, pero no todo el código está optimizado, y no todos los lugares donde esto sucede son fáciles de alcanzar.

    – Yakk – Adam Nevraumont

    22 de diciembre de 2014 a las 13:43

  • La elisión de copia puede funcionar en una tarea, como dice @BenVoigt. El mejor término es RVO (optimización del valor de retorno) y solo funciona si foo() se ha implementado así.

    – DrumM

    13 mayo 2018 a las 11:30


¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad