2009-07-16 15 views
10

Estoy teniendo dificultades para superar mi compilador mediante el ensamblaje en línea.¿Cuál es un ejemplo de una simple función C que se implementa más rápidamente en el ensamblaje en línea?

¿Qué ejemplos son buenos y no artificiales de una función que el compilador tiene dificultades para hacer realmente, muy rápido y simple? Pero eso es relativamente simple de hacer con el ensamblaje en línea.

+7

No para molestarlo, pero hay muchísimas personas que piden optimización y preguntas de velocidad, y muy pocos dicen que lo necesitan porque no cumplen con los requisitos. Aparentemente no hemos vencido en la optimización prematura es la raíz de todo mal "mantra suficiente :) –

+0

Lo que provocó mis preguntas fue que yo estaba dando vueltas con el montaje en línea en el iPhone y que iba a escribir una publicación en el blog sobre eso . Pero no pude por mi vida superar a mi compilador. Así que me llamó la atención ver si hay casos límite conocidos en los que los compiladores producen código ineficiente. –

+1

El ensamblaje de ARM es uno de los conjuntos de instrucciones "más limpios". Parte de la filosofía de los procesadores RISC es no agregar instrucciones que no sean fácilmente utilizadas por el compilador. Tendría que mirar el conjunto de instrucciones de la variante de ARM particular y encontrar códigos de operación que no tienen una traducción clara de C. – NoMoreZealots

Respuesta

7

Como está relacionado con el iPhone y el código de ensamblaje, daré un ejemplo que sería relevante en el mundo del iPhone (y no en algunos asse sse o x86). Si alguien decide escribir código de ensamblaje para alguna aplicación del mundo real, lo más probable es que se trate de algún tipo de procesamiento de señal digital o manipulación de imágenes. Ejemplos: conversión de espacio de color de píxeles RGB, codificación de imágenes al formato jpeg/png o codificación de sonido a mp3, amr o g729 para aplicaciones de voip. En caso de codificación de sonido hay muchas rutinas que no pueden ser traducidas por el compilador a código asm eficiente, simplemente no tienen equivalente en C. Ejemplos de las cosas comúnmente utilizadas en el procesamiento de sonido: matemática saturada, rutinas de acumulación múltiple, multiplicación de matrices .

Ejemplo de saturado agregar: 32-bit firm int tiene rango: 0x8000 0000 < = int32 < = 0x7fff ffff. Si agrega dos entradas, el resultado podría desbordarse, pero esto podría ser inaceptable en ciertos casos en el procesamiento de señales digitales. Básicamente, si el resultado se desborda o desborda saturada, add debe devolver 0x8000 0000 o 0x7fff ffff. Esa sería una función c completa para verificar eso. una versión optimizada del complemento saturada podría ser:

 
int saturated_add(int a, int b) 
{ 
    int result = a + b; 

    if (((a^b) & 0x80000000) == 0) 
    { 
     if ((result^a) & 0x80000000) 
     { 
      result = (a < 0) ? 0x80000000 : 0x7fffffff; 
     } 
    } 
    return result; 
} 

es posible que también se consiguen múltiples if/else para comprobar si hay desbordamiento o en x86 puede comprobar indicador de desbordamiento (que también requiere el uso de ASM). iPhone usa las CPU armv6 o v7 que tienen dsp asm. Por lo tanto, la función saturated_add con múltiples brunches (sentencias if/else) y 2 constantes de 32 bits podría ser una instrucción asm simple que utiliza solo un ciclo de CPU. Por lo tanto, simplemente haciendo que la instrucción saturada use asm podría hacer que el algoritmo completo sea dos o tres veces más rápido (y de menor tamaño). Aquí está el manual QADD: otros QADD

ejemplos de código que a menudo ejecutan en bucles largos son

 
res1 = a + b1*c1; 
res2 = a + b2*c2; 
res3 = a + b3*c3; 

parece que nada no puede ser optimizado aquí, pero en la CPU ARM puede utilizar instrucciones DSP específicos que ¡toma menos ciclos que para hacer una simple multiplicación! Así es, a + b * c con instrucciones específicas podría ejecutar más rápido que simple a * b. Para este tipo de casos, los compiladores simplemente no pueden entender la lógica de su código y no pueden usar estas instrucciones dsp directamente y es por eso que necesita escribir asm manualmente para optimizar el código, PERO solo debe escribir manualmente algunas partes del código que no necesitan ser optimizado ¡Si comienza a escribir bucles simples manualmente, es casi seguro que no le ganará al compilador! Hay varios documentos buenos en la web para el ensamblaje en línea para codificar filtros de abeto, codificación/descodificación de amr, etc.

0

Mi mejor triunfo sobre un compilador fue en una rutina de memcpy simple ... Me salteé muchas cosas de la configuración básica (por ejemplo, no necesitaba mucho de un marco de pila, así que guardo unos ciclos allí), e hizo algunas cosas bastante peludas.

