¿Cuál es la diferencia entre ‘asm’, ‘__asm’ y ‘__asm__’?

12 minutos de lectura

avatar de usuario
Adrián

Por lo que puedo decir, la única diferencia entre __asm { ... }; y __asm__("..."); es que los primeros usos mov eax, var y el segundo usos movl %0, %%eax con :"=r" (var) al final. ¿Qué otras diferencias hay? ¿Y qué hay de sólo asm?

avatar de usuario
Ciro Santilli Путлер Капут 六四事

asm contra __asm__ en CCG

asm no funciona con -std=c99tienes dos alternativas:

  • usar __asm__
  • usar -std=gnu99

Más detalles: error: ‘asm’ no declarado (primer uso en esta función)

__asm contra __asm__ en CCG

no pude encontrar donde __asm está documentado (notablemente no mencionado en https://gcc.gnu.org/onlinedocs/gcc-7.2.0/gcc/Alternate-Keywords.html#Alternate-Keywords ), pero desde el Fuente GCC 8.1 Son exactamente lo mismo:

  { "__asm",        RID_ASM,    0 },
  { "__asm__",      RID_ASM,    0 },

así que solo usaría __asm__ que está documentado.

  • El OP pregunta por __asm { ... }; (con llaves, no paréntesis), por lo que definitivamente no es un asm en línea GNU C. Clang y MSVC admiten asm en línea de estilo MSVC, pero gcc no.

    – Peter Cordes

    28 de mayo de 2018 a las 22:19

  • @PeterCordes gracias por la información. En parte respondiendo esto aquí debido a la etiqueta GCC, y en parte debido a la teoría del éxito de Google. Ahora, mirando el historial, la etiqueta GCC no fue agregada por el OP, pero tampoco por mí 😉

    – Ciro Santilli Путлер Капут 六四事

    28 mayo 2018 a las 22:25

  • @PeterCordes, ¿puede agregar también la etiqueta del compilador de Microsoft adecuada a la pregunta además de gcc? No sé cuál es por suerte 🙂

    – Ciro Santilli Путлер Капут 六四事

    28 mayo 2018 a las 22:31

  • visual-c++ es la etiqueta para el dialecto de C++ de Microsoft, implementado por MSVC.

    – Peter Cordes

    28 mayo 2018 a las 22:38

Con el compilador gcc, no es una gran diferencia. asm o __asm o __asm__ son iguales, solo se usan para evitar conflictos con el propósito del espacio de nombres (hay una función definida por el usuario que nombra asm, etc.)

El que uses depende de tu compilador. Esto no es estándar como el lenguaje C.

  • Uhm, sí. El objetivo de esas barras subyacentes principales es dejar en claro que esto no es estándar.

    – Steven Sudit

    24 de julio de 2010 a las 1:35

  • @StevenSudit no, los guiones bajos iniciales significan “reservado”, consulte wiki.sei.cmu.edu/confluence/display/c/…

    – Zafiro_Brick

    30 de marzo de 2020 a las 16:57


  • @Sapphire_Brick: En particular reservado para el uso de la implementaciónlo que significa que cuando los vea, sabrá que se trata de un comportamiento específico de la implementación.

    – Ben Voigt

    30 de marzo de 2020 a las 19:08

  • Esta respuesta debería ser un comentario, en realidad no es una respuesta.

    – Ayberk Özgür

    16 de abril de 2021 a las 12:54

  • @AyberkÖzgür: OP quería saber cuál usar. La respuesta es que no tiene otra opción en el asunto, tiene que usar el que admite su compilador. No puede usar el asm en línea de GCC en Visual C++ y no puede usar el asm en línea de MSVC en gcc.

    – Ben Voigt

    16 abr 2021 a las 14:59

avatar de usuario
pedro cordes

Hay una gran diferencia entre MSVC inline asm y GNU C inline asm. La sintaxis de GCC está diseñada para una salida óptima sin instrucciones desperdiciadas, para envolver una sola instrucción o algo así. La sintaxis de MSVC está diseñada para ser bastante simple, pero AFAICT es imposible de usar sin la latencia y las instrucciones adicionales de un viaje de ida y vuelta a través de la memoria para sus entradas y salidas.

Si usa asm en línea por razones de rendimiento, esto hace que el asm en línea de MSVC solo sea viable si escribe un ciclo completo completamente en asm, no para envolver secuencias cortas en una función en línea. El siguiente ejemplo (envolviendo idiv con una función) es el tipo de cosas en las que MSVC es malo: ~8 instrucciones adicionales de almacenamiento/carga.

MSVC inline asm (usado por MSVC y probablemente icc, quizás también disponible en algunos compiladores comerciales):

  • mira su asm para averiguar qué registros pisa su código.
  • solo puede transferir datos a través de la memoria. El compilador almacena los datos que estaban activos en los registros para prepararlos para su mov ecx, shift_count, por ejemplo. Por lo tanto, usar una sola instrucción asm que el compilador no generará por usted implica un viaje de ida y vuelta a través de la memoria al entrar y al salir.
  • más fácil de usar para principiantes, pero a menudo imposible de evitar la sobrecarga de entrada/salida de datos. Incluso además de las limitaciones de sintaxis, el optimizador en las versiones actuales de MSVC tampoco es bueno para optimizar alrededor de bloques asm en línea.

GNU C inline asm no es una buena manera de aprender asm. Tienes que entender muy bien asm para que puedas decirle al compilador sobre tu código. Y debe comprender lo que los compiladores necesitan saber. Esa respuesta también tiene enlaces a otras guías de asm en línea y preguntas y respuestas. El wiki de etiquetas x86 tiene muchas cosas buenas para asm en general, pero solo enlaces a eso para asm en línea de GNU. (Las cosas en esa respuesta también se aplican a GNU inline asm en plataformas que no son x86).

La sintaxis asm en línea de GNU C es utilizada por gcc, clang, icc y quizás algunos compiladores comerciales que implementan GNU C:

  • Tienes que decirle al compilador lo que golpeas. Si no se hace esto, se romperá el código circundante de maneras no obvias y difíciles de depurar.
  • Potente pero difícil de leer, aprender y usar sintaxis para decirle al compilador cómo proporcionar entradas y dónde encontrar salidas. p.ej "c" (shift_count) obtendrá el compilador para poner el shift_count variable en ecx antes de que se ejecute su asm en línea.
  • extra torpe para grandes bloques de código, porque el asm tiene que estar dentro de una constante de cadena. Por lo que normalmente necesita

    "insn   %[inputvar], %%reg\n\t"       // comment
    "insn2  %%reg, %[outputvar]\n\t"
    
  • muy implacable / más duro, pero permite una sobrecarga más baja, especialmente. para envolver instrucciones individuales. (envolver instrucciones individuales era la intención del diseño original, por lo que debe informarle especialmente al compilador sobre los primeros clobbers para evitar que use el mismo registro para una entrada y una salida si eso es un problema).


Ejemplo: división entera de ancho completo (div)

En una CPU de 32 bits, dividir un entero de 64 bits por un entero de 32 bits, o hacer una multiplicación completa (32×32->64), puede beneficiarse del asm en línea. gcc y clang no se aprovechan idiv por (int64_t)a / (int32_t)b, probablemente porque la instrucción falla si el resultado no cabe en un registro de 32 bits. Entonces, a diferencia de estas preguntas y respuestas sobre cómo obtener el cociente y el resto de una div, este es un caso de uso para asm en línea. (A menos que haya una manera de informar al compilador que el resultado encajará, por lo que idiv no fallará).

Usaremos convenciones de llamada que colocan algunos argumentos en registros (con hi incluso en el derecho registrarse), para mostrar una situación más cercana a lo que vería al insertar una pequeña función como esta.


MSVC

Tenga cuidado con las convenciones de llamadas de register-arg cuando use inline-asm. Aparentemente, el soporte inline-asm está tan mal diseñado/implementado que es posible que el compilador no guarde/restaure los registros de argumentos alrededor del asm en línea, si esos argumentos no se usan en el asm en línea. Gracias @RossRidge por señalar esto.

// MSVC.  Be careful with _vectorcall & inline-asm: see above
// we could return a struct, but that would complicate things
int _vectorcall div64(int hi, int lo, int divisor, int *premainder) {
    int quotient, tmp;
    __asm {
        mov   edx, hi;
        mov   eax, lo;
        idiv   divisor
        mov   quotient, eax
        mov   tmp, edx;
        // mov ecx, premainder   // Or this I guess?
        // mov   [ecx], edx
    }
    *premainder = tmp;
    return quotient;     // or omit the return with a value in eax
}

Actualización: aparentemente dejando un valor en eax o edx:eax y luego caer al final de una función no nula (sin un return) es compatible, incluso cuando se inserta. Supongo que esto funciona solo si no hay código después de la asm declaración. Ver Hace __asm{}; devolver el valor de eax? Esto evita almacenar/recargar para la salida (al menos para quotient), pero no podemos hacer nada con las entradas. En una función no en línea con argumentos de pila, ya estarán en la memoria, pero en este caso de uso estamos escribiendo una pequeña función que podría ser útil en línea.


Compilado con MSVC 19.00.23026 /O2 en rextester (con un main() que encuentra el directorio del exe y vuelca la salida ASM del compilador a la salida estándar).

## My added comments use. ##
; ... define some symbolic constants for stack offsets of parameters
; 48   : int ABI div64(int hi, int lo, int divisor, int *premainder) {
    sub esp, 16                 ; 00000010H
    mov DWORD PTR _lo$[esp+16], edx      ## these symbolic constants match up with the names of the stack args and locals
    mov DWORD PTR _hi$[esp+16], ecx

    ## start of __asm {
    mov edx, DWORD PTR _hi$[esp+16]
    mov eax, DWORD PTR _lo$[esp+16]
    idiv    DWORD PTR _divisor$[esp+12]
    mov DWORD PTR _quotient$[esp+16], eax  ## store to a local temporary, not *premainder
    mov DWORD PTR _tmp$[esp+16], edx
    ## end of __asm block

    mov ecx, DWORD PTR _premainder$[esp+12]
    mov eax, DWORD PTR _tmp$[esp+16]
    mov DWORD PTR [ecx], eax               ## I guess we should have done this inside the inline asm so this would suck slightly less
    mov eax, DWORD PTR _quotient$[esp+16]  ## but this one is unavoidable
    add esp, 16                 ; 00000010H
    ret 8

Hay un montón de instrucciones de movimiento adicionales, y el compilador ni siquiera se acerca a optimizar nada de eso. Pensé que tal vez vería y entendería el mov tmp, edx dentro del asm en línea, y conviértalo en una tienda para premainder. Pero eso requeriría cargar premainder de la pila a un registro antes del bloque asm en línea, supongo.

Esta función es en realidad peor con _vectorcall que con la ABI normal de todo en la pila. Con dos entradas en los registros, los almacena en la memoria para que el asm en línea pueda cargarlos desde variables nombradas. Si esto estuviera en línea, aún más parámetros podrían estar potencialmente en los registros, y tendría que almacenarlos todos, ¡así que el asm tendría operandos de memoria! Entonces, a diferencia de gcc, no ganamos mucho al incluir esto.

Haciendo *premainder = tmp dentro del bloque asm significa más código escrito en asm, pero evita la ruta de almacenamiento/carga/almacenamiento totalmente insensata para el resto. Esto reduce el recuento de instrucciones en 2 en total, hasta 11 (sin incluir el ret).

Estoy tratando de obtener el mejor código posible de MSVC, no “usarlo mal” y crear un argumento de hombre de paja. Pero AFAICT es horrible para envolver secuencias muy cortas. Presumiblemente, hay una función intrínseca para la división 64/32 -> 32 que permite que el compilador genere un buen código para este caso en particular, por lo que toda la premisa de usar asm en línea para esto en MSVC podría ser un argumento de paja.. Pero te muestra que los intrínsecos son mucho mejor que asm en línea para MSVC.


GNU C (gcc/clang/icc)

Gcc funciona incluso mejor que la salida que se muestra aquí cuando se inserta div64, porque normalmente puede hacer que el código anterior genere el entero de 64 bits en edx:eax en primer lugar.

No puedo hacer que gcc compile para el vectorcall ABI de 32 bits. Clang puede, pero apesta en asm en línea con "rm" restricciones (pruébelo en el enlace de Godbolt: rebota la función arg a través de la memoria en lugar de usar la opción de registro en la restricción). La convención de llamadas MS de 64 bits está cerca de la llamada vectorial de 32 bits, con los dos primeros parámetros en edx, ecx. La diferencia es que 2 parámetros más entran en los registros antes de usar la pila (y que la persona que recibe la llamada no extrae los argumentos de la pila, que es lo que ret 8 se trataba en la salida de MSVC).

// GNU C
// change everything to int64_t to do 128b/64b -> 64b division
// MSVC doesn't do x86-64 inline asm, so we'll use 32bit to be comparable
int div64(int lo, int hi, int *premainder, int divisor) {
    int quotient, rem;
    asm ("idivl  %[divsrc]"
          : "=a" (quotient), "=d" (rem)    // a means eax,  d means edx
          : "d" (hi), "a" (lo),
            [divsrc] "rm" (divisor)        // Could have just used %0 instead of naming divsrc
            // note the "rm" to allow the src to be in a register or not, whatever gcc chooses.
            // "rmi" would also allow an immediate, but unlike adc, idiv doesn't have an immediate form
          : // no clobbers
        );
    *premainder = rem;
    return quotient;
}

compilado con gcc -m64 -O3 -mabi=ms -fverbose-asm. Con -m32 solo obtienes 3 cargas, idiv y una tienda, como puedes ver al cambiar cosas en ese enlace de Godbolt.

mov     eax, ecx  # lo, lo
idivl  r9d      # divisor
mov     DWORD PTR [r8], edx       # *premainder_7(D), rem
ret

Para llamadas vectoriales de 32 bits, gcc haría algo como

## Not real compiler output, but probably similar to what you'd get
mov     eax, ecx               # lo, lo
mov     ecx, [esp+12]          # premainder
idivl   [esp+16]               # divisor
mov     DWORD PTR [ecx], edx   # *premainder_7(D), rem
ret   8

MSVC usa 13 instrucciones (sin incluir el ret), en comparación con las 4 de gcc. Con la inserción, como dije, potencialmente compila solo una, mientras que MSVC aún usaría probablemente 9. (No necesitará reservar espacio de pila o cargar premainder; Supongo que todavía tiene que almacenar alrededor de 2 de las 3 entradas. Luego los recarga dentro del asm, ejecuta idiv, almacena dos salidas y las vuelve a cargar fuera del asm. Así que son 4 cargas/almacenes para entrada y otras 4 para salida).

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad