Математическая производительность Haskell при операции умножения-сложения
-
29-09-2019 - |
Вопрос
Я пишу игру на Haskell, и моя нынешняя работа над пользовательским интерфейсом включает в себя множество процедурной генерации геометрии.В настоящее время я сосредоточен на определении производительности одной конкретной операции (псевдокод C-ish):
Vec4f multiplier, addend;
Vec4f vecList[];
for (int i = 0; i < count; i++)
vecList[i] = vecList[i] * multiplier + addend;
То есть стандартное умножение-сложение четырех чисел с плавающей точкой, подходящее для SIMD-оптимизации.
Результат попадает в буфер вершин OpenGL, поэтому в конечном итоге его приходится сбрасывать в плоский массив C.По той же причине вычисления, вероятно, следует выполнять для типов C с плавающей запятой.
Я искал либо библиотеку, либо собственное идиоматическое решение, чтобы быстро сделать подобные вещи в Haskell, но каждое решение, которое я придумал, кажется, колеблется в пределах 2% производительности (то есть в 50 раз медленнее) по сравнению с C из GCC с правильными флагами.Конечно, я начал работать с Haskell пару недель назад, поэтому мой опыт ограничен — именно поэтому я и обратился к вам, ребята.Может ли кто-нибудь из вас предложить предложения по более быстрой реализации Haskell или указать документацию о том, как писать высокопроизводительный код Haskell?
Во-первых, самое последнее решение Haskell (тактовая частота около 12 секунд).Я попробовал штуки с челкой из этот ТАК-пост, но это не имело значения AFAICT.Замена «multAdd» на «(\i v -> v * 4)» сократила время выполнения до 1,9 секунды, так что побитовые вещи (и, как следствие, проблемы с автоматической оптимизацией), похоже, не слишком уж виноваты.
{-# 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)
Вот что у меня есть в C.В приведенном здесь коде есть несколько #ifdef, которые не позволяют его скомпилировать напрямую;прокрутите вниз, чтобы найти тест-драйвер.
#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);
}
Этот скрипт скомпилирует и запустит тесты с несколькими комбинациями флагов gcc.Наилучшую производительность в моей системе показал cmath-64-native-O3-restrict-vector-nocopy — 0,22 секунды.
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
Решение
Роман Лещинский отвечает:
На самом деле, ядро выглядит в основном нормально меня.Использование unsafeIndex вместо (!) делает программу более чем в два раза быстро (смотри мой ответ выше).В программа ниже намного быстрее, хотя Meme it (и чище, ИМО).Я подозреваю, что оставшаяся разница между этим и Meme it программа C должна к генералу GHC отстойность, когда дело доходит до плавания точка.HEAD производит лучшие результаты с NCG и -msse2
Сначала определите новый тип данных Vec4:
{-# LANGUAGE BangPatterns #-}
import Data.Vector.Storable
import qualified Data.Vector.Storable as V
import Foreign
import Foreign.C.Types
-- Define a 4 element vector type
data Vec4 = Vec4 {-# UNPACK #-} !CFloat
{-# UNPACK #-} !CFloat
{-# UNPACK #-} !CFloat
{-# UNPACK #-} !CFloat
Убедитесь, что мы можем сохранить его в массиве
instance Storable Vec4 where
sizeOf _ = sizeOf (undefined :: CFloat) * 4
alignment _ = alignment (undefined :: CFloat)
{-# INLINE peek #-}
peek p = do
a <- peekElemOff q 0
b <- peekElemOff q 1
c <- peekElemOff q 2
d <- peekElemOff q 3
return (Vec4 a b c d)
where
q = castPtr p
{-# INLINE poke #-}
poke p (Vec4 a b c d) = do
pokeElemOff q 0 a
pokeElemOff q 1 b
pokeElemOff q 2 c
pokeElemOff q 3 d
where
q = castPtr p
Значения и методы этого типа:
a = Vec4 0.2 0.1 0.6 1.0
m = Vec4 0.99 0.7 0.8 0.6
add :: Vec4 -> Vec4 -> Vec4
{-# INLINE add #-}
add (Vec4 a b c d) (Vec4 a' b' c' d') = Vec4 (a+a') (b+b') (c+c') (d+d')
mult :: Vec4 -> Vec4 -> Vec4
{-# INLINE mult #-}
mult (Vec4 a b c d) (Vec4 a' b' c' d') = Vec4 (a*a') (b*b') (c*c') (d*d')
vsum :: Vec4 -> CFloat
{-# INLINE vsum #-}
vsum (Vec4 a b c d) = a+b+c+d
multList :: Int -> Vector Vec4 -> Vector Vec4
multList !count !src
| count <= 0 = src
| otherwise = multList (count-1) $ V.map (\v -> add (mult v m) a) src
main = do
print $ Data.Vector.Storable.sum
$ Data.Vector.Storable.map vsum
$ multList repCount
$ Data.Vector.Storable.replicate arraySize (Vec4 0 0 0 0)
repCount, arraySize :: Int
repCount = 10000
arraySize = 20000
С ghc 6.12.1, -O2 -fasm:
- 1.752
С ghc HEAD (26 июня), -O2 -fasm -msse2
- 1.708
Это выглядит как наиболее идиоматический способ записи массива Vec4 и обеспечивает наилучшую производительность (в 11 раз быстрее, чем исходный вариант).(И это может стать эталоном для бэкэнда LLVM GHC)
Другие советы
Ну, это лучше. 3,5с вместо 14с.
{-# LANGUAGE BangPatterns #-}
{-
-- multiply-add of four floats,
Vec4f multiplier, addend;
Vec4f vecList[];
for (int i = 0; i < count; i++)
vecList[i] = vecList[i] * multiplier + addend;
-}
import qualified Data.Vector.Storable as V
import Data.Vector.Storable (Vector)
import Data.Bits
repCount, arraySize :: Int
repCount = 10000
arraySize = 20000
a, m :: Vector Float
a = V.fromList [0.2, 0.1, 0.6, 1.0]
m = V.fromList [0.99, 0.7, 0.8, 0.6]
multAdd :: Int -> Float -> Float
multAdd i v = v * (m `V.unsafeIndex` (i .&. 3)) + (a `V.unsafeIndex` (i .&. 3))
go :: Int -> Vector Float -> Vector Float
go n s
| n <= 0 = s
| otherwise = go (n-1) (f s)
where
f = V.imap multAdd
main = print . V.sum $ go repCount v
where
v :: Vector Float
v = V.replicate (arraySize * 4) 0
-- ^ a flattened Vec4f []
Что лучше, чем это было:
$ ghc -O2 --make A.hs
[1 of 1] Compiling Main ( A.hs, A.o )
Linking A ...
$ time ./A
516748.13
./A 3.58s user 0.01s system 99% cpu 3.593 total
Multadd компилирует просто хорошо:
case readFloatOffAddr#
rb_aVn
(word2Int#
(and# (int2Word# sc1_s1Yx) __word 3))
realWorld#
of _ { (# s25_X1Tb, x4_X1Te #) ->
case readFloatOffAddr#
rb11_X118
(word2Int#
(and# (int2Word# sc1_s1Yx) __word 3))
realWorld#
of _ { (# s26_X1WO, x5_X20B #) ->
case writeFloatOffAddr#
@ RealWorld
a17_s1Oe
sc3_s1Yz
(plusFloat#
(timesFloat# x3_X1Qz x4_X1Te) x5_X20B)
Тем не менее, вы делаете 4-элементное во время умножения в C Code Code, поэтому нам понадобится сделать это напрямую, а не подделывать его путем петли и маскировки. GCC, вероятно, развернут петлю тоже.
Итак, чтобы получить одинаковую производительность, нам понадобится вектор размножается (немного сложно, возможно, через Backend LLVM) и разверните петлю (возможно, смущий его). Я отложу в Roman здесь, чтобы посмотреть, есть ли другие очевидные вещи.
Одна идея может быть фактически использовать вектор Vec4, а не сглаживаться.