c++ - ¿Debo usar si es poco probable que se produzcan errores graves?
gcc c++17 (4)
A menudo me encuentro escribiendo un código que se parece a esto:
if(a == nullptr) throw std::runtime_error("error at " __FILE__ ":" S__LINE__);
¿Debería preferir manejar los errores if unlikely
?
if unlikely(a == nullptr) throw std::runtime_error("error at " __FILE__ ":" S__LINE__);
¿El compilador deducirá automáticamente qué parte del código debe almacenarse en la memoria caché o es algo realmente útil? ¿Por qué no veo a muchas personas manejando errores como este?
¿Debo usar "si es improbable" para errores graves?
Para casos como esos, prefiero mover código que se lance a una función externa independiente que esté marcada como noreturn
. De esta manera, su código real no está "contaminado" con muchos códigos relacionados con excepciones (o cualquiera que sea su código de "bloqueo duro"). Contrariamente a la respuesta aceptada, no necesita marcarla como cold
, pero realmente necesita noreturn
para hacer que el compilador no intente generar código para preservar registros o cualquier estado y, en esencia, asumir que después de ir allí no hay vuelta atrás.
Por ejemplo, si escribes código de esta manera:
#include <stdexcept>
#define _STR(x) #x
#define STR(x) _STR(x)
void test(const char* a)
{
if(a == nullptr)
throw std::runtime_error("error at " __FILE__ ":" STR(__LINE__));
}
el compilador generará muchas instrucciones relacionadas con la construcción y el lanzamiento de esta excepción. También introduces dependencia en std::runtime_error
. Vea cómo se verá el código generado si tiene solo tres controles como ese en su función de test
:
Primera mejora: moverlo a una función independiente :
void my_runtime_error(const char* message);
#define _STR(x) #x
#define STR(x) _STR(x)
void test(const char* a)
{
if (a == nullptr)
my_runtime_error("error at " __FILE__ ":" STR(__LINE__));
}
De esta manera, evitará generar todo ese código relacionado con la excepción dentro de su función. De inmediato, las instrucciones generadas se vuelven más simples y limpias y reducen el efecto sobre las instrucciones que genera su código real donde realiza las comprobaciones:
Todavía hay espacio para mejorar. Como sabe que my_runtime_error
no regresará, debe informarle al compilador, para que no tenga que conservar los registros antes de llamar a my_runtime_error
:
#if defined(_MSC_VER)
#define NORETURN __declspec(noreturn)
#else
#define NORETURN __attribute__((__noreturn__))
#endif
void NORETURN my_runtime_error(const char* message);
...
Cuando lo usa varias veces en su código, puede ver que el código generado es mucho más pequeño y reduce el efecto sobre las instrucciones que genera su código real:
Como puede ver, de esta manera el compilador no necesita conservar los registros antes de llamar a my_runtime_error
.
También sugeriría no concatenar cadenas de error con __FILE__
y __LINE__
en cadenas de mensaje de error monolíticas. ¡Páselos como parámetros independientes y simplemente haga una macro que los pase!
void NORETURN my_runtime_error(const char* message, const char* file, int line);
#define MY_ERROR(msg) my_runtime_error(msg, __FILE__, __LINE__)
void test(const char* a)
{
if (a == nullptr)
MY_ERROR("error");
if (a[0] == ''a'')
MY_ERROR("first letter is ''a''");
if (a[0] == ''b'')
MY_ERROR("first letter is ''b''");
}
Puede parecer que hay más código generado por cada llamada my_runtime_error (2 instrucciones más en caso de compilación x64), pero el tamaño total es en realidad menor, ya que el tamaño guardado en cadenas constantes es mucho más grande que el tamaño de código adicional.
Además, tenga en cuenta que estos ejemplos de código son buenos para mostrar el beneficio de hacer que su función de "colisión" sea un externo. La necesidad de noreturn
vuelve más obvia en el código real, por ejemplo :
#include <math.h>
#if defined(_MSC_VER)
#define NORETURN __declspec(noreturn)
#else
#define NORETURN __attribute__((noreturn))
#endif
void NORETURN my_runtime_error(const char* message, const char* file, int line);
#define MY_ERROR(msg) my_runtime_error(msg, __FILE__, __LINE__)
double test(double x)
{
int i = floor(x);
if (i < 10)
MY_ERROR("error!");
return 1.0*sqrt(i);
}
Intente eliminar NORETURN
, o cambie __attribute__((noreturn))
a __attribute__((cold))
y verá un ensamblaje generado completamente diferente .
Como último punto (que es obvio IMO y se omitió). my_runtime_error
definir su función my_runtime_error
en algún archivo cpp. Dado que solo será una copia, puede poner el código que desee en esta función.
void NORETURN my_runtime_error(const char* message, const char* file, int line)
{
// you can log the message over network,
// save it to a file and finally you can throw it an error:
std::string msg = message;
msg += " at ";
msg += file;
msg += ":";
msg += std::to_string(line);
throw std::runtime_error(msg);
}
Un punto más: clang realmente reconoce que este tipo de función se beneficiaría de noreturn
y advierte al respecto si se habilita la advertencia -Wmissing-noreturn
:
advertencia: la función ''my_runtime_error'' podría declararse con el atributo ''noreturn'' [-Wmissing-noreturn] {^
Depende.
En primer lugar, definitivamente puede hacer esto, y esto probablemente no dañará el rendimiento de su aplicación. Pero tenga en cuenta que los atributos probables / improbables son específicos del compilador, y deben ser decorados en consecuencia.
En segundo lugar, si desea una ganancia de rendimiento, el resultado dependerá de la plataforma de destino (y del backend del compilador correspondiente). Si estamos hablando de la arquitectura x86 ''predeterminada'', no obtendrá una gran ganancia con los chips modernos. El único cambio que estos atributos producirán es un cambio en el diseño del código (a diferencia de los tiempos anteriores cuando x86 admitía la predicción de rama de software) . Para sucursales pequeñas (como su ejemplo), tendrá muy poco efecto en la utilización de la memoria caché y / o en las latencias frontales.
ACTUALIZAR:
¿El compilador deducirá automáticamente qué parte del código debe almacenarse en la memoria caché o es algo realmente útil?
Este es en realidad un tema muy amplio y complicado. Lo que hará el compilador dependerá del compilador particular, su backend (arquitectura de destino) y las opciones de compilación. Nuevamente, para x86, aquí está la siguiente regla (tomada del Manual de referencia de optimización de arquitecturas Intel® 64 e IA-32 ):
Regla de codificación de ensamblaje / compilador 3. (Impacto M, generalidad H) Organice el código para que sea coherente con el algoritmo de predicción de ramificación estática: haga que el código directo a continuación de una ramificación condicional sea el objetivo probable de una rama con un objetivo hacia adelante, y haga el código de caída que sigue a una rama condicional puede ser el objetivo improbable de una rama con un objetivo hacia atrás.
Por lo que yo sé, eso es lo único que queda de la predicción de rama estática en el x86 moderno, y los atributos probables / improbables solo podrían usarse para "sobrescribir" este comportamiento predeterminado.
Si tu puedes hacerlo. Pero incluso mejor es mover el throw
a una función separada y marcarlo con __attribute__((cold, noreturn))
. Esto eliminará la necesidad de decir unlikely()
en cada sitio de llamada, y puede mejorar la generación de código al mover la lógica de lanzamiento de excepciones completamente fuera del camino feliz, mejorando la eficiencia de la caché de instrucciones y las posibilidades de integración.
Si prefiere usar un unlikely()
para la notación semántica (para facilitar la lectura del código), también está bien, pero no es óptimo por sí solo.
Ya que de todos modos te estás "estrellando duro", me gustaría ir con
#include <cassert>
...
assert(a != nullptr);
Esto es independiente del compilador, debería brindarle un rendimiento cercano al óptimo, le brinda un punto de interrupción cuando se ejecuta en un depurador, genera un volcado de NDEBUG
cuando no está en un depurador y se puede desactivar configurando el símbolo del preprocesador NDEBUG
, que muchos sistemas de compilación hacen. por defecto para las versiones de lanzamiento.