2009-10-24 9 views
16

A veces, un bucle donde la CPU pasa la mayor parte del tiempo tiene algunas fallas de predicción de ramificaciones (error de predicción) muy a menudo (probabilidad de cerca de .5). He visto algunas técnicas en hilos muy aislados pero nunca en una lista. Los que conozco ya arreglan situaciones donde la condición se puede convertir en un bool y que 0/1 se usa de alguna manera para cambiar. ¿Hay otras ramas condicionales que se pueden evitar?¿Qué técnicas para evitar la ramificación condicional, sabes?

p. Ej. (Pseudocódigo)

loop() { 
    if (in[i] < C) 
    out[o++] = in[i++] 
    ... 
} 

pueden reescribirse, podría decirse que perder algo de lectura, con algo como esto:

loop() { 
    out[o] = in[i] // copy anyway, just don't increment 
    inc = in[i] < C // increment counters? (0 or 1) 
    o += inc 
    i += inc 
} 

También he visto técnicas en el cambio de &&-& en condicional en ciertos contextos salvaje escapando de mi mente ahora mismo. Soy un novato en este nivel de optimización, pero seguro que se siente como si tuviera que haber más.

+0

Mal ejemplo. Incluso si el código sin sucursales se puede ver como equivalente al original, eso es solo si el código original no tenía ningún sentido en primer lugar. – AnT

+1

¿Por qué tanta gente responde con una respuesta que no responde realmente? – jasonk

Respuesta

11

Creo que la forma más común de evitar la bifurcación es aprovechar el paralelismo de bits para reducir el total de saltos presentes en el código. Cuanto más largos sean los bloques básicos, con menor frecuencia se vaciará la tubería.

Como alguien más ha mencionado, si desea hacer algo más que desenrollar bucles, y proporcionar sugerencias de bifurcación, querrá lanzarse al ensamblaje. Por supuesto, esto debe hacerse con la máxima precaución: el compilador típico puede escribir mejor ensamblaje en la mayoría de los casos que un humano. Su mejor esperanza es eliminar asperezas y hacer suposiciones que el compilador no puede deducir.

Aquí hay un ejemplo del siguiente código C:

if (b > a) b = a; 

En el montaje sin ningún tipo de saltos, mediante el uso de de manipulación de bits (y comentando extrema):

sub eax, ebx ; = a - b 
sbb edx, edx ; = (b > a) ? 0xFFFFFFFF : 0 
and edx, eax ; = (b > a) ? a - b : 0 
add ebx, edx ; b = (b > a) ? b + (a - b) : b + 0 

Nota que mientras condicionales movimientos son Los entusiastas de la asamblea se adelantaron inmediatamente, eso es solo porque se entienden fácilmente y brindan un concepto de lenguaje de nivel más alto en una sola instrucción conveniente. No son necesariamente más rápidos, no están disponibles en los procesadores más antiguos, y mapeando su código C en las correspondientes instrucciones de movimiento condicional, usted está haciendo el trabajo del compilador.

+0

Hm, ¿su código ensamblador no asume ningún desbordamiento en 'sub eax, exb'? – Deduplicator

7

La generalización del ejemplo que usted da es "reemplace la evaluación condicional con matemáticas"; la evitación de la rama condicional se reduce a eso.

Lo que está sucediendo con la sustitución de && con & es que, dado que && es un cortocircuito, constituye una evaluación condicional en sí misma. & obtiene los mismos resultados lógicos si ambos lados son 0 o 1, y no es un cortocircuito. Lo mismo se aplica a || y | excepto que no necesita asegurarse de que los lados están limitados a 0 o 1 (nuevamente, solo por motivos lógicos, es decir, está utilizando el resultado solo Booleanly).

4

GCC ya es lo suficientemente inteligente como para reemplazar los condicionales con instrucciones más simples. Por ejemplo, los procesadores Intel más nuevos proporcionan cmov (movimiento condicional). Si puede usarlo, SSE2 proporciona algunas instrucciones al compare 4 integers (u 8 pantalones cortos, o 16 caracteres) a la vez.

