我正把David Blei最初的C implementation个潜在的Dirichlet分配移植到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);
}

我把这句话翻译成或多或少习惯用的哈斯克尔,如下所示:

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函数.

但我很好奇是否有什么容易摘到的果子我错过了--某种扩展、库或注释,我可以用它们来加快数值运算的速度,同时又不会把它弄得太难看.


UPDATE:多亏了Don StewartYitz,这里有两个更好的解决方案.我稍微修改了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

两者的性能似乎几乎完全相同,一方或另一方以一两个百分点的优势获胜,这取决于编译器标志.

正如camccannover at Reddit,这个故事的寓意是"为了获得最好的结果,请使用Don Stewart作为您的GHC后端代码生成器."除了该解决方案之外,最安全的 Select 似乎是直接将C控制 struct 转换为Haskell,尽管循环融合可以以更惯用的方式提供类似的性能.

我可能最终会在代码中使用Data.Vector方法.

推荐答案

使用相同的控制和数据 struct ,从而实现:

{-# 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展开循环,唯一的方法是通过模板haskell或手动展开.如果大量这样做,您可能会考虑这个(TH宏).

实际上,GHC LLVM后端确实展开了循环:-)

最后,如果您真的喜欢原始的Haskell版本,使用stream fusion combinators,编写它,GHC会将其转换回循环.(读者练习).

C++相关问答推荐

是否可以在C中进行D3 D12申请?

从C函数调用asm函数时生成错误的BLX指令(STM32H753上的gcc)

如何避免重新分配指针数组时,我们从一开始就不知道确切的大小

VS代码C/C++扩展intellisense无法检测环境特定函数'

Tiva TM4C123GXL的I2C通信

从组播组地址了解收到的数据包长度

在一个小型玩具项目中实现终端历史记录功能

C++中矢量类型定义和数据保护的高效解决方案

为什么该函数不将参数值保存到数据 struct 中?

如何使用C for Linux和Windows的标准输入与gdb/mi进行通信?

&;(str[i])和(&;str)[i]有什么区别?

从uint8_t*转换为char*可接受

防止C++中递归函数使用堆栈内存

处理来自浏览器的HTTP请求

Wcstok导致分段故障

C-try 将整数和 struct 数组存储到二进制文件中

C:Assignment中的链表赋值从指针目标类型中丢弃‘const’限定符

GetText不适用于包含国际字符的帐户名称

在NASM中链接Linux共享库时出错-';将R_ X86_64_;foo';

全局变量 y0 与 mathlib 冲突,无法编译最小的 C 代码