Eso fue hace unos 6 años, con algún compilador propietario de calidad desconocida. Tendré que desenterrar el código que tenía y probarlo contra GCC ahora; No sé si podría ser más rápido, pero no lo descartaría.

Al final, aunque mi memcpy era en promedio aproximadamente 15 veces más rápido que el de nuestra biblioteca C, simplemente lo guardé en mi bolsillo trasero en caso de que lo necesitara. Para mí era un juguete jugar con el ensamblaje de PPC, y el aumento de velocidad no era necesario en nuestra aplicación.

2

Si desea hacer cosas como las operaciones SIMD, es posible que pueda vencer a un compilador. Sin embargo, esto requerirá un buen conocimiento de la arquitectura y del conjunto de instrucciones.

+0

No se puede subestimar la importancia de comprender la arquitectura y el conjunto de instrucciones cuando se trata de ensamblaje. Por lo general, evito el uso de asm, pero aún así apunto a conocer las capacidades de la arquitectura para poder tener una idea del rendimiento teórico disponible. – NoMoreZealots

8

Si no tenemos en cuenta las operaciones SIMD trampa, por lo general, puede escribir el montaje SIMD que rinde mucho mejor que sus habilidades compiladores autovectorization (si es que tiene autovectorization!)

Here's un SSE muy básico (Uno de los x86 Tutoriales de juegos SIMD). Es para el ensamblado en línea de Visual C++.

Editar: Aquí hay un pequeño par de funciones si quieres probarlo por ti mismo. Es el cálculo de un producto de punto de longitud n. Uno está usando instrucciones SSE 2 en línea (sintaxis en línea GCC) el otro es muy básico C.

Es muy muy simple y me sorprendería mucho si un buen compilador no pudiera vectorizar el simple C loop , pero si no lo hace, debería ver una aceleración en el SSE2. La versión SSE 2 probablemente podría ser más rápida si utilizara más registros, pero no quiero extender mis habilidades SSE muy débiles :).

float dot_asm(float *a, float*b, int n) 
{ 
    float ans = 0; 
    int i; 
    // I'm not doing checking for size % 8 != 0 arrays. 
    while(n > 0) { 
    float tmp[4] __attribute__ ((aligned(16))); 

    __asm__ __volatile__(
      "xorps  %%xmm0, %%xmm0\n\t" 
      "movups  (%0), %%xmm1\n\t" 
      "movups  16(%0), %%xmm2\n\t" 
      "movups  (%1), %%xmm3\n\t" 
      "movups  16(%1), %%xmm4\n\t" 
      "add  $32,%0\n\t" 
      "add  $32,%1\n\t" 
      "mulps  %%xmm3, %%xmm1\n\t" 
      "mulps  %%xmm4, %%xmm2\n\t" 
      "addps  %%xmm2, %%xmm1\n\t" 
      "addps  %%xmm1, %%xmm0" 
      :"+r" (a), "+r" (b) 
      : 
      :"xmm0", "xmm1", "xmm2", "xmm3", "xmm4"); 

    __asm__ __volatile__(
     "movaps  %%xmm0, %0" 
     : "=m" (tmp) 
     : 
     :"xmm0", "memory");    

    for(i = 0; i < 4; i++) { 
     ans += tmp[i]; 
    } 
    n -= 8; 
    } 
    return ans; 
} 

float dot_c(float *a, float *b, int n) { 

    float ans = 0; 
    int i; 
    for(i = 0;i < n; i++) { 
    ans += a[i]*b[i]; 
    } 
    return ans; 
} 
+1

SIMD definitivamente no está haciendo trampa. Proporciona un caso claro de dónde los compiladores no se han mantenido al día con el hardware. C no maneja bien el paralelismo de nivel de instrucción. Tal vez puede desenrollar bucles aquí y allá, pero las rutinas más avanzadas necesitan ajustes serios. – NoMoreZealots

+0

Hay muchos compiladores que generarán instrucciones SIMD. – jrockway

+0

Lo harán, para casos limitados. Básicamente, siempre y cuando el código esté escrito con una técnica o algoritmo común. Una vez que el conjunto de instrucciones crece demasiado, el uso óptimo de muchas instrucciones comienza a perderse en el lavado al escribir un compilador u optimizador simplemente debido a la complejidad. Esta fue una gran parte de la base del concepto de procesador "RISC". La optimización es simalar al ajedrez, una computadora puede vencer a la mayoría de las personas, pero se necesita mucho más que una computadora de escritorio para vencer a un gran maestro. – NoMoreZealots

6

menos que sea un assembly guru las probabilidades de golpear el compilador son muy bajo.

Un fragmento desde el enlace anterior,

Por ejemplo, el orientado a bits "XOR % EAX,% EAX" instrucción era la forma más rápido para establecer un registro a cero en las primeras generaciones del x86, pero la mayoría del código es generado por compiladores y compiladores rara vez genera instrucción XOR. Por lo que los diseñadores IA, decidió trasladar la que aparecen con frecuencia compilador instrucciones generadas hasta el frente de la lógica combinatoria decodificación haciendo que el literal "MOVL $ 0,% EAX" instrucción ejecutan más rápidamente que la instrucción XOR.

