Haskell的C代码

时间:2012-11-28 13:03:08

标签: c haskell

所以,我想将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;
}

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

7 个答案:

答案 0 :(得分:12)

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
  1. 他使用mod代替rem。后者映射到机器分区,前者执行额外检查以确保非负结果。因此modrem慢一点,并且如果满足需要 - 因为它们在两个参数都是非负的情况下产生相同的结果;或者因为结果只与0比较(这里满足两个条件) - rem是优选的。更好,更习惯的是使用even(出于上述原因使用rem)。但差别并不大。

  2. 没有类型签名。这意味着代码是(类型 - 类)多态,因此不可能进行严格性分析,也不进行任何特化。如果代码在特定类型的同一模块中使用,GHC可以(并且通常会在启用优化的情况下)为该特定类型创建一个专用版本,该版本允许严格性分析和其他一些优化(类似于{{}的类方法的内联1}}等等,在这种情况下,一个人不支付多态惩罚。但是如果使用站点位于不同的模块中,则不会发生这种情况。如果需要(类型)多态代码,则应将其标记为(+)INLINABLE(对于GHC <7),以便在INLINE文件中公开其展开功能可以在使用现场进行专业化和优化。

    由于.hi是递归的,所以不能内联[意思是,GHC不能内联它;原则上它是可能的]在使用地点,这通常会比仅仅专业化更能实现优化。

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

    在这种情况下,我们只有一个类型为g的静态参数,因此使用SAT的工作者/包装器转换通常没有区别(根据经验,SAT会在

    时获得回报)
    • 静态参数是一个函数
    • 几个非函数参数是静态的

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

    a

    与执行相同优化的顶级递归版本的执行效果不同。

  3. 严。 GHC的严格分析仪很好,但并不完美。通过该算法无法看到足够的确定函数

      {li>严格{-# 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' 如果p(假设加法 - n >= 1 - 两个参数都严格的话) 如果(+)(假设两个参数中都a严格),
    • 也严格n >= 2

    然后生成一个严格的工人。相反,您会找到一个工作人员使用未装箱的(*)用于Int#而未装箱的n用于Float#(我在这里使用类型s,对应于C),以及Int -> Float -> Float -> Float -> FloatFloat的框a s。因此,在每次迭代中,您将获得两次拆箱和重新装箱。这花费了(相对)大量的时间,因为除此之外,它只是一些简单的算术和测试。 稍微帮助GHC,并在p(例如爆炸模式)中使工人(或g本身,如果你不做工人/包装变换)严格。这足以让GHC在整个过程中使用未装箱的值来生产工人。

  4. 使用除法来测试奇偶校验(如果类型为p且使用LLVM后端,则不适用)。

    GHC的优化工具还没有达到低级别的位,因此本机代码生成器会发出

    的除法指令
    Int

    并且,当循环体的其余部分与此处一样便宜时,这会花费大量时间。已经教会LLVM的优化器用x `rem` 2 == 0 类型的位掩码替换它,因此对于Int,您不需要手动执行此操作。使用本机代码生成器,用

    替换它
    ghc -O2 -fllvm

    (当然需要x .&. 1 == 0 )产生显着的加速(在正常平台上,按位并且比分割快得多)。

  5. 最终结果

    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的结果的测试值没有明显不同(对于测试值),其中gcc用乘法替换乘法(假设所有NaN都相等)。


    (1)他并不孤单,

    a = -1

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

    (2)有一个例外;)

答案 1 :(得分:5)

作为第一步,让我们简化您的代码:

float g(int n, float a, float p, float s) {
    if (n <= 0) return s;

    float s2 = n % 2 == 0 ? s + p : s - p;
    return g(n - 1, a, a*p, s2)
}

我们已将您的原始函数转换为具有特定结构的递归函数。这是一个序列!我们可以方便地将其转换为Haskell:

gs :: Bool -> Float -> Float -> Float -> [Float]
gs nb a p s = s : gs (not nb) a (a*p) (if nb then s - p else s + p)

最后,我们只需索引此列表:

g :: Integer -> Float -> Float -> Float -> Float
g n a p s = gs (even n) a p s !! (n - 1)

代码未经过测试,但应该可以使用。如果没有,那可能只是一个一个错误。

答案 2 :(得分:5)

以下是我将如何解决Haskell中的这个问题。首先,我观察到这里有几个循环合并为一个:我们是

  1. 形成几何序列(其因子是p的适当负值形式)
  2. 获取序列的前缀
  3. 总结结果
  4. 所以我的解决方案也遵循这个结构,只需要一点点sp,因为这就是你的代码所做的。在一个从零开始的版本中,我可能完全放弃这两个参数。

    g n a p s = sum (s : take n (iterate (*(-a)) start)) where
        start | odd n     = -p
              | otherwise = p
    

答案 3 :(得分:4)

相当直接的翻译是:

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

答案 4 :(得分:4)

看看这个float g(int n, float a, float p, float s)你会知道你的haskell函数会收到4个元素并返回一个浮点数,因此:

g :: Integer -> Float -> Float -> Float -> Float

查看您看到n > 0是停止案例的循环,以及n--;将是递归调用使用的递减步骤。因此:

g :: Integer -> Float -> Float -> Float -> Float
g n a p s | n <= 0 = s

到n&gt; 0,循环中有另一个条件if (!(n % 2)) s += p; else s -= p;。如果n为奇数,您将执行s += pp *= a和n--。在haskell中将是:

g :: Integer -> Float -> Float -> Float -> Float
g n a p s | n <= 0 = s
          | odd n = g (n-1) a (p*a) (s+p)

如果n是偶数,则执行s-=pp*=a;和n--。因此:

g :: Integer -> Float -> Float -> Float -> Float
g n a p s | n <= 0 = s
          | odd n = g (n-1) a (p*a) (s+p)
          | otherwise = g (n-1) a (p*a) (s-p)

答案 5 :(得分:3)

您可以使用Haskell Prelude函数until :: (a -> Bool) -> (a -> a) -> a -> a几乎自然地编码循环:

g :: Int -> Float -> Float -> Float -> Float
g n a p s = 
  fst.snd $ 
    until ((<= 0).fst) 
          (\(n,(!s,!p)) -> (n-1, (if even n then s+p else s-p, p*a)))
          (n,(s,p))

爆炸模式!s!p标记严格计算的中间变量,以防止过度的懒惰,否则会影响效率。

until pred step start重复应用step函数,直到pred调用最后生成的值将保持,从初始值start开始。它可以用伪代码表示:

def until (pred, step, start):             // well, actually,
  while( true ):                         def until (pred, step, start): 
    if pred(start): return(start)          if pred(start): return(start)
    start := step(start)                   call until(pred, step, step(start))

第一个伪代码相当于actually implemented存在的第二个伪代码(until tail call optimization},这就是为什么在TCO存在的许多函数式语言中循环是通过递归编码。

因此,在Haskell中,until被编码为

until p f x  | p x       = x
             | otherwise = until p f (f x)

但它可能有不同的编码,明确了中期结果:

until p f x = last $ go x     -- or, last (go x)
  where go x | p x       = [x]
             | otherwise = x : go (f x)

使用Haskell标准高阶函数breakiterate这可以写成流处理代码,

until p f x = let (_,(r:_)) = break p (iterate f x) in r
                       -- or: span (not.p) ....

或只是

until p f x = head $ dropWhile (not.p) $ iterate f x    -- or, equivalently,
           -- head . dropWhile (not.p) . iterate f $ x

如果在给定的Haskell实现中不存在TCO,则最后一个版本将是要使用的版本。


希望这可以更清楚地了解来自Daniel Wagner's answer的流处理代码,

g n a p s = s + (sum . take n . iterate (*(-a)) $ if odd n then (-p) else p)

因为涉及的谓词是关于从n

倒计时
fst . snd . head . dropWhile ((> 0).fst) $
  iterate (\(n,(!s,!p)) -> (n-1, (if even n then s+p else s-p, p*a)))
          (n,(s,p))
===
fst . snd . head . dropWhile ((> 0).fst) $
  iterate (\(n,(!s,!p)) -> (n-1, (s+p, p*(-a))))
          (n,(s, if odd n then (-p) else p))          -- 0 is even
===
fst . (!! n) $
  iterate (\(!s,!p) -> (s+p, p*(-a)))
          (s, if odd n then (-p) else p)    
===
foldl' (+) s . take n . iterate (*(-a)) $ if odd n then (-p) else p

pure FP中,流处理范例使计算的所有历史记录都可用,作为值的流(列表)。

答案 6 :(得分:2)

在@Landei和@MathematicalOrchid的评论下面扩展问题:提出解决手头问题的算法总是O(n)。但是,如果你意识到你实际在做的是计算geometric series的部分和,你可以使用众所周知的求和公式:

g n a p s = s + (-1)**n * p * ((-a)**n-1) / (-a-1) 

由于repeated squaringother clever methods可以比O(n)更快地进行取幂,所以这将更快,这可能会被现代编译器自动用于整数幂。