¿El compilador usa instrucciones SSE para un código C normal?

8 minutos de lectura

Avatar de usuario de Jennifer M.
jennifer m

Veo gente usando -msse -msse2 -mfpmath=sse banderas de forma predeterminada con la esperanza de que esto mejore el rendimiento. Sé que SSE se involucra cuando se usan tipos de vectores especiales en el código C. Pero, ¿estos indicadores hacen alguna diferencia para el código C normal? ¿El compilador usa SSE para optimizar el código C normal?

  • Todas las arquitecturas x86-64 (también conocidas como AMD64) tienen SSE y SSE2. Entonces, si compila para el objetivo x86-64, el compilador usará registros SSE y extensiones SSE y SSE2. En general, los compiladores de C son bastante deficientes en la vectorización de código, por lo que la mayoría de los compiladores emitirán código SSE/SSE2 no vectorizado para “C normal” en x86-64. (Escribí esto como un comentario y no como una respuesta, porque no sé exactamente lo que estás preguntando).

    – Animal nominal

    10 jun 2018 a las 17:40

  • Estoy preguntando: ¿es razonable proporcionar estas banderas a clang o gcc y esperar que el código que no está escrito en términos de tipos vectorizados gane rendimiento? ¿El compilador realmente usa instrucciones SSE para vectorizar algo en tal caso, o no?

    –Jennifer M.

    10 de junio de 2018 a las 18:09

  • Depende de la arquitectura de destino y de las otras banderas utilizadas.. Definitivamente ayudan en la arquitectura Intel (x86) de 32 bits (el código se puede ejecutar en cualquier procesador x86-64 (si el código de 32 bits es compatible con el sistema operativo/kernel), o cualquier procesador x86 compatible con SSE2). Para objetivos x86-64, son superfluos (esas banderas están en vigor de forma predeterminada, según el objetivo). Si el compilador sabe que SSE/SSE2 está disponible (en función de la arquitectura o de los indicadores de opción), hará todo lo posible para vectorizar incluso “C normal”; simplemente no son muy buenos en eso en general.

    – Animal nominal

    10 de junio de 2018 a las 18:58


Avatar de usuario de Peter Cordes
pedro cordes

Sí, los compiladores modernos vectorizan automáticamente con SSE2 si compila con optimización completa. Clang vectoriza bucles en -O2CCG en -O3.

(GCC12 permite la vectorización en -O2 pero sólo cuando es “muy barato”, con -O3 todavía se requiere vectorizar la mayoría de los bucles con conteos de viajes variables en tiempo de ejecución).

Incluso a -O1 o -Oslos compiladores usarán las instrucciones de carga/almacenamiento SIMD para copiar o inicializar estructuras u otros objetos más anchos que un registro entero. Eso realmente no cuenta como vectorización automática; es más como parte de su estrategia memset / memcpy incorporada predeterminada para bloques pequeños de tamaño fijo. (Sin -fno-builtinesto también se aplicará al uso explícito de memcpy con longitudes constantes pequeñas.) Se aprovecha y requiere que las instrucciones SIMD sean compatibles y habilitadas por el kernel, ya sea que lo llame o no “vectorización”. (Uso de núcleos -mgeneral-regs-onlyo en GCC anterior -mno-mmx -mno-sse para deshabilitar esto.)


SSE2 es básico/no opcional para x86-64, por lo que los compiladores siempre pueden usar instrucciones SSE1/SSE2 cuando se dirigen a x86-64. Los conjuntos de instrucciones posteriores (SSE4, AVX, AVX2, AVX512 y extensiones que no son SIMD como BMI2, popcnt, etc.) deben habilitarse manualmente (por ejemplo, -march=x86-64-v3 o -msse4.1) para decirle al compilador que está bien crear un código que no se ejecutará en CPU más antiguas. O para que genere múltiples versiones de código y elija en tiempo de ejecución, pero eso tiene una sobrecarga adicional y solo vale la pena para funciones más grandes.

-msse -msse2 -mfpmath=sse ya es el predeterminado para x86-64, pero no para i386 de 32 bits. Algunas convenciones de llamadas de 32 bits devuelven valores de FP en registros x87, por lo que puede ser inconveniente usar SSE/SSE2 para el cálculo y luego tener que almacenar/recargar el resultado para obtenerlo en x87. st(0). Con -mfpmath=sselos compiladores más inteligentes aún pueden usar x87 para un cálculo que produce un valor de retorno de FP.

En x86 de 32 bits, -msse2 Es posible que no esté activado de forma predeterminada, depende de cómo se configuró su compilador. Si está utilizando 32 bits porque se dirige a CPU que son tan antiguas que no poder ejecutar código de 64 bits, es posible que desee asegurarse de que esté deshabilitado, o solo -msse.

La mejor manera de hacer un binario ajustado para la CPU en la que está compilando es -O3 -march=native -mfpmath=ssey utilice la optimización del tiempo de vinculación + optimización guiada por perfil. (gcc -fprofile-generate / ejecutar algunos datos de prueba / gcc -fprofile-use).

