¿Qué código es mejor para la optimización de predicción de sucursales?


10

Dada la predicción de la rama, y ​​también el efecto de las optimizaciones del compilador, ¿qué código tiende a ofrecer un rendimiento superior?

Tenga en cuenta que bRareExceptionPresent representa una condición poco común. No es el camino normal de la lógica.

/* MOST COMMON path must branch around IF clause */

bool SomeFunction(bool bRareExceptionPresent)
{
  // abort before function
  if(bRareExceptionPresent)
  {
     return false;
  }    
  .. function primary body ..    
  return true;
}

/* MOST COMMON path does NOT branch */

bool SomeFunction(bool bRareExceptionPresent)
{
  if(!bRareExceptionPresent)
  {
    .. function primary body ..
  }
  else
  {
    return false;
  }
  return true;
}

99
Voy a arriesgarme aquí y decir que no hay diferencia alguna.
Robert Harvey

77
Esto probablemente depende de la CPU específica para la que está compilando, ya que tienen diferentes arquitecturas de canalización (ranuras de retraso versus ninguna ranura de retraso). Es probable que el tiempo que ha pasado pensando en esto sea mucho más que el tiempo ahorrado al ejecutar: primero el perfil y luego optimizar.

2
Es casi seguro que es una microoptimización prematura.
Robert Harvey

2
@MichaelT Sí, la creación de perfiles es, de hecho, la única forma confiable de saber qué está pasando realmente con el rendimiento del código en el objetivo, la plataforma, dentro de su contexto. Sin embargo, tenía curiosidad sobre si uno era generalmente preferido.
dyasta

1
@RobertHarvey: es una microoptimización prematura, excepto en los casos en que se cumplen ambas condiciones: (1) el ciclo se llama miles de millones (no millones) de veces; y (2) irónicamente, cuando el cuerpo del bucle es pequeño en términos de código de máquina. La condición n. ° 2 significa que la fracción del tiempo dedicado a los gastos generales no es insignificante en comparación con el tiempo dedicado al trabajo útil. La buena noticia es que, por lo general, en situaciones en las que se cumplen ambas condiciones, SIMD (vectorización), que por naturaleza no tiene ramificaciones, resolverá todos los problemas de rendimiento.
rwong

Respuestas:


10

En el mundo de hoy, no importa mucho, si es que lo hace.

La predicción de ramificación dinámica (algo pensado durante décadas (ver Análisis de las cargas de trabajo del sistema de esquema de predicción de ramificación dinámica publicada en 1996)) es bastante común.

Un ejemplo de esto se puede encontrar en el procesador ARM. Desde el Centro de información del brazo sobre predicción de sucursales

Para mejorar la precisión de predicción de rama, se emplea una combinación de técnicas estáticas y dinámicas.

La pregunta entonces es "¿qué es la predicción de ramificación dinámica en el procesador de brazo?" La lectura continua de la predicción de rama dinámica muestra que utiliza un esquema de predicción de 2 bits (descrito en el documento) genera información sobre si la rama se toma con fuerza o debilidad o no.

Con el tiempo (y con el tiempo me refiero a algunos pasos a través de ese bloque), esto acumula información sobre hacia dónde irá el código.

Para la predicción estática , analiza la apariencia del código y la forma en que se realiza la ramificación en la prueba, según una instrucción anterior o una más en el código:

El esquema utilizado en el procesador ARM1136JF-S predice que no se toman todas las ramas condicionales hacia adelante y todas las ramas hacia atrás. Alrededor del 65% de todas las ramas están precedidas por suficientes ciclos sin ramas para predecirse por completo.

Como lo mencionó Sparky, esto se basa en la comprensión de que los bucles con mayor frecuencia son bucles. El bucle se ramifica hacia atrás (tiene una ramificación al final del bucle para reiniciarlo en la parte superior); normalmente lo hace.

El peligro de intentar adivinar el compilador es que no sabes cómo se va a compilar ese código (y optimizarlo). Y en su mayor parte, no importa. Con la predicción dinámica, dos veces a través de la función predecirá un salto sobre la declaración de guardia para un retorno prematuro. Si el rendimiento de dos tuberías enjuagadas es de rendimiento crítico, hay otras cosas de las que preocuparse.

Es probable que el tiempo que lleva leer un estilo sobre el otro sea de mayor importancia: limpiar el código para que un humano pueda leerlo, porque el compilador funcionará bien sin importar cuán desordenado o idealizado sea el código.


77
Una famosa pregunta de stackoverflow mostró que la predicción de rama importa, incluso hoy.
Florian Margaine

3
@FlorianMargaine, aunque sí importa, entrar en una situación en la que realmente importa parece requerir la comprensión de lo que está compilando y cómo funciona (arm vs x86 vs mips ...). Escribir código tratando de hacer esta microoptimización al principio probablemente funcione desde premisas equivocadas y no logre el efecto deseado.

Bueno, por supuesto, no citemos a DK. Pero creo que esta pregunta fue claramente en el sentido de optimización, cuando ya has pasado la etapa de creación de perfiles. :-)
Florian Margaine

2
@MichaelT Buena respuesta, y estoy muy de acuerdo con tu conclusión. Este tipo de optimización previa al perfilado / resumen definitivamente puede ser contraproducente. Termina siendo un juego de adivinanzas, lo que hace que uno tome decisiones de diseño por razones irracionales. Aún así, me sentí curioso; o
dyasta


9

Tengo entendido que la primera vez que la CPU encuentra una rama, predecirá (si es compatible) que no se toman ramas hacia adelante y hacia atrás. La razón de esto es que se supone que se toman bucles (que generalmente se ramifican hacia atrás).

En algunos procesadores, puede dar una pista en las instrucciones de ensamblaje sobre qué ruta es más probable. Los detalles de esto se me escapan en este momento.

Además, algunos compiladores de C también admiten la predicción de rama estática para que pueda decirle al compilador qué rama es más probable. A su vez, puede reorganizar el código generado o usar instrucciones modificadas para aprovechar esta información (o incluso simplemente ignorarla).

__builtin_expect((long)!!(x), 1L)  /* GNU C to indicate that <x> will likely be TRUE */
__builtin_expect((long)!!(x), 0L)  /* GNU C to indicate that <x> will likely be FALSE */

Espero que esto ayude.


3
"Tengo entendido que la primera vez que la CPU encuentra una rama, predecirá (si es compatible) que no se toman ramas hacia adelante y hacia atrás". Este es un pensamiento muy interesante. ¿Tiene alguna evidencia de que esto realmente se implementa en arquitecturas comunes?
blubb

55
Directamente desde la boca del caballo: una rama delantera por defecto es no tomada. Una rama hacia atrás por defecto es tomada . Y desde la misma página: "prefijo 0x3E - predice estáticamente una rama como tomada".
MSalters

¿Existe una plataforma pragma agnóstica que sea equivalente __builtin_expect?
MarcusJ
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.