393 votos

Cuando se ensamblador más rápido que C?

Una de las razones mencionadas para saber ensamblador es que, en ocasiones, puede ser empleado para escribir el código que va a ser más eficientes que escribir ese código en un lenguaje de alto nivel, C) en particular. Sin embargo, también he oído que se dijo muchas veces que a pesar de que no es completamente falso, los casos en ensamblador puede en realidad ser utilizado para generar más eficientes código son extremadamente raros y requieren conocimientos especializados y experiencia en ensamblador.

Esta pregunta no conseguir incluso en el hecho de que el ensamblador instrucciones serán específicos de la máquina y no portátiles, o cualquiera de los otros aspectos de ensamblador. Hay un montón de buenas razones para saber ensamblador, además de éste, por supuesto, pero este está destinado a ser una pregunta específica solicitar ejemplos y datos, no en un largo discurso en ensamblador frente de más alto nivel de idiomas.

¿Alguien puede proporcionar algunos ejemplos específicos de casos donde ensamblador será más rápido que bien escrito el código en C utilizando un moderno compilador, y se puede apoyar esa afirmación con perfiles de evidencia? Estoy bastante seguro de estos casos existen, pero realmente quiero saber exactamente cómo esotérico estos casos, ya que parece ser un punto de contienda alguna.

231voto

Nils Pipenbrinck Puntos 41006

Aquí está un ejemplo del mundo real: punto Fijo se multiplica.

Estos no sólo útil en dispositivos sin necesidad de punto flotante, brillan cuando se trata de precisión, ya que te dan de 32 bits de precisión, con un error de predicción (float sólo tiene 23 bits y es más difícil predecir la pérdida de precisión)

Una forma de escribir un punto fijo se multiplican en una de 32 bits de la arquitectura se parece a esto:

int inline FixedPointMul (int a, int b)
{
  long long a_long = a; // cast to 64 bit.

  long long product = a_long * b; // perform multiplication

  return (int) (product >> 16);  // shift by the fixed point bias
}

El problema con este código es que hagamos algo que no puede ser expresado directamente en lenguaje C. Queremos multiplicar dos números de 32 bits y obtener una de 64 bits resultado de que nos devuelva el medio de 32 bits. Sin embargo, en C este multiplicar no existe. Todo lo que puedes hacer es promover los números enteros de 64 bits y hacer un 64*64 = 64 multiplicar.

El x86 (ARM, MIPS y otros), se puede hacer el multiplican en una sola instrucción. Muchos de los compiladores aún ignoran este hecho y generar el código que llama a una biblioteca de tiempo de ejecución de la función para hacer la multiplicación. El cambio por 16 también se suele hacer por una rutina de biblioteca (también el x86 puede hacer tales cambios).

Así que nos quedamos con uno o dos llamadas a la biblioteca sólo para multiplicar. Esto tiene serias consecuencias. No sólo es el cambio más lento, los registros deben ser conservados a través de las llamadas de función y no ayuda inline y el código de desenrollar el condón.

Si usted volver a escribir el mismo código en ensamblador se puede obtener una considerable mejora en la velocidad.

En adición a esto: el uso de ASM no es la mejor manera de resolver el problema. La mayoría de los compiladores de permitir el uso de algunos ensamblador instrucciones en forma intrínseca si no se puede expresar en C. La VS.NET2008 compilador, por ejemplo, expone el 32*32=64 bits mul como __emul y la versión de 64 bits de turno como __ll_rshift.

El uso de los elementos intrínsecos puede reescribir la función de manera que el compilador de C tiene una oportunidad para entender lo que está pasando. Esto permite que el código de estar en línea, registro asignado, subexpresión común, la eliminación y la constante de propagación se puede hacer así. Usted obtendrá una gran mejora del rendimiento a través de los escritos a mano código ensamblador de esa manera.

De referencia: El resultado final para el punto fijo para el mul VS.NET compilador:

int inline FixedPointMul (int a, int b)
{
    return (int) __ll_rshift(__emul(a,b),16);
}

Por cierto, La diferencia de rendimiento de punto fijo divide son aún peores. Tuve mejoras de hasta un factor de 10 para los de la división pesada de punto fijo código por escribir un par de asm-líneas.

113voto

lilburne Puntos 482

Hace muchos años yo estaba enseñando a alguien a programar en C. El ejercicio fue para girar un gráfico a 90 grados. Regresó con una solución que se tomó unos minutos para completar, principalmente porque estaba usando multiplica y divide etc. Le mostré cómo reformular el problema utilizando bits turnos, y el tiempo para procesar bajó a alrededor de 30 segundos sobre la no optimización del compilador que él tenía. Yo acababa de un compilador de optimización y el mismo código de girar el gráfico en < de 5 segundos. Miré a la asamblea código que el compilador genera, y por lo que vi decidí que no y, a continuación, que en los días de mi escritura ensamblador de encima.

58voto

Skizz Puntos 30682