Usando -march=native crea binarios que podrían no ejecutarse en CPU anteriores, si el compilador elige usar nuevas instrucciones. La optimización guiada por perfiles es muy útil para gcc: nunca despliega bucles sin ella. Pero con PGO, sabe qué bucles se ejecutan con frecuencia/durante muchas iteraciones, es decir, qué bucles están “calientes” y en los que vale la pena gastar más tamaño de código. La optimización del tiempo de enlace permite insertar/propagación constante entre archivos. Eso muy útil si tiene C ++ con muchas funciones pequeñas que en realidad no define en los archivos de encabezado.


Consulte ¿Cómo eliminar el “ruido” de la salida del ensamblaje GCC/clang? para obtener más información sobre cómo observar la salida del compilador y darle sentido.

Aquí hay algunos ejemplos específicos en el explorador del compilador Godbolt para x86-64. Godbolt también tiene gcc para varias otras arquitecturas, y con clang puedes agregar -target mips o lo que sea, para que también pueda ver la vectorización automática para ARM NEON con las opciones de compilación correctas para habilitarlo. Puedes usar -m32 con los compiladores x86-64 para obtener una generación de código de 32 bits.

int sumint(int *arr) {
    int sum = 0;
    for (int i=0 ; i<2048 ; i++){
        sum += arr[i];
    }
    return sum;
}

lazo interior con gcc8.1 -O3 (sin -march=haswell o cualquier cosa para habilitar AVX/AVX2):

.L2:                                 # do {
    movdqu  xmm2, XMMWORD PTR [rdi]    # load 16 bytes
    add     rdi, 16
    paddd   xmm0, xmm2                 # packed add of 4 x 32-bit integers
    cmp     rax, rdi
    jne     .L2                      # } while(p != endp)

    # then horizontal add and extract a single 32-bit sum

Sin -ffast-mathlos compiladores no pueden reordenar las operaciones de FP, por lo que el float equivalente no auto-vectorizar (ver el enlace de Godbolt: obtienes escalar addss). (OpenMP puede habilitarlo por ciclo, o usar -ffast-math).

Pero algunas cosas de FP pueden auto-vectorizarse de manera segura sin cambiar el orden de las operaciones.

// clang won't contract this into an FMA without -ffast-math :/
// but gcc will (if you compile with -march=haswell)
void scale_array(float *arr) {
    for (int i=0 ; i<2048 ; i++){
        arr[i] = arr[i] * 2.1f + 1.234f;
    }
}

  # load constants: xmm2 = {2.1,  2.1,  2.1,  2.1}
  #                 xmm1 = (1.23, 1.23, 1.23, 1.23}
.L9:   # gcc8.1 -O3                       # do {
    movups  xmm0, XMMWORD PTR [rdi]         # load unaligned packed floats
    add     rdi, 16
    mulps   xmm0, xmm2                      # multiply Packed Single-precision
    addps   xmm0, xmm1                      # add Packed Single-precision
    movups  XMMWORD PTR [rdi-16], xmm0      # store back to the array
    cmp     rax, rdi
    jne     .L9                           # }while(p != endp)

multiplicador = 2.0f resultados en el uso addps al doble, reduciendo el rendimiento por un factor de 2 en Haswell/Broadwell! Porque antes de SKL, FP add solo se ejecuta en un puerto de ejecución, pero hay dos unidades FMA que pueden ejecutar multiplicaciones. SKL eliminó el sumador y ejecuta add con el mismo rendimiento y latencia de 2 por reloj que mul y FMA. (http://agner.org/optimizar/y vea otros enlaces de rendimiento en la wiki de etiquetas x86).

Compilando con -march=haswell permite que el compilador use un solo FMA para escalar + agregar. (Pero clang no contraerá la expresión en un FMA a menos que use -ffast-math. IIRC hay una opción para habilitar la contracción de FP sin otras operaciones agresivas).

  • Los compiladores también usan SSE en otros lugares que no caerían bajo el manto tradicional de auto-vectorización (bucle). Incluso a -O1 y -Os ambos sonido metálico y gcc use instrucciones SSE/AVX para copias de estructuras, por ejemplo.

    – BeeOnRope

    10 jun 2018 a las 20:31

  • Conjuntos de instrucciones posteriores (SSE4, AVX, AVX2, AVX512 y extensiones que no son SIMD como BMI2, popcnt, etc.) tiene que ser habilitado manualmente para decirle al compilador que está bien hacer un código que no se ejecutará en CPU más antiguas”. — Que quieres decir con “tiene que ser habilitado manualmente” ? ¿Quiere decir que tenemos que pasar las banderas del compilador requeridas? ¿O hay algunas configuraciones de nivel de sistema en las que necesitamos habilitar estas funciones? Supongo que es lo primero.

    – Nawaz

    26 de julio de 2020 a las 9:08


  • @Nawaz: me refiero a las banderas del compilador para permitir que code-gen las use. Todos los núcleos principales que admiten SSE, AVX y AVX512 configuran los bits de registro de control de la CPU de manera adecuada para habilitarlos de forma predeterminada.

    – Peter Cordes

    26 de julio de 2020 a las 15:26

  • @PeterCordes Gracias por una respuesta tan perspicaz.

    – picchiolu

    hace 2 días

¿Ha sido útil esta solución?