Adicionalmente para calcular mínimo que se puede utilizar (ver estos magic tricks):

min(x, y) = x+(((y-x)>>(WORDBITS-1))&(y-x)) 

Sin embargo, prestar atención a cosas como:

c[i][j] = min(c[i][j], c[i][k] + c[j][k]); // from Floyd-Warshal algorithm 

incluso sin saltos están implicadas es mucho más lento que

int tmp = c[i][k] + c[j][k]; 
if (tmp < c[i][j]) 
    c[i][j] = tmp; 

Mi mejor estimación es que en el primer fragmento se contamina el cach e más a menudo, mientras que en el segundo no lo haces.

+4

Tenga en cuenta que 'cmov' tiene la desventaja de ser considerado como dependiente de su operando de origen desde el punto de vista del reordenamiento de instrucciones y la ejecución paralela. Para una condición que a menudo es falsa, un salto condicional bien pronosticado puede ser más rápido que un 'cmov' estancado. –

2

En mi opinión, si está llegando a este nivel de optimización, es probable que sea hora de pasar directamente al lenguaje ensamblador.

Esencialmente usted está contando con el compilador que genera un patrón de ensamblaje específico para aprovechar esta optimización en C de todos modos. Es difícil adivinar exactamente qué código generará un compilador, por lo que tendrías que mirarlo cada vez que se realice un pequeño cambio, ¿por qué no hacerlo en el ensamblado y terminar con él?

+0

Es cierto. Es por eso que la etiqueta de ensamblaje. Si tienes técnicas en ensamble para este tipo de optimización, sería muy apreciado si puedes compartir (¡también enlaces!) – alecco

+2

No estoy seguro de que haya mucho que pueda compartir: mi ensamblaje está principalmente en el lado de la lectura (cuando se depura) o haciendo cosas a nivel de hardware que no se pueden hacer en C (no optimización) en sistemas integrados. Una cosa que me viene a la cabeza es específica de ARM, y no es un gran truco. Las instrucciones ARM tienen un campo para permitir que se ejecuten de forma condicional, por lo que en lugar de tener que saltar a su alrededor, se convierten en NOP sin efecto en la línea de instrucción. –

1

Este nivel de optimización es poco probable que haga una diferencia que valga la pena en todas las zonas menos calientes, excepto en las más populares.Suponiendo que sí (sin probarlo en un caso específico) es una forma de conjeturando, y la primera regla de optimización es no actúe en conjeturas.

+0

Creo que el ejemplo en la pregunta es bastante real y está lejos de conjeturar. De hecho, está justo allí en este código. Esto es, por supuesto, para los componentes más internos de los circuitos cerrados para comprimir/clasificar/buscar, por lo que definitivamente es un punto de acceso. No está optimizando hello-world solo para patadas. Gracias. – alecco

+1

@aleccolocco: Esto es lo que quiero decir. Elija un programa real, no uno creado solo para hacer una pregunta. Haga algo de ajuste de rendimiento para realmente exprimirlo. Problemas como la predicción de ramificación no entran hasta que todo lo demás se agota, por lo que comenzar con la suposición de que realmente importan no se basa en saber cuáles son realmente los problemas. http: // stackoverflow.com/questions/926266/performance-optimization-strategies-of-last-resort/927773 # 927773 –

+1

... al mismo tiempo, cuando llegas a puntos de acceso como ese, tienes razón, pueden marcar la diferencia. (Lo siento. Para mí es un tema candente que mucha gente parece pensar que la optimización comienza y termina en el nivel bajo, cuando eso es solo la punta del iceberg). –

3

En este nivel, las cosas dependen del hardware y del compilador. ¿El compilador que está utilizando es lo suficientemente inteligente como para compilar < sin flujo de control? gcc en x86 es lo suficientemente inteligente; lcc no es. En conjuntos de instrucciones anteriores o incorporados, es posible que no se pueda calcular < sin flujo de control.

