Esta pregunta es una extensión de dos discusiones que surgieron recientemente en las respuestas a " C ++ vs Fortran for HPC ". Y es un poco más un desafío que una pregunta ...
Uno de los argumentos más escuchados a favor de Fortran es que los compiladores son simplemente mejores. Como la mayoría de los compiladores de C / Fortran comparten el mismo back-end, el código generado para programas semánticamente equivalentes en ambos idiomas debe ser idéntico. Sin embargo, se podría argumentar que C / Fortran es más / menos fácil de optimizar para el compilador.
Así que decidí probar una prueba simple: obtuve una copia de daxpy.f y daxpy.c y los compilé con gfortran / gcc.
Ahora daxpy.c es solo una traducción f2c de daxpy.f (código generado automáticamente, feo como diablos), así que tomé ese código y lo limpié un poco (conozca daxpy_c), lo que básicamente significaba volver a escribir el bucle más interno como
for ( i = 0 ; i < n ; i++ )
dy[i] += da * dx[i];
Finalmente, lo reescribí (ingrese daxpy_cvec) usando la sintaxis vectorial de gcc:
#define vector(elcount, type) __attribute__((vector_size((elcount)*sizeof(type)))) type
vector(2,double) va = { da , da }, *vx, *vy;
vx = (void *)dx; vy = (void *)dy;
for ( i = 0 ; i < (n/2 & ~1) ; i += 2 ) {
vy[i] += va * vx[i];
vy[i+1] += va * vx[i+1];
}
for ( i = n & ~3 ; i < n ; i++ )
dy[i] += da * dx[i];
Tenga en cuenta que uso vectores de longitud 2 (eso es todo lo que permite SSE2) y que proceso dos vectores a la vez. Esto se debe a que en muchas arquitecturas, podemos tener más unidades de multiplicación que elementos vectoriales.
Todos los códigos se compilaron usando gfortran / gcc versión 4.5 con las marcas "-O3 -Wall -msse2 -march = native -ffast-math -fomit-frame-pointer -malign-double -fstrict-aliasing". En mi computadora portátil (CPU Intel Core i5, M560, 2.67GHz) obtuve la siguiente salida:
pedro@laika:~/work/fvsc$ ./test 1000000 10000
timing 1000000 runs with a vector of length 10000.
daxpy_f took 8156.7 ms.
daxpy_f2c took 10568.1 ms.
daxpy_c took 7912.8 ms.
daxpy_cvec took 5670.8 ms.
Entonces, el código Fortran original lleva un poco más de 8.1 segundos, la traducción automática del mismo toma 10.5 segundos, la implementación ingenua de C lo hace en 7.9 y el código explícitamente vectorizado lo hace en 5.6, marginalmente menos.
Eso es Fortran siendo un poco más lento que la implementación de C ingenua y 50% más lento que la implementación de C vectorizada.
Así que aquí está la pregunta: soy un programador nativo de C y estoy bastante seguro de que hice un buen trabajo con ese código, pero el código Fortran se tocó por última vez en 1993 y, por lo tanto, podría estar un poco desactualizado. Dado que no me siento tan cómodo codificando en Fortran como otros aquí, ¿alguien puede hacer un mejor trabajo, es decir, más competitivo en comparación con cualquiera de las dos versiones C?
Además, ¿alguien puede probar esta prueba con icc / ifort? La sintaxis vectorial probablemente no funcionará, pero me gustaría ver cómo se comporta allí la ingenua versión C. Lo mismo ocurre con cualquiera con xlc / xlf por ahí.
He subido las fuentes y un Makefile aquí . Para obtener tiempos precisos, configure CPU_TPS en test.c a la cantidad de Hz en su CPU. Si encuentra alguna mejora en alguna de las versiones, ¡publíquela aquí!
Actualizar:
Agregué el código de prueba de stali a los archivos en línea y lo completé con una versión C. Modifiqué los programas para hacer 1'000'000 bucles en vectores de longitud 10'000 para que sean consistentes con la prueba anterior (y porque mi máquina no podía asignar vectores de longitud 1'000'000'000, como en el original de stali código). Como los números ahora son un poco más pequeños, utilicé la opción -par-threshold:50
para que el compilador sea más propenso a paralelizarse. La versión icc / ifort utilizada es 12.1.2 20111128 y los resultados son los siguientes
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=1 time ./icctest_c
3.27user 0.00system 0:03.27elapsed 99%CPU
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=1 time ./icctest_f
3.29user 0.00system 0:03.29elapsed 99%CPU
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=2 time ./icctest_c
4.89user 0.00system 0:02.60elapsed 188%CPU
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=2 time ./icctest_f
4.91user 0.00system 0:02.60elapsed 188%CPU
En resumen, los resultados son, a todos los efectos prácticos, idénticos para las versiones C y Fortran, y ambos códigos se paralelizan automáticamente. ¡Tenga en cuenta que los tiempos rápidos en comparación con la prueba anterior se deben al uso de la aritmética de coma flotante de precisión simple!
Actualizar:
Aunque realmente no me gusta a dónde va la carga de la prueba aquí, he vuelto a codificar el ejemplo de multiplicación de matriz de stali en C y lo agregué a los archivos en la web . Estos son los resultados del bucle triple para una y dos CPU:
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=1 time ./mm_test_f 2500
triple do time 3.46421700000000
3.63user 0.06system 0:03.70elapsed 99%CPU
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=1 time ./mm_test_c 2500
triple do time 3.431997791385768
3.58user 0.10system 0:03.69elapsed 99%CPU
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=2 time ./mm_test_f 2500
triple do time 5.09631900000000
5.26user 0.06system 0:02.81elapsed 189%CPU
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=2 time ./mm_test_c 2500
triple do time 2.298916975280899
4.78user 0.08system 0:02.62elapsed 184%CPU
Tenga cpu_time
en cuenta que en Fortran mide el tiempo de CPU y no el tiempo del reloj de pared, así que terminé las llamadas time
para compararlas con 2 CPU. No hay una diferencia real entre los resultados, excepto que la versión C funciona un poco mejor en dos núcleos.
Ahora para el matmul
comando, por supuesto solo en Fortran ya que este intrínseco no está disponible en C:
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=1 time ./mm_test_f 2500
matmul time 23.6494780000000
23.80user 0.08system 0:23.91elapsed 99%CPU
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=2 time ./mm_test_f 2500
matmul time 26.6176640000000
26.75user 0.10system 0:13.62elapsed 197%CPU
Guau. Eso es absolutamente terrible. ¿Alguien puede descubrir lo que estoy haciendo mal o explicar por qué este intrínseco sigue siendo algo bueno?
No agregué las dgemm
llamadas al punto de referencia, ya que son llamadas de biblioteca a la misma función en el Intel MKL.
Para las pruebas futuras, puede alguien sugerir un ejemplo conocido a ser más lenta en C que en Fortran?
Actualizar
Para verificar la afirmación de stali de que lo matmul
intrínseco es "un orden de magnitud" más rápido que el producto de matriz explícito en matrices más pequeñas, modifiqué su propio código para multiplicar matrices de tamaño 100x100 usando ambos métodos, 10'000 veces cada una. Los resultados, en una y dos CPU, son los siguientes:
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=1 time ./mm_test_f 10000 100
matmul time 3.61222500000000
triple do time 3.54022200000000
7.15user 0.00system 0:07.16elapsed 99%CPU
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=2 time ./mm_test_f 10000 100
matmul time 4.54428400000000
triple do time 4.31626900000000
8.86user 0.00system 0:04.60elapsed 192%CPU
Actualizar
Grisu tiene razón al señalar que, sin optimizaciones, gcc convierte las operaciones en números complejos en llamadas a funciones de la biblioteca, mientras que gfortran las incluye en unas pocas instrucciones.
El compilador de C generará el mismo código compacto si la opción -fcx-limited-range
está configurada, es decir, se le indica al compilador que ignore los posibles sobre / subflujos en los valores intermedios. Esta opción está configurada de forma predeterminada en gfortran y puede dar lugar a resultados incorrectos. Forzar -fno-cx-limited-range
en gfortran no cambió nada.
Por lo tanto, este es en realidad un argumento en contra del uso de gfortran para cálculos numéricos: las operaciones con valores complejos pueden desbordarse / desbordarse incluso si los resultados correctos están dentro del rango de punto flotante. Este es en realidad un estándar de Fortran. En gcc, o en C99 en general, el valor predeterminado es hacer las cosas estrictamente (leer IEEE-754 compatible) a menos que se especifique lo contrario.
Recordatorio: tenga en cuenta que la pregunta principal era si los compiladores de Fortran producen un código mejor que los compiladores de C. Este no es el lugar para discusiones sobre los méritos generales de un idioma sobre otro. Lo que realmente me interesaría es si alguien puede encontrar una manera de convencer a gfortran para que produzca un daxpy tan eficiente como el de C usando la vectorización explícita, ya que esto ejemplifica los problemas de tener que confiar en el compilador exclusivamente para la optimización SIMD, o un caso en el que un compilador Fortran supera a su contraparte C.
fuente
restrict
palabra clave que le dice al compilador exactamente eso: suponer que una matriz no se superpone con ninguna otra estructura de datos.Respuestas:
La diferencia en sus tiempos parece deberse al desenrollado manual del daxpy Fortran de paso de unidad . Los siguientes tiempos están en un Xeon X5650 de 2.67 GHz, usando el comando
Compiladores Intel 11.1
Fortran con desenrollado manual: 8.7 segundos
Fortran sin desenrollado manual: 5.8 segundos
C sin desenrollado manual: 5.8 segundos
Compiladores GNU 4.1.2
Fortran con desenrollado manual: 8.3 seg.
Fortran sin desenrollado manual: 13.5 seg.
C sin desenrollado manual: 13.6 seg.
C con atributos vectoriales: 5.8 seg.
Compiladores GNU 4.4.5
Fortran con desenrollado manual: 8.1 segundos
Fortran sin desenrollado manual: 7.4 segundos
C sin desenrollado manual: 8.5 segundos
C con atributos vectoriales: 5.8 segundos
Conclusiones
¿Es hora de probar rutinas más complicadas como dgemv y dgemm?
fuente
Llego tarde a esta fiesta, así que es difícil para mí seguir todo lo anterior. La pregunta es grande, y creo que si estás interesado, podría dividirse en pedazos más pequeños. Una cosa que me interesó fue simplemente el rendimiento de sus
daxpy
variantes, y si Fortran es más lento que C en este código muy simple.Al ejecutar tanto en mi computadora portátil (Macbook Pro, Intel Core i7, 2.66 GHz), el rendimiento relativo de su versión C vectorizada a mano y la versión Fortran no vectorizada a mano dependen del compilador utilizado (con sus propias opciones):
Entonces, parece que GCC mejoró en la vectorización del bucle en la rama 4.6 de lo que era antes.
En el debate general, creo que uno puede escribir código rápido y optimizado tanto en C como en Fortran, casi como en lenguaje ensamblador. Sin embargo, señalaré una cosa: al igual que cuando el ensamblador es más tedioso de escribir que C pero le da un control más preciso sobre lo que ejecuta la CPU, C tiene un nivel más bajo que Fortran. Por lo tanto, le brinda más control sobre los detalles, lo que puede ayudar a optimizar, donde la sintaxis estándar de Fortran (o sus extensiones de proveedor) pueden carecer de funcionalidad. Un caso es el uso explícito de tipos de vectores, otro es la posibilidad de especificar la alineación de variables a mano, algo de lo que Fortran es incapaz.
fuente
La forma en que escribiría AXPY en Fortran es ligeramente diferente. Es la traducción exacta de las matemáticas.
m_blas.f90
Ahora llamemos a la rutina anterior en un programa.
test.f90
Ahora compilemos y ejecútelo ...
Tenga en cuenta que no estoy usando ningún bucle o ninguna directiva explícita de OpenMP . ¿Sería esto posible en C (es decir, sin uso de bucles y auto-paralelización)? No uso C, así que no lo sé.
fuente
icc
también realiza paralelización automática. He agregado un archivoicctest.c
a las otras fuentes. ¿Puede compilarlo con las mismas opciones que utilizó anteriormente, ejecutarlo e informar los tiempos? Tuve que agregar una declaración printf a mi código para evitar que gcc optimizara todo. ¡Esto es solo un truco rápido y espero que esté libre de errores!Creo que no solo es interesante cómo un compilador optimiza el código para el hardware moderno. Especialmente entre GNU C y GNU Fortran, la generación de código puede ser muy diferente.
Consideremos otro ejemplo para mostrar las diferencias entre ellos.
Usando números complejos, el compilador GNU C produce una gran sobrecarga para operaciones aritméticas casi muy básicas en un número complejo. El compilador Fortran ofrece un código mucho mejor. Echemos un vistazo al siguiente pequeño ejemplo en Fortran:
da (gfortran -g -o complex.fo -c complex.f95; objdump -d -S complex.fo):
Cuáles son los códigos de máquina de 39 bytes. Cuando consideramos lo mismo en C
y eche un vistazo a la salida (hecho de la misma manera que arriba), obtenemos:
También son códigos de máquina de 39 bytes, pero el paso 57 de la función se refiere, realiza la parte adecuada del trabajo y realiza la operación deseada. Entonces tenemos un código de máquina de 27 bytes para ejecutar la operación múltiple. La función detrás es muldc3 proporcionada por
libgcc_s.so
y tiene una huella de 1375 bytes en el código de máquina. Esto ralentiza drásticamente el código y proporciona una salida interesante cuando se utiliza un generador de perfiles.Cuando implementamos los ejemplos BLAS anteriores
zaxpy
y realizamos la misma prueba, el compilador Fortran debería dar mejores resultados que el compilador C.(Utilicé GCC 4.4.3 para este experimento, pero noté este comportamiento que otros GCC lanzan).
Entonces, en mi opinión, no solo pensamos en la paralelización y la vectorización cuando pensamos en cuál es el mejor compilador, también tenemos que ver cómo se traducen las cosas básicas al código del ensamblador. Si esta traducción da un código incorrecto, la optimización solo puede usar esto como entrada.
fuente
complex.c
y lo agregué al código en línea. Tuve que agregar toda la entrada / salida para asegurarme de que nada esté optimizado. Solo recibo una llamada__muldc3
si no la uso-ffast-math
. Con-O2 -ffast-math
yo obtengo 9 líneas de ensamblador en línea. ¿Puedes confirmar esto?-ffast-math
) no deberías usar Fortran para tus cálculos de valores complejos. Como describo en la actualización de mi pregunta,-ffast-math
o, más en general,-fcx-limited-range
obliga a gcc a usar los mismos cálculos de rango restringido que no son IEEE que son estándar en Fortran. Entonces, si desea la gama completa de valores complejos y los Inf y NaN correctos, no debe usar Fortran ...Amigos
Esta discusión me pareció muy interesante, pero me sorprendió ver que reordenar los bucles en el ejemplo de Matmul cambió la imagen. No tengo un compilador de inteligencia disponible en mi máquina actual, así que estoy usando gfortran, pero reescribo los bucles en mm_test.f90 para
Cambié los resultados completos de mi máquina.
Los resultados de sincronización de la versión anterior fueron:
mientras que con los bucles triples reorganizados como se indica anteriormente:
Esto es gcc / gfortran 4.7.2 20121109 en una CPU Intel (R) Core (TM) i7-2600K a 3.40GHz
Las banderas del compilador utilizadas fueron las del Makefile que obtuve aquí ...
fuente
No son los lenguajes los que hacen que el código se ejecute más rápido, aunque sí ayudan. Es el compilador, la CPU y el sistema operativo los que hacen que los códigos se ejecuten más rápido. Comparar idiomas es simplemente un nombre inapropiado, inútil y sin sentido. No tiene ningún sentido porque está comparando dos variables: el idioma y el compilador. Si un código se ejecuta más rápido, no sabe cuánto es el idioma o cuánto es el compilador. No entiendo por qué la comunidad informática simplemente no entiende esto :-(
fuente