+4

No soy un gurú de ensamblaje, y supero al compilador. Raramente recurro al montaje.Fue un último recurso cuando tuve que hacerlo. Esto simplemente parece no decirlo. E ignora su pregunta. Él admite que no es fácil en la pregunta. – NoMoreZealots

+1

No dije que fuera imposible. Si asimila el conjunto de instrucciones, puede intentar escribir un código más rápido o apretar la rutina con menos instrucciones. Si tiene un compilador no muy sofisticado o si el compilador no maneja el archivo sse, los conjuntos 3dnow, el ensamblaje de escritura podría ser la forma * apropiada * de implementar algunas rutinas. –

+1

Tiene razón, comprender el conjunto de instrucciones es una necesidad absoluta si quiere tener alguna esperanza de vencer a un compilador. Pero incluso con un buen compilador, puede encontrar instrucciones que no tienen construcciones C que se correspondan bien con ellas en las arquitecturas modernas. Todavía hay "lagunas" en las abstracciones que crecen a medida que el paradigma multinúcleo se convierte en la norma. Y en el mercado impulsado por la energía y consciente de la energía de hoy en día, no podemos suponer velocidades de núcleo de CPU más rápidas en nuestras aplicaciones. Las CPU alcanzan 1 GHz en 1999, y las nuevas aplicaciones que se ejecutan en el disco "más popular" están registrando 400Mhz en la actualidad. – NoMoreZealots

5

Implementé una simple correlación cruzada utilizando una implementación genérica de "Estrecho C". Y ENTONCES cuando tomó más tiempo que el timeslice que tenía disponible, recurrí a la paralelización explícita del algoritmo y al uso del procesador intrínseco para forzar las instrucciones específicas que se utilizarán en los cálculos. Para este caso particular, el tiempo de cálculo se redujo de> 30 ms a poco más de 4 ms. Tenía una ventana de 15 ms para completar el procesamiento antes de que ocurriera la siguiente adquisición de datos.

Esta fue una optimización de tipo SIMD en un procesador VLWI. Esto solo requiere 4 o menos de los intrínsecos del procesador, que son básicamente instrucciones de lenguaje ensamblador que dan la apariencia de una llamada a función en el código fuente. Podría hacer lo mismo con el ensamblaje en línea, pero la sintaxis y la administración de registros son un poco más agradables con los intrínsecos del procesador.

Aparte de eso, si el tamaño importa, el ensamblador es el rey. Fui a la escuela con un tipo que escribió un editor de texto de pantalla completa en menos de 512 bytes.

+0

Este es un caso clásico donde el ensamblador es sensato. El código fue escrito en C; funcionó, pero no lo suficientemente rápido. Recodificar en ensamblador lo hizo funcionar lo suficientemente rápido, esa fue una buena razón para caer en ensamblador. –

+0

Me decepcionó la actuación que obtuve de la versión Estrecho C, la propaganda del vendedor de chips se jactó de lo bueno que era su compilador de C. Y su cadena de herramientas más reciente tampoco sirve para optimizarla. Lamentablemente, los DSP con VLWI no son fáciles de escribir para un optimizador. – NoMoreZealots

5

Tengo un algoritmo de suma de comprobación que requiere que las palabras giren en una cierta cantidad de bits. Para ponerlo en práctica, Tengo esta macro:

//rotate word n right by b bits 
#define ROR16(n,b) (((n)>>(b))|(((n)<<(16-(b)))&0xFFFF)) 

//... and inside the inner loop: 
sum ^= ROR16(val, pos); 

liberación VisualStudio acumulación expande a esto: (val es de hacha, pos está en dx, sum está en BX)

mov   ecx,10h 
sub   ecx,edx 
mov   ebp,eax 
shl   ebp,cl 
mov   cx,dx 
sar   ax,cl 
add   esi,2 
or   bp,ax 
xor   bx,bp 

Cuanto más ensamblador generado a mano equivalente eficiente sería:

mov  cl,dx 
ror  ax,cl 
xor  bx,ax 

no he encontrado la manera de emitir la instrucción ror de 'c' pura código. Sin embargo ...
Mientras escribía esto, recordé los intrínsecos del compilador. Puedo generar el segundo conjunto de instrucciones con:

sum ^= _rotr16(val,pos); 

Así que mi respuesta es: Incluso si usted piensa que puede vencer el compilador C puro, compruebe las características intrínsecas antes de recurrir a inline montaje.

+0

Buen ejemplo concreto. – NoMoreZealots

+0

Intenté esto en gcc (4.0.1) con -O4. Realiza una instrucción ROR para una rotación de 32 bits, pero no para 16 bits. – finnw

Cuestiones relacionadas