C 代码到 Haskell

标签 c haskell code-translation

所以,我想将 C 代码的一部分转换为 Haskell。我用 C 编写了这部分(这是我想要做的事情的一个简化示例),但是作为 Haskell 的新手,我无法真正让它工作。

float g(int n, float a, float p, float s)
{
    int c;
    while (n>0)
    {
        c = n % 2;
        if (!c) s += p;
        else s -= p;
        p *= a;
        n--;
    }
    return s;
}

有人有任何想法/解决方案吗?

最佳答案

Lee's translation 已经很不错了(好吧,他混淆了奇数和偶数情况(1)),但他陷入了几个性能陷阱。

g n a p s =
  if n > 0
  then
    let c = n `mod` 2
        s' = (if c == 0 then (-) else (+)) s p
        p' = p * a
    in g (n-1) a p' s'        
  else s
  • 他使用 mod 而不是 rem 。后者映射到机器除法,前者执行额外的检查以确保非负结果。因此 modrem 慢一点,如果两者都满足需要 - 因为在两个参数都为非负的情况下它们产生相同的结果;或者因为结果只与 0 进行比较(这里两个条件都满足) - rem 更可取。更好,更惯用的是使用 even(出于上述原因使用 rem)。不过,差异并不大。
  • 没有类型签名。这意味着代码是(类型类)多态的,因此不可能进行严格性分析,也没有任何特化。如果代码在特定类型的同一模块中使用,GHC 可以(并且通常会,如果启用优化)为该特定类型创建一个专门的版本,允许严格性分析和一些其他优化(内联类方法,如 (+) 等.),在这种情况下,一个人不支付多态惩罚。但是,如果使用站点位于不同的模块中,则不会发生这种情况。如果需要(类型类)多态代码,则应将其标记为 INLINABLEINLINE(对于 GHC < 7),以便其展开在 .hi 文件中公开,并且该功能可以在使用现场进行专门化和优化。

    由于 g 是递归的,所以不能内联 [意思是,GHC 不能内联它;原则上它是可能的] 在使用地点,这通常可以实现更多的优化,而不仅仅是特化。

    通常可以更好地优化递归函数的一种技术是工作程序/包装器转换。创建一个调用递归(本地) worker 的包装器,然后可以内联非递归包装器,当使用已知参数调用 worker 时,可以实现进一步优化,例如常量折叠,或者在函数参数的情况下,内联。尤其是后者,当与静态参数转换(在递归调用中永远不会改变的参数不会作为参数传递给递归 worker )结合时,通常会产生巨大的影响。

    在这种情况下,我们只有一个 Float 类型的静态参数,因此带有 SAT 的工作器/包装器转换通常没有区别(根据经验,SAT 在
  • 静态参数是一个函数
  • 几个非函数参数是静态的

  • 所以根据这个规则,我们不应该期望 w/w + SAT 有任何好处,一般来说,没有任何好处)。这里我们有一个特殊情况,w/w + SAT 可以产生很大的不同,那就是当因子 a 为 1 时。 GHC 有 {-# RULES #-} 可以消除各种类型的乘法,并且在如此短的循环体中,乘法每次迭代或多或少都会产生影响,在应用第 3 点和第 4 点后,运行时间减少了大约 40%。 (没有 RULES 用于乘以 0 或 -1 用于浮点类型,因为 0*x = 0(-1)*x = -x 不适用于 NaN。)对于所有其他 a ,w/w + SATed

    {-# INLINABLE g #-}
    g n a p s = worker n p s
      where
        worker n p s
          | n <= 0    = s
          | otherwise = let s' = if even n then s + p else s - p
                        in worker (n-1) a (p*a) s'
    

    与具有相同优化的顶级递归版本的性能没有明显不同。
  • 严格。 GHC 的严格度分析器很好,但并不完美。它无法通过算法看得足够远来确定该函数是
  • p 中严格如果 n >= 1(假设加法 - (+) - 在两个参数中都是严格的)
  • 如果 a 也是严格的 n >= 2(假设两个参数中的 (*) 都严格)

  • 然后产生一个对两者都严格的 worker 。相反,您会得到一个 worker ,它使用未装箱的 Int# n 和未装箱的 Float# s(我在这里使用类型 Int -> Float -> Float -> Float -> Float,对应于 C),以及 Floatap 。因此,在每次迭代中,您会得到两次拆箱和一次重新装箱。这会花费(相对)很多时间,因为除此之外它只是一些简单的算术和测试。
    帮助 GHC 一点点,并在 g(例如 bang 模式)中严格执行工作程序(或 p 本身,如果您不进行工作程序/包装器转换)。这足以让 GHC 在整个过程中使用未装箱的值生产 worker 。
  • 使用除法测试奇偶校验(如果类型为 Int 且使用 LLVM 后端,则不适用)。

    GHC 的优化器还没有深入到低级位,因此 native 代码生成器发出除法指令

    x `rem` 2 == 0
    

    而且,当循环体的其余部分像这里一样便宜时,这会花费很多时间。 LLVM 的优化器已经被教导用 Int 类型的位掩码替换它,因此使用 ghc -O2 -fllvm 您不需要手动执行此操作。使用 native 代码生成器,将其替换为

    x .&. 1 == 0
    

    (当然需要 import Data.Bits)产生显着的加速(在正常平台上,按位并且比除法快得多)。

  • 最终结果

    {-# INLINABLE g #-}
    g n a p s = worker n p s
      where
        worker k !ap acc
            | k > 0 = worker (k-1) (ap*a) (if k .&. (1 :: Int) == 0 then acc + ap else acc - ap)
            | otherwise = acc
    

    gcc -O3 -msse2 loop.c 的结果没有明显不同(对于测试值),除了 a = -1 ,其中 gcc 用否定替换乘法(假设所有 NaN 等效)。

    (1) 他并不孤单,

    c = n % 2;
    if (!c) s += p;
    else s -= p;
    

    似乎真的很棘手,据我所知,每个人(2)都错了。

    (2) 除了一个异常(exception) ;)

    关于C 代码到 Haskell,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13605700/

    相关文章:

    c - typedef 到底声明了什么?

    无法理解简单程序集 x86 中 %rax 的值

    algorithm - 用于处理由邻居列表函数定义的(可能是无限的)图的库

    c# - 将 MSDN 示例从 C# 转换为 C++/CLI

    c# - 位移和数据解释

    Haskell,简单延续

    haskell - 如何将图像转换为颜色矩阵?

    TypeScript 编译错误无法调用类型缺少调用签名的表达式

    javascript - 将 C 翻译成 JavaScript

    c - 通过c中的函数传递的二维数组