c++ - tipo - noexcept, desenrollado y rendimiento de la pila
pilas de combustibles pdf (3)
Hay un "no" sobrecarga y luego no hay gastos generales. Puedes pensar en el compilador de diferentes maneras:
- Genera un programa que realiza ciertas acciones.
- Genera un programa que satisface ciertas restricciones.
El TR dice que no hay gastos generales en el presupuesto basado en tablas porque no es necesario tomar medidas siempre que no se produzca un lanzamiento. El camino de ejecución no excepcional va directo hacia adelante.
Sin embargo, para que las tablas funcionen, el código no excepcional aún necesita restricciones adicionales. Cada objeto debe inicializarse por completo antes de que cualquier excepción pueda conducir a su destrucción, lo que limita el reordenamiento de las instrucciones (por ejemplo, desde un constructor integrado) a través de potenciales llamadas. Del mismo modo, un objeto debe ser completamente destruido antes de cualquier posible excepción posterior.
El desenrollado basado en tablas solo funciona con funciones que siguen las convenciones de llamadas ABI, con marcos de pila. Sin la posibilidad de una excepción, el compilador puede haber sido libre de ignorar el ABI y omitir el marco.
La sobrecarga de espacio, también llamada saturación, en forma de tablas y rutas de código excepcionales separadas, puede no afectar el tiempo de ejecución, pero aún puede afectar el tiempo necesario para descargar el programa y cargarlo en la RAM.
Todo es relativo, pero no noexcept
al compilador un poco de holgura.
El siguiente draft del nuevo libro C ++ 11 de Scott Meyers dice (página 2, líneas 7-21)
La diferencia entre desenrollar la pila de llamadas y posiblemente desenrollarla tiene un impacto sorprendentemente grande en la generación de código. En una función noexcept, los optimizadores no necesitan mantener la pila de tiempo de ejecución en un estado no enrollable si una excepción se propagara fuera de la función, ni deben garantizar que los objetos en una función noexcept se destruyan en el orden inverso de construcción si una excepción deja la función . El resultado es más oportunidades para la optimización, no solo dentro del cuerpo de una función noexcept, sino también en los sitios donde se llama a la función. Dicha flexibilidad está presente solo para las funciones sin excepción. Las funciones con especificaciones de excepción "throw ()" carecen de ella, al igual que las funciones sin especificación de excepción.
Por el contrario, la sección 5.4
de "Informe técnico sobre el rendimiento de C ++" describe las formas de "código" y "tabla" para implementar el manejo de excepciones. En particular, se muestra que el método de "tabla" no tiene sobrecarga de tiempo cuando no se lanzan excepciones y solo tiene una sobrecarga de espacio.
Mi pregunta es esta: ¿de qué optimizaciones habla Scott Meyers cuando habla de desenrollar y posiblemente desenrollarse? ¿Por qué estas optimizaciones no se aplican a throw()
? ¿Sus comentarios se aplican solo al método de "código" mencionado en el TR 2006?
La diferencia entre noexcept
y throw()
es que en el caso de throw()
la pila de excepciones aún se desenrolla y se invocan destructores, por lo que la implementación debe hacer un seguimiento de la pila (ver 15.5.2 The std::unexpected() function
en el estandar).
Por el contrario, std::terminate()
no requiere que la pila se desenrolle ( 15.5.1
establece que está definida por la implementación ya sea que la pila se desenrolle o no antes de llamar a std::terminate()
).
GCC parece realmente no desenrollar la pila para no noexcept
: Demo
Mientras clang todavía se desenrolla: Demo
(Puede comentar f_noexcept()
y descomentar f_emptythrow()
en las demostraciones para ver que para throw()
tanto GCC como clang desenrollen la pila)
Toma el siguiente ejemplo:
#include <stdio.h>
int fun(int a) {
int res;
try
{
res = a *11;
if(res == 33)
throw 20;
}
catch (int e)
{
char *msg = "error";
printf(msg);
}
return res;
}
int main(int argc, char** argv) {
return fun(argc);
}
los datos pasados como entrada no son previsibles desde la perspectiva del compilador y, por lo tanto, no se pueden hacer suposiciones incluso con optimizaciones de -O3
para eludir por completo la llamada o el sistema de excepción.
En LLVM IR, la función de fun
se traduce aproximadamente como
define i32 @_Z3funi(i32 %a) #0 {
entry:
%mul = mul nsw i32 %a, 11 // The actual processing
%cmp = icmp eq i32 %mul, 33
br i1 %cmp, label %if.then, label %try.cont // jump if res == 33 to if.then
if.then: // lots of stuff happen here..
%exception = tail call i8* @__cxa_allocate_exception(i64 4) #3
%0 = bitcast i8* %exception to i32*
store i32 20, i32* %0, align 4, !tbaa !1
invoke void @__cxa_throw(i8* %exception, i8* bitcast (i8** @_ZTIi to i8*), i8* null) #4
to label %unreachable unwind label %lpad
lpad:
%1 = landingpad { i8*, i32 } personality i8* bitcast (i32 (...)* @__gxx_personality_v0 to i8*)
catch i8* bitcast (i8** @_ZTIi to i8*)
... // also here..
invoke.cont:
... // and here
br label %try.cont
try.cont: // This is where the normal flow should go
ret i32 %mul
eh.resume:
resume { i8*, i32 } %1
unreachable:
unreachable
}
como puede ver la ruta de código, incluso si es directa en el caso de un flujo de control normal (sin excepciones), ahora consta de varias ramas de bloques básicos en la misma función.
Es cierto que en tiempo de ejecución casi no se asocia ningún costo, ya que pagas por lo que usas (si no lo haces, no ocurre nada extra), pero tener varias sucursales también puede perjudicar tu rendimiento, por ejemplo
- predicción de rama se vuelve más difícil
- la presión de registro podría aumentar sustancialmente
- [otros]
y seguramente no puede ejecutar optimizaciones de paso a paso entre el flujo de control normal y los puntos de entrada de las plataformas de aterrizaje / excepción.
Las excepciones son un mecanismo complejo y no noexcept
facilita en gran medida la vida del compilador incluso en el caso de EH de costo cero.
Editar: en el caso específico del especificador no noexcept
, si el compilador no puede " demostrar " que su código no arroja, se configura un EH std::terminate
(con detalles que dependen de la implementación). En ambos casos (el código no arroja y / o no puede probar que el código no arroja) los mecanismos involucrados son más simples y el compilador tiene menos limitaciones. De todos modos, realmente no se usa sin noexcept
por razones de optimización, también es una indicación semántica importante.