¿Cuál es un ejemplo de una simple función de C que se implementa en más rápido ensamblado en línea?

StackOverflow https://stackoverflow.com/questions/1139030

  •  16-09-2019
  •  | 
  •  

Pregunta

Estoy teniendo dificultades para vencer a mi compilador utilizando ensamblador en línea.

¿Qué es un buen ejemplo, no artificiales de una función que el compilador tiene dificultades para hacer muy, muy rápido y sencillo? Pero eso es relativamente fácil de hacer con el montaje en línea.

¿Fue útil?

Solución

Desde que está relacionado con el iPhone y el código de montaje después daré un ejemplo que sería relevante en el mundo iPhone (y no un sse o x86 ASM). Si alguien decide escribir código ensamblador para algunas aplicaciones del mundo real, lo más probable es que esto va a ser una especie de procesamiento de señal digital o la manipulación de imágenes. Ejemplos: la conversión de espacio de color RGB de píxeles, la codificación de imágenes a formato JPEG / PNG, o el sonido de codificación de MP3, AMR o G729 para aplicaciones VoIP. En caso de codificación de sonido hay muchas rutinas que no se pueden traducir por el compilador para código asm eficiente, que simplemente no tienen equivalente en C. Ejemplos de la materia utilizada comúnmente en procesamiento de sonido: matemáticas saturado, las rutinas de multiplicar-se acumulan, multiplicación de matrices.

Ejemplo de complemento saturado: 32-bit int firmado tiene rango: 0x8000 0000 <= int32 <= 0x7FFF ffff. Si agrega dos enteros resultado podría desbordarse, pero esto podría ser inaceptable en algunos casos en el procesamiento de señales digitales. Básicamente, si se desborda resultado o underflow saturados complemento debería devolver 0x8000 0000 o ffff 0x7FFF. Eso sería una función c completo para comprobar que. 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;
} 

También puede hacer 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 utiliza la CPU ARMv6 o v7 que tienen asm DSP. Por lo tanto, la función saturated_add con múltiples ramales (si / else) y 2 constantes de 32 bits podría ser una instrucción asm simple que utiliza un solo ciclo de la CPU. Por lo tanto, sólo tiene que hacer saturated_add para utilizar la instrucción asm podría hacer que todo algoritmo de dos a tres veces más rápido (y de menor tamaño). Aquí está el manual QADD: QADD

otros 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 como si nada no puede ser optimizado aquí, pero en la CPU ARM puede utilizar instrucciones específicas de DSP que tienen menos ciclos que hacer una simple multiplicación! Así es, a + b * c con instrucciones específicas podría ejecutar más rápido que la simple a * b. Para este tipo de casos compiladores simplemente no puede entender la lógica de su código y no puede utilizar estas instrucciones DSP directamente y por eso tiene que escribir manualmente asm para optimizar el código, pero sólo se debe escribir manualmente algunas partes del código que no tiene por qué ser optimizado. Si usted comienza a escribir bucles simples manualmente entonces es casi seguro que no se puede superar el compilador! Hay varios buenos artículos en la web para el montaje en línea para codificar los filtros FIR, AMR codificación / decodificación, etc.

Otros consejos

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

Aquí es un SSE muy básico ( uno de los escenarios) tutorial de instrucciones SIMD de x86. Es para Visual C ++ de montaje en línea.

Edit: He aquí un pequeño par de funciones, si quieres probar por sí mismo. Es el cálculo de un producto de punto de longitud n. Uno está utilizando SSE 2 instrucciones en línea (GCC en-línea de sintaxis) el otro es muy básico C.

Es muy, muy simple y yo estaría muy sorprendido si un buen compilador no podía vectorizar el bucle C simple, pero si lo hace, no debería ver una velocidad en el SSE2. La versión 2 SSE probablemente podría ser más rápido si utiliza más registros, pero no quiero estirar las 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;
}

A menos que usted es un gurú de montaje las probabilidades de golpear el compilador son muy baja .

Un fragmento desde el enlace anterior,

  

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

I implementado una correlación cruzada simple usando una implementación genérica "estrecho C". Y luego, cuando se tomó más tiempo que la porción de tiempo que tenía disponible, recurrí a la paralelización explícita del algoritmo y el procesador intrínseca para forzar las instrucciones específicas que se utilizarán en los cálculos. Para este caso particular, el tiempo de cálculo era reducir de> 30 ms a poco más de 4 ms. Tenía una ventana de 15 ms para completar el proceso antes de que ocurriera la próxima adquisición de datos.

Esta fue una optimización de tipo SIMD en un procesador VLWI. Esto sólo se requieren 4 o menos de las características intrínsecas del procesador, que son básicamente instrucciones en lenguaje ensamblador que dan la apariencia de una llamada de función en el código fuente. Se podría hacer lo mismo con el montaje en línea, pero la sintaxis y el registro de gestión es un poco más agradable con las características intrínsecas del procesador.

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

Tengo un algoritmo de suma de comprobación que requiere palabras para ser girado por un cierto número 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 está en 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 

El conjunto generado a mano equivalente más 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ódigo puro 'c'. Sin embargo ...
Mientras escribía esto, recordé 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

Si quieres hacer cosas como las operaciones SIMD, que podría ser capaz de vencer a un compilador. Para ello será necesario un buen conocimiento de la arquitectura y el conjunto de instrucciones sin embargo.

Mi mejor ganar a lo largo de un compilador fue en una simple rutina de establecimiento de memoria ... me dio un montón de las cosas configuración básica (por ejemplo, yo no necesitaba mucho de un marco de pila, por lo que ahorrar unos cuantos ciclos de allí) , e hizo algunas cosas muy peludas.

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

Al final, a pesar de que mi memcpy fue en promedio alrededor de 15 veces más rápido que el de nuestra biblioteca de C, sólo guardé en el bolsillo de atrás en caso de que lo necesitara. Era un juguete para mí jugar con el conjunto de PPC y el aumento de velocidad no era necesario en nuestra aplicación.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top