Estoy escribiendo un juego en Haskell, y mi pase actual en la interfaz de usuario implica una gran cantidad de generación de geometría de procedimiento. Actualmente estoy enfocado en la identificación de realización de una operación en particular (pseudocódigo C-ish):Rendimiento matemático de Haskell en operación de adición múltiple
Vec4f multiplier, addend;
Vec4f vecList[];
for (int i = 0; i < count; i++)
vecList[i] = vecList[i] * multiplier + addend;
Es decir, un pantano-estándar de multiplicar-complemento de cuatro flotadores, el tipo de cosas maduro para la optimización SIMD.
El resultado va a un búfer de vértices OpenGL, por lo que tiene que ser arrojado a una matriz plana C eventualmente. Por la misma razón, los cálculos probablemente deberían hacerse en C 'float'.
He buscado una biblioteca o una solución idiomática nativa para hacer este tipo de cosas rápidamente en Haskell, pero cada solución que he encontrado parece rondar el 2% del rendimiento (es decir, 50x) más lento) en comparación con C de GCC con los indicadores de la derecha. Por supuesto, comencé con Haskell hace un par de semanas, por lo que mi experiencia es limitada, y es por eso que me dirijo a ustedes. ¿Alguno de ustedes puede ofrecer sugerencias para una implementación más rápida de Haskell, o sugerencias para la documentación sobre cómo escribir código Haskell de alto rendimiento?
En primer lugar, la solución Haskell más reciente (relojes de aproximadamente 12 segundos). Probé las cosas de patrones de explosión de this SO post, pero no hizo una diferencia AFAICT. Al reemplazar 'multAdd' por '(\ i v -> v * 4)', el tiempo de ejecución se redujo a 1.9 segundos, por lo que las cosas a nivel de bit (y los consiguientes desafíos para la optimización automática) no parecen tener demasiada culpa.
{-# LANGUAGE BangPatterns #-}
{-# OPTIONS_GHC -O2 -fvia-C -optc-O3 -fexcess-precision -optc-march=native #-}
import Data.Vector.Storable
import qualified Data.Vector.Storable as V
import Foreign.C.Types
import Data.Bits
repCount = 10000
arraySize = 20000
a = fromList $ [0.2::CFloat, 0.1, 0.6, 1.0]
m = fromList $ [0.99::CFloat, 0.7, 0.8, 0.6]
multAdd :: Int -> CFloat -> CFloat
multAdd !i !v = v * (m ! (i .&. 3)) + (a ! (i .&. 3))
multList :: Int -> Vector CFloat -> Vector CFloat
multList !count !src
| count <= 0 = src
| otherwise = multList (count-1) $ V.imap multAdd src
main = do
print $ Data.Vector.Storable.sum $ multList repCount $
Data.Vector.Storable.replicate (arraySize*4) (0::CFloat)
Esto es lo que tengo en C. El código aquí tiene algunos #ifdefs la que le impide ser compilado recto-para arriba; desplácese hacia abajo para el controlador de prueba.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
typedef float v4fs __attribute__ ((vector_size (16)));
typedef struct { float x, y, z, w; } Vector4;
void setv4(v4fs *v, float x, float y, float z, float w) {
float *a = (float*) v;
a[0] = x;
a[1] = y;
a[2] = z;
a[3] = w;
}
float sumv4(v4fs *v) {
float *a = (float*) v;
return a[0] + a[1] + a[2] + a[3];
}
void vecmult(v4fs *MAYBE_RESTRICT s, v4fs *MAYBE_RESTRICT d, v4fs a, v4fs m) {
for (int j = 0; j < N; j++) {
d[j] = s[j] * m + a;
}
}
void scamult(float *MAYBE_RESTRICT s, float *MAYBE_RESTRICT d,
Vector4 a, Vector4 m) {
for (int j = 0; j < (N*4); j+=4) {
d[j+0] = s[j+0] * m.x + a.x;
d[j+1] = s[j+1] * m.y + a.y;
d[j+2] = s[j+2] * m.z + a.z;
d[j+3] = s[j+3] * m.w + a.w;
}
}
int main() {
v4fs a, m;
v4fs *s, *d;
setv4(&a, 0.2, 0.1, 0.6, 1.0);
setv4(&m, 0.99, 0.7, 0.8, 0.6);
s = calloc(N, sizeof(v4fs));
d = s;
double start = clock();
for (int i = 0; i < M; i++) {
#ifdef COPY
d = malloc(N * sizeof(v4fs));
#endif
#ifdef VECTOR
vecmult(s, d, a, m);
#else
Vector4 aa = *(Vector4*)(&a);
Vector4 mm = *(Vector4*)(&m);
scamult((float*)s, (float*)d, aa, mm);
#endif
#ifdef COPY
free(s);
s = d;
#endif
}
double end = clock();
float sum = 0;
for (int j = 0; j < N; j++) {
sum += sumv4(s+j);
}
printf("%-50s %2.5f %f\n\n", NAME,
(end - start)/(double) CLOCKS_PER_SEC, sum);
}
Este script compilará y ejecutará las pruebas con una serie de combinaciones de banderas gcc. El mejor rendimiento lo obtuvo cmath-64-native-O3-restrict-vector-nocopy en mi sistema, con 0.22 segundos.
import System.Process
import GHC.IOBase
cBase = ("cmath", "gcc mult.c -ggdb --std=c99 -DM=10000 -DN=20000")
cOptions = [
[("32", "-m32"), ("64", "-m64")],
[("generic", ""), ("native", "-march=native -msse4")],
[("O1", "-O1"), ("O2", "-O2"), ("O3", "-O3")],
[("restrict", "-DMAYBE_RESTRICT=__restrict__"),
("norestrict", "-DMAYBE_RESTRICT=")],
[("vector", "-DVECTOR"), ("scalar", "")],
[("copy", "-DCOPY"), ("nocopy", "")]
]
-- Fold over the Cartesian product of the double list. Probably a Prelude function
-- or two that does this, but hey. The 'perm' referred to permutations until I realized
-- that this wasn't actually doing permutations. '
permfold :: (a -> a -> a) -> a -> [[a]] -> [a]
permfold f z [] = [z]
permfold f z (x:xs) = concat $ map (\a -> (permfold f (f z a) xs)) x
prepCmd :: (String, String) -> (String, String) -> (String, String)
prepCmd (name, cmd) (namea, cmda) =
(name ++ "-" ++ namea, cmd ++ " " ++ cmda)
runCCmd name compileCmd = do
res <- system (compileCmd ++ " -DNAME=\\\"" ++ name ++ "\\\" -o " ++ name)
if res == ExitSuccess
then do system ("./" ++ name)
return()
else putStrLn $ name ++ " did not compile"
main = do
mapM_ (uncurry runCCmd) $ permfold prepCmd cBase cOptions
Reescribiendo para usar más tipos idiomáticos aproximadamente a la mitad del tiempo de ejecución, http://hpaste.org/fastcgi/hpaste.fcgi/view?id=26551#a26551 pero estoy reenviando esto a Roman para considerar. –