c++ gcc optimization assert compiler-optimization

c++ - ¿Cómo guiar las optimizaciones de GCC basadas en afirmaciones sin costo de tiempo de ejecución?



optimization assert (3)

Tengo una macro utilizada en todo mi código que en el modo de depuración hace:

#define contract(condition) / if (!(condition)) / throw exception("a contract has been violated");

... pero en modo de lanzamiento:

#define contract(condition) / if (!(condition)) / __builtin_unreachable();

Lo que esto hace sobre un assert() es que, en las compilaciones de lanzamiento, el compilador puede optimizar el código en gran medida gracias a la propagación de UB.

Por ejemplo, probando con el siguiente código:

int foo(int i) { contract(i == 1); return i; } // ... foo(0);

... lanza una excepción en el modo de depuración, pero produce el ensamblaje para un return 1; incondicional return 1; en modo de lanzamiento:

foo(int): mov eax, 1 ret

La condición, y todo lo que dependía de ella, se ha optimizado.

Mi problema surge con condiciones más complejas. Cuando el compilador no puede probar que la condición no tiene ningún efecto secundario, no la optimiza, lo que es una penalización de ejecución en comparación con no usar el contrato.

¿Hay una manera de expresar que la condición en el contrato no tiene ningún efecto secundario, para que siempre se optimice?


¿Hay una manera de expresar que la condición en el contrato no tiene ningún efecto secundario, para que siempre se optimice?

No es probable.

Se sabe que no puede tomar una gran colección de aserciones, convertirlas en suposiciones (a través de __builtin_unreachable ) y esperar buenos resultados (por ejemplo, las aserciones son pesimistas, las suposiciones son optimistas por John Regehr).

Algunas pistas:

  • CLANG, aunque ya tenía el __builtin_unreachable intrinsic, introdujo __builtin_assume exactamente para este propósito.

  • N4425 - Supuestos dinámicos generalizados (*) señala que:

    GCC no proporciona explícitamente una facilidad de suposición general, pero las suposiciones generales pueden codificarse utilizando una combinación de flujo de control y el __builtin_unreachable intrinsic

    ...

    Las implementaciones existentes que proporcionan supuestos genéricos utilizan alguna palabra clave en el espacio de identificador __assume la implementación ( __assume , __builtin_assume , etc.). Debido a que el argumento de la expresión no se evalúa (los efectos secundarios se descartan), es difícil especificar esto en términos de una función de biblioteca especial (por ejemplo, std::assume ).

  • La biblioteca de soporte de directrices ( GSL , alojada por Microsoft, pero de ninguna manera específica de Microsoft) tiene "simplemente" este código:

    #ifdef _MSC_VER #define GSL_ASSUME(cond) __assume(cond) #elif defined(__clang__) #define GSL_ASSUME(cond) __builtin_assume(cond) #elif defined(__GNUC__) #define GSL_ASSUME(cond) ((cond) ? static_cast<void>(0) : __builtin_unreachable()) #else #define GSL_ASSUME(cond) static_cast<void>(!!(cond)) #endif

    y señala que:

    // GSL_ASSUME(cond) // // Tell the optimizer that the predicate cond must hold. It is unspecified // whether or not cond is actually evaluated.

*) Documento rejected : la guía del EWG fue proporcionar la funcionalidad dentro de las instalaciones del contrato propuesto.


Entonces, no es una respuesta, sino algunos resultados interesantes que pueden llevar a alguna parte.

Terminé con el siguiente código de juguete:

#define contract(x) / if (![&]() __attribute__((pure, noinline)) { return (x); }()) / __builtin_unreachable(); bool noSideEffect(int i); int foo(int i) { contract(noSideEffect(i)); contract(i == 1); return i; }

Puedes seguir en casa , también;)

noSideEffect es la función que sabemos que no tiene efectos secundarios, pero el compilador no.
Fue así:

  1. GCC tiene __attribute__((pure)) para marcar una función que no tiene ningún efecto secundario.

  2. La calificación de noSideEffect con el atributo pure elimina completamente la llamada a la función. ¡Bonito!

  3. Pero no podemos modificar la declaración de noSideEffect . Entonces, ¿qué hay de envolver su llamada dentro de una función que es pure sí misma? Y como estamos tratando de hacer una macro autocontenida, una lambda suena bien.

  4. Sorprendentemente, eso no funciona ... ¡a menos que agreguemos noinline a la lambda! Supongo que el optimizador alinea el lambda primero, perdiendo el atributo pure en el camino, antes de considerar optimizar la llamada a noSideEffect . Con noinline , de una manera un tanto contraintuitiva, el optimizador es capaz de borrar todo. ¡Genial!

  5. Sin embargo, ahora hay dos problemas: con el atributo noinline , el compilador genera un cuerpo para cada lambda, incluso si nunca se usan. Meh - el enlazador probablemente podrá tirarlos de todos modos.
    Pero lo que es más importante ... Perdimos las optimizaciones que __builtin_unreachable() había habilitado :(

Para resumir, puedes eliminar o volver a colocar noinline en el fragmento de código anterior y terminar con uno de estos resultados:

Con noinline

; Unused code foo(int)::{lambda()#2}::operator()() const: mov rax, QWORD PTR [rdi] cmp DWORD PTR [rax], 1 sete al ret foo(int)::{lambda()#1}::operator()() const: mov rax, QWORD PTR [rdi] mov edi, DWORD PTR [rax] jmp noSideEffect(int) ; No function call, but the access to i is performed foo(int): mov eax, edi ret

Sin noinline

; No unused code ; Access to i has been optimized out, ; but the call to `noSideEffect` has been kept. foo(int): sub rsp, 8 call noSideEffect(int) mov eax, 1 add rsp, 8 ret


No hay forma de forzar el código de optimización como si fuera un código muerto, porque GCC siempre tiene que presentar una queja ante el estándar.

Por otro lado, se puede verificar la expresión para que no tenga ningún efecto secundario utilizando el error atributo que mostrará un error cada vez que la llamada de una función no se pueda optimizar.

Un ejemplo de una macro que comprueba lo que está optimizado y realiza la propagación de UB:

#define _contract(condition) / { ([&]() __attribute__ ((noinline,error ("contract could not be optimized out"))) { if (condition) {} // using the condition in if seem to hide `unused` warnings. }()); if (!(condition)) __builtin_unreachable(); }

El atributo de error no funciona sin optimización (por lo que esta macro solo se puede utilizar para la compilación del modo de versión / optimización). Tenga en cuenta que el error que indica lo que el contrato tiene efectos secundarios se muestra durante el enlace.

Una prueba que muestra un error con contrato no optimizable.

Una prueba que optimiza un contrato pero hace propagación de UB con él.