我正在将David Blei的潜在Dirichlet分配的原始C implementation移植到Haskell,我正在尝试决定是否在C中留下一些低级别的东西。以下函数是一个例子 - 它是lgamma
的二阶导数的近似值:
double trigamma(double x)
{
double p;
int i;
x=x+6;
p=1/(x*x);
p=(((((0.075757575757576*p-0.033333333333333)*p+0.0238095238095238)
*p-0.033333333333333)*p+0.166666666666667)*p+1)/x+0.5*p;
for (i=0; i<6 ;i++)
{
x=x-1;
p=1/(x*x)+p;
}
return(p);
}
我已将其转化为或多或少惯用的Haskell,如下所示:
trigamma :: Double -> Double
trigamma x = snd $ last $ take 7 $ iterate next (x' - 1, p')
where
x' = x + 6
p = 1 / x' ^ 2
p' = p / 2 + c / x'
c = foldr1 (\a b -> (a + b * p)) [1, 1/6, -1/30, 1/42, -1/30, 5/66]
next (x, p) = (x - 1, 1 / x ^ 2 + p)
问题在于,当我通过Criterion运行时,我的Haskell版本慢了六到七倍(我在GHC 6.12.1上用-O2
进行编译)。一些类似的功能甚至更糟。
我对Haskell的性能几乎一无所知,而且我对digging through Core或类似的东西并不十分感兴趣,因为我总是可以通过FFI调用少数数学密集型C函数。
但是我很好奇我是否还缺少一些低调的水果 - 某种扩展,图书馆或注释,我可以用来加速这些数字而不会让它太难看。
更新:由于Don Stewart和Yitz,以下是两个更好的解决方案。我稍微修改了Yitz的答案以使用Data.Vector
。
invSq x = 1 / (x * x)
computeP x = (((((5/66*p-1/30)*p+1/42)*p-1/30)*p+1/6)*p+1)/x+0.5*p
where p = invSq x
trigamma_d :: Double -> Double
trigamma_d x = go 0 (x + 5) $ computeP $ x + 6
where
go :: Int -> Double -> Double -> Double
go !i !x !p
| i >= 6 = p
| otherwise = go (i+1) (x-1) (1 / (x*x) + p)
trigamma_y :: Double -> Double
trigamma_y x = V.foldl' (+) (computeP $ x + 6) $ V.map invSq $ V.enumFromN x 6
两者的性能似乎几乎完全相同,其中一个或另一个赢得一个或两个百分点,具体取决于编译器标志。
正如camccann所说over at Reddit,故事的寓意是“为了获得最佳效果,请使用Don Stewart作为GHC后端代码生成器。”除了这个解决方案之外,最安全的选择似乎只是将C控制结构直接转换为Haskell,尽管循环融合可以以更惯用的方式提供类似的性能。
我可能最终会在我的代码中使用Data.Vector
方法。
答案 0 :(得分:49)
使用相同的控制和数据结构,产生:
{-# LANGUAGE BangPatterns #-}
{-# OPTIONS_GHC -fvia-C -optc-O3 -fexcess-precision -optc-march=native #-}
{-# INLINE trigamma #-}
trigamma :: Double -> Double
trigamma x = go 0 (x' - 1) p'
where
x' = x + 6
p = 1 / (x' * x')
p' =(((((0.075757575757576*p-0.033333333333333)*p+0.0238095238095238)
*p-0.033333333333333)*p+0.166666666666667)*p+1)/x'+0.5*p
go :: Int -> Double -> Double -> Double
go !i !x !p
| i >= 6 = p
| otherwise = go (i+1) (x-1) (1 / (x*x) + p)
我没有你的测试套件,但这会产生以下asm:
A_zdwgo_info:
cmpq $5, %r14
jg .L3
movsd .LC0(%rip), %xmm7
movapd %xmm5, %xmm8
movapd %xmm7, %xmm9
mulsd %xmm5, %xmm8
leaq 1(%r14), %r14
divsd %xmm8, %xmm9
subsd %xmm7, %xmm5
addsd %xmm9, %xmm6
jmp A_zdwgo_info
哪个看起来不错。这是-fllvm
后端做得很好的代码。
GCC展开循环,唯一的方法是通过Template Haskell或手动展开。如果做了很多这样的话,你可能会考虑(一个TH宏)。
实际上,GHC LLVM后端会展开循环: - )
最后,如果您真的喜欢原始的Haskell版本,请使用stream fusion combinators,编写它,GHC会将其转换回循环。 (为读者练习)。
答案 1 :(得分:8)
在优化工作之前,我不会说你的原始翻译是在Haskell中表达C代码的最惯用的方式。
如果我们开始使用以下内容,优化过程将如何进行:
trigamma :: Double -> Double
trigamma x = foldl' (+) p' . map invSq . take 6 . iterate (+ 1) $ x
where
invSq y = 1 / (y * y)
x' = x + 6
p = invSq x'
p' =(((((0.075757575757576*p-0.033333333333333)*p+0.0238095238095238)
*p-0.033333333333333)*p+0.166666666666667)*p+1)/x'+0.5*p