Bastante mucho cualquier momento el compilador ve de punto flotante de código, escritas a mano versión será más rápido. La razón principal es que el compilador no puede realizar cualquier sólido optimizaciones. Consulte este artículo de MSDN para una discusión sobre el tema. Aquí un ejemplo en el que la asamblea de la versión es el doble de la velocidad de la versión C (compilado con VS2K5):

#include "stdafx.h"
#include <windows.h>

float KahanSum
(
  const float *data,
  int n
)
{
   float
     sum = 0.0f,
     C = 0.0f,
     Y,
     T;

   for (int i = 0 ; i < n ; ++i)
   {
      Y = *data++ - C;
      T = sum + Y;
      C = T - sum - Y;
      sum = T;
   }

   return sum;
}

float AsmSum
(
  const float *data,
  int n
)
{
  float
    result = 0.0f;

  _asm
  {
    mov esi,data
    mov ecx,n
    fldz
    fldz
l1:
    fsubr [esi]
    add esi,4
    fld st(0)
    fadd st(0),st(2)
    fld st(0)
    fsub st(0),st(3)
    fsub st(0),st(2)
    fstp st(2)
    fstp st(2)
    loop l1
    fstp result
    fstp result
  }

  return result;
}

int main (int, char **)
{
  int
    count = 1000000;

  float
    *source = new float [count];

  for (int i = 0 ; i < count ; ++i)
  {
    source [i] = static_cast <float> (rand ()) / static_cast <float> (RAND_MAX);
  }

  LARGE_INTEGER
    start,
    mid,
    end;

  float
    sum1 = 0.0f,
    sum2 = 0.0f;

  QueryPerformanceCounter (&start);

  sum1 = KahanSum (source, count);

  QueryPerformanceCounter (&mid);

  sum2 = AsmSum (source, count);

  QueryPerformanceCounter (&end);

  cout << "  C code: " << sum1 << " in " << (mid.QuadPart - start.QuadPart) << endl;
  cout << "asm code: " << sum2 << " in " << (end.QuadPart - mid.QuadPart) << endl;

  return 0;
}

Y algunos números de mi PC que ejecuta un defecto de construcción de la release*:

  C code: 500137 in 103884668
asm code: 500137 in 52129147

De interés, cambié el bucle con una dec/jnz y no hizo ninguna diferencia para los tiempos - a veces más rápido, a veces más lento. Supongo que la memoria aspecto limitado de los enanos otras optimizaciones.

Huy, yo estaba corriendo una versión ligeramente diferente del código, en la que se obtienen los números al revés (es decir. C fue más rápido!). Corregido y actualizado de los resultados.

51voto

Liedman Puntos 3144

Sin dar ningún ejemplo específico o el analizador de prueba, usted puede escribir mejor ensamblador de que el compilador cuando usted sabe más que el compilador.

En el caso general, un moderno compilador de C sabe mucho más acerca de cómo optimizar el código en cuestión: se sabe que el procesador pipeline funciona, se puede intentar reordenar las instrucciones más rápido que un humano puede, y así sucesivamente - es básicamente el mismo que el de un ordenador que sea tan bueno o mejor que el mejor jugador humano para juegos de mesa, etc. simplemente porque se pueden hacer búsquedas dentro del espacio del problema más rápido de lo que la mayoría de los seres humanos. A pesar de que, teóricamente, puede realizar así como el equipo en un caso específico, que sin duda no puede hacerlo a la misma velocidad, lo que no resulta factible por más de un par de casos (es decir. el compilador más que seguro de superar a la que si tratas de escribir más de un par de rutinas en ensamblador).

Por otro lado, hay casos en los que el compilador no tiene tanta información - yo diría que principalmente cuando se trabaja con diferentes formas de hardware externos, de que el compilador no tiene conocimiento. El ejemplo principal es, probablemente, controladores de dispositivos, donde ensamblador combinado con un humano el conocimiento íntimo del hardware en cuestión puede obtener mejores resultados que un compilador de C podría hacer.

Otros han mencionado que de propósito especial de instrucciones, que es lo que yo estoy hablando en el párrafo anterior - instrucciones de que el compilador podría haber limitado o ningún conocimiento en absoluto, lo que es posible para un ser humano para escribir código más rápido.

40voto

Nir Puntos 18250

Sólo cuando el uso de algunos de propósito especial de los conjuntos de instrucciones el compilador no apoyo.

Para maximizar el poder de procesamiento de una CPU moderna con múltiples tuberías de distribución y de predicción de ramificación que usted necesita para estructurar el programa de la asamblea en una manera que hace que sea una) casi imposible para un ser humano para escribir b) aún más imposible de mantener.

También, mejores algoritmos, estructuras de datos, gestión de memoria y le dará al menos un orden de magnitud mayor rendimiento de la micro-optimizaciones que usted puede hacer en la asamblea.

Iteramos.com

Iteramos es una comunidad de desarrolladores que busca expandir el conocimiento de la programación mas allá del inglés.
Tenemos una gran cantidad de contenido, y también puedes hacer tus propias preguntas o resolver las de los demás.

Powered by:

X