Más allá de esta advertencia tipo Cassandra, es difícil hacer declaraciones generales útiles. Así que aquí están algunas declaraciones generales que pueden ser poco útil:

  • de hardware rama de predicción moderna es terriblemente buena. Si pudieras encontrar un programa real en el que la predicción de una sucursal defectuosa costara más de un 1% -2% de desaceleración, estaría muy sorprendido.

  • Los contadores de rendimiento u otras herramientas que le indican dónde encontrar errores en las bifurcaciones son indispensables.

  • Si realmente se necesita para mejorar dicho código, me vería en la programación de traza y bucle desenrollado:

    • desenrollado Loop replica cuerpos de bucle y le da a su optimizador de un mayor flujo de control para trabajar con ellos.

    • La planificación de trazas identifica qué rutas son más probables de tomar, y entre otros trucos, puede ajustar las direcciones de las ramas para que el hardware de predicción de ramificaciones funcione mejor en las rutas más comunes. Con bucles desenrollados, hay más y más largos caminos, por lo que el planificador de seguimiento tiene más para trabajar con

  • me gustaría ser recelosos de intentar codificar esto mismo en el montaje. Cuando salga el próximo chip con nuevo hardware de predicción de bifurcaciones, las posibilidades son excelentes de que todo su trabajo arduo se esfume. En su lugar, buscaría un compilador de optimización dirigido a comentarios.

+0

Genial, gracias! Estoy haciendo compresión SIMD, ordenando y buscando en grandes conjuntos de datos. Hace una diferencia cuando la probabilidad es de aproximadamente .5 (por eso está en la pregunta al principio). Bien, guarde Itanium o arquitecturas así, pero ese no es mi caso. La naturaleza de los datos variará significativamente ya que no está especializada para un tipo de conjunto de datos (podría ser aleatorio, incremental, etc.) Por lo tanto, la retroalimentación ayudará hasta cierto punto. Y hay muchos casos como el ejemplo en la pregunta que se puede resolver fácilmente sin siquiera sumergirse en el ensamblaje. Esa es mi búsqueda :) – alecco

1

La mayoría de los procesadores proporcionan una predicción de bifurcación superior al 50%. De hecho, si obtienes una mejora del 1% en la predicción de ramas, entonces probablemente puedas publicar un documento. Hay una montaña de documentos sobre este tema si está interesado.

Es mejor que se preocupe por los éxitos y errores en la caché.

+1

He encontrado que, al menos en algunos casos, la solución para omitir la predicción de bifurcación a menudo también es mejor para el rendimiento de la memoria caché. Puede ser un ganar-ganar. –

2

Una extensión de la técnica demostrada en la pregunta original se aplica cuando tiene que hacer varias pruebas anidadas para obtener una respuesta. Puedes construir una pequeña máscara de bits a partir de los resultados de todas las pruebas y "buscar" la respuesta en una tabla.

if (a) { 
    if (b) { 
    result = q; 
    } else { 
    result = r; 
    } 
} else { 
    if (b) { 
    result = s; 
    } else { 
    result = t; 
    } 
} 

Si a y b son casi al azar (por ejemplo, a partir de datos arbitrario), y esto es en un bucle estrecho, entonces fallos de predicción de ramificación realmente pueden retrasar este hacia abajo. Puede escribirse como:

// assuming a and b are bools and thus exactly 0 or 1 ... 
static const table[] = { t, s, r, q }; 
unsigned index = (a << 1) | b; 
result = table[index]; 

Puede generalizar esto a varios condicionales. Lo he visto hecho para 4. Sin embargo, si el anidamiento es tan profundo, debes asegurarte de que probarlos a todos es realmente más rápido que hacer solo las pruebas mínimas sugeridas por la evaluación de cortocircuito.

9

Usando el ejemplo de Matt Joiner:

if (b > a) b = a; 

También puede hacer lo siguiente, sin tener que excavar en código ensamblador:

bool if_else = b > a; 
b = a * if_else + b * !if_else; 
Cuestiones relacionadas