programar programa ejecutar compilar como c++ gcc intel pragma branch-prediction

c++ - programa - g++ linux



¿Hay alguna sugerencia del compilador para que GCC obligue a la predicción de rama a ir siempre de cierta manera? (7)

Como las otras respuestas han sugerido adecuadamente, puede usar __builtin_expect para darle al compilador una pista sobre cómo organizar el código de ensamblaje. Como señalan los here , en la mayoría de los casos, el ensamblador integrado en su cerebro no será tan bueno como el creado por el equipo de GCC. Siempre es mejor usar datos de perfil reales para optimizar su código, en lugar de adivinar.

En líneas similares, pero aún no se menciona, hay una forma específica de GCC para forzar al compilador a generar código en una ruta "fría". Esto implica el uso de los atributos noinline y cold , que hacen exactamente lo que parecen. Estos atributos solo se pueden aplicar a funciones, pero con C ++ 11, puede declarar funciones lambda en línea y estos dos atributos también se pueden aplicar a funciones lambda.

Aunque esto todavía cae en la categoría general de una microoptimización, y por lo tanto se aplica el consejo estándar (prueba, no lo adivine). Siento que es más útil en general que __builtin_expect . Casi ninguna generación del procesador x86 utiliza sugerencias de predicción de bifurcación ( reference ), por lo que lo único que podrá afectar de todos modos es el orden del código de ensamblaje. Como sabe qué es el código de manejo de errores o "caso límite", puede usar esta anotación para asegurarse de que el compilador nunca prediga una bifurcación y la vinculará lejos del código "activo" al optimizar el tamaño.

Uso de la muestra:

void FooTheBar(void* pFoo) { if (pFoo == nullptr) { // Oh no! A null pointer is an error, but maybe this is a public-facing // function, so we have to be prepared for anything. Yet, we don''t want // the error-handling code to fill up the instruction cache, so we will // force it out-of-line and onto a "cold" path. [&]() __attribute__((noinline,cold)) { HandleError(...); }(); } // Do normal stuff ⋮ }

Aún mejor, GCC ignorará esto automáticamente a favor de la retroalimentación del perfil cuando esté disponible (por ejemplo, al compilar con -fprofile-use ).

Consulte la documentación oficial aquí: https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes

Para las arquitecturas de Intel, ¿hay alguna forma de instruir al compilador GCC para que genere código que siempre fuerce la predicción de ramificaciones de una manera particular en mi código? ¿El hardware Intel incluso admite esto? ¿Qué pasa con otros compiladores o hardwares?

Lo usaría en el código C ++ donde conozco el caso en el que deseo correr rápido y no me importa la ralentización cuando se necesita tomar la otra rama, incluso cuando ha tomado esa rama recientemente.

for (;;) { if (normal) { // How to tell compiler to always branch predict true value? doSomethingNormal(); } else { exceptionalCase(); } }

Como una pregunta de seguimiento para Evdzhan Mustafa, ¿puede la sugerencia solo especificar una sugerencia por primera vez que el procesador encuentra la instrucción, toda la predicción de rama posterior, funciona normalmente?


Con respecto al OP, no, no hay forma en GCC de decirle al procesador que siempre asuma que la rama está o no tomada. Lo que tienes es __builtin_expect, que hace lo que otros dicen que hace. Además, creo que no quiere decirle al procesador si la rama se toma o no siempre . Los procesadores de hoy, como la arquitectura Intel, pueden reconocer patrones bastante complejos y adaptarse de manera efectiva.

Sin embargo, hay veces que desea asumir el control de si se predice que una rama se tomará o no: cuando sepa que el código se denominará "en frío" con respecto a las estadísticas de ramificación.

Un ejemplo concreto: código de gestión de excepciones. Por definición, el código de administración ocurrirá excepcionalmente, pero tal vez cuando ocurra se desee el máximo rendimiento (puede haber un error crítico para solucionarlo lo antes posible), por lo tanto, es posible que desee controlar la predicción predeterminada.

Otro ejemplo: puede clasificar su entrada y saltar al código que maneja el resultado de su clasificación. Si hay muchas clasificaciones, el procesador puede recopilar estadísticas pero las pierde porque la misma clasificación no ocurre lo suficientemente pronto y los recursos de predicción se dedican al código recientemente llamado. Desearía que hubiera una primitiva para decirle al procesador "por favor no dedique recursos de predicción a este código" de la forma en que a veces puede decir "no almacene esto en caché".


GCC admite la función __builtin_expect(long exp, long c) para proporcionar este tipo de característica. Puedes consultar la documentación here .

Donde exp es la condición utilizada c es el valor esperado. Por ejemplo, en tu caso querrías

if (__builtin_expect(normal, 1))

Debido a la sintaxis incómoda, esto generalmente se usa al definir dos macros personalizadas como

#define likely(x) __builtin_expect (!!(x), 1) #define unlikely(x) __builtin_expect (!!(x), 0)

solo para facilitar la tarea.

Eso sí:

  1. esto no es estándar
  2. un predictor de compilación / rama de CPU probablemente sea más hábil que usted para decidir tales cosas, por lo que podría ser una microoptimización prematura

La forma correcta de definir macros probables / improbables en C ++ 11 es la siguiente:

#define LIKELY(condition) __builtin_expect(static_cast<bool>(condition), 1) #define UNLIKELY(condition) __builtin_expect(static_cast<bool>(condition), 0)

Cuando estas macros se definen de esta manera:

#define LIKELY(condition) __builtin_expect(!!(condition), 1)

Eso puede cambiar el significado de las declaraciones if y romper el código. Considere el siguiente código:

#include <iostream> struct A { explicit operator bool() const { return true; } operator int() const { return 0; } }; #define LIKELY(condition) __builtin_expect((condition), 1) int main() { A a; if(a) std::cout << "if(a) is true/n"; if(LIKELY(a)) std::cout << "if(LIKELY(a)) is true/n"; else std::cout << "if(LIKELY(a)) is false/n"; }

Y su salida:

if(a) is true if(LIKELY(a)) is false

Como puede ver, la definición de PROBABLEMENTE usando !! como un elenco para bool rompe la semántica de if .

El punto aquí no es que el operator int() y el operator bool() deberían estar relacionados. Lo cual es una buena práctica.

Más bien, el uso de !!(x) lugar de static_cast<bool>(x) pierde el contexto para las conversiones contextuales de C ++ 11 .


__builtin_expect se puede usar para decirle al compilador en qué dirección espera que vaya una rama. Esto puede influir en cómo se genera el código. Los procesadores típicos ejecutan código más rápido secuencialmente. Entonces si escribes

if (__builtin_expect (x == 0, 0)) ++count; if (__builtin_expect (y == 0, 0)) ++count; if (__builtin_expect (z == 0, 0)) ++count;

el compilador generará código como

if (x == 0) goto if1; back1: if (y == 0) goto if2; back2: if (z == 0) goto if3; back3: ; ... if1: ++count; goto back1; if2: ++count; goto back2; if3: ++count; goto back3;

Si su sugerencia es correcta, esto ejecutará el código sin que se realicen ramas. Se ejecutará más rápido que la secuencia normal, donde cada instrucción if se ramificará alrededor del código condicional y ejecutará tres ramificaciones.

Los procesadores x86 más nuevos tienen instrucciones para las ramas que se espera que se tomen, o para las ramas que se espera que no se tomen (hay un prefijo de instrucciones; no estoy seguro de los detalles). No estoy seguro si el procesador usa eso. No es muy útil, porque la predicción de bifurcación manejará esto muy bien. Así que no creo que puedas influir en la predicción de la rama.


gcc tiene __builtin_expect largo (exp largo, largo c) ( énfasis mío ):

Puede usar __builtin_expect para proporcionar al compilador información de predicción de rama. En general, debe preferir usar comentarios de perfil reales para esto (-fprofile-arcs), ya que los programadores son notoriamente malos para predecir cómo funcionan realmente sus programas . Sin embargo, hay aplicaciones en las que es difícil recopilar estos datos.

El valor de retorno es el valor de exp, que debería ser una expresión integral. La semántica del incorporado es que se espera que exp == c. Por ejemplo:

if (__builtin_expect (x, 0)) foo ();

indica que no esperamos llamar a foo, ya que esperamos que x sea cero. Como está limitado a expresiones integrales para exp, debe usar construcciones como

if (__builtin_expect (ptr != NULL, 1)) foo (*ptr);

al probar puntero o valores de punto flotante.

Como señala la documentación, debe preferir usar comentarios de perfil reales y este artículo muestra un ejemplo práctico de esto y cómo, en su caso, al menos termina siendo una mejora sobre el uso de __builtin_expect . Consulte también ¿Cómo utilizar optimizaciones guiadas por perfil en g ++? .

También podemos encontrar un artículo para principiantes del kernel de Linux sobre las macros del kernel probable () y poco probable () que usan esta función:

#define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0)

Tenga en cuenta el !! usado en la macro podemos encontrar la explicación de esto en ¿Por qué usar !! (condición) en lugar de (condición)? .

El hecho de que esta técnica se use en el kernel de Linux no significa que siempre tenga sentido usarla. Podemos ver en esta pregunta que recientemente respondí la diferencia entre el rendimiento de la función al pasar el parámetro como constante de tiempo de compilación o variable que muchas técnicas de optimización enrolladas a mano no funcionan en el caso general. Necesitamos perfilar el código cuidadosamente para comprender si una técnica es efectiva. Es posible que muchas técnicas antiguas ni siquiera sean relevantes con las optimizaciones modernas del compilador.

Tenga en cuenta que, aunque los componentes incorporados no son portátiles, el sonido metálico también admite __builtin_expect .

También en algunas arquitecturas puede que no haga la diferencia .


No no hay. (Al menos en los procesadores x86 modernos).

__builtin_expect mencionado en otras respuestas influye en la forma en que gcc organiza el código de ensamblaje. No influye directamente en el predictor de rama de la CPU. Por supuesto, habrá efectos indirectos en la predicción de la rama causados ​​por la reordenación del código. Pero en los procesadores x86 modernos no hay instrucciones que le digan a la CPU "suponga que esta rama es / no es tomada".

Consulte esta pregunta para obtener más detalles: ¿ Predicción de rama de prefijo Intel x86 0x2E / 0x3E realmente utilizada?

Para ser claros, __builtin_expect y / o el uso de -fprofile-arcs pueden mejorar el rendimiento de su código, tanto al dar pistas al predictor de ramificación a través del diseño del código (consulte Optimizaciones de rendimiento del ensamblaje x86-64 - Alineación y predicción de ramificación ), y también mejora el comportamiento del caché al mantener el código "improbable" alejado del código "probable".