【问题标题】:C code to Haskell到 Haskell 的 C 代码
【发布时间】:2020-11-21 06:23:00
【问题描述】:

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

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

【问题讨论】:

  • 通常你在 Haskell 中看待问题的方式与在 C 中完全不同。尝试在 Haskell 中解决问题,而不是翻译你已经拥有的东西,否则你会得到笨拙和尴尬的解决方案,而你不会当你真正摸索某些东西时,不要体验所有那些疯狂的禅宗时刻。实际上,要学习 Haskel,您可以做的最重要的事情就是忘记您对 C 的了解。

标签: c haskell code-translation


【解决方案1】:

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 可以(并且通常会,如果启用优化)为该特定类型创建允许严格性分析和其他一些优化的专用版本(内联类方法,如 @987654330 @ 等),在这种情况下,一个人不会支付多态惩罚。但是,如果使用站点位于不同的模块中,则不会发生这种情况。如果需要(类型类)多态代码,则应将其标记为INLINABLEINLINE(对于 GHC .hi 文件中公开,并且该功能可以在以下位置进行专门化和优化使用地点。

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

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

    在这种情况下,我们只有一个 Float 类型的静态参数,因此带有 SAT 的工作者/包装器转换通常没有任何区别(根据经验,SAT 在以下情况下会得到回报

    • 静态参数是一个函数
    • 几个非函数参数是静态的

    所以根据这条规则,我们不应该期望 w/w + SAT 有任何好处,一般来说,没有任何好处)。这里我们有 一个 特殊情况,其中 w/w + SAT 可以产生很大的不同,那就是当因子 a 为 1 时。GHC 有 {-# RULES #-} 可以消除各种类型的乘以 1 ,并且使用如此短的循环体,每次迭代或多或少的乘法都会产生影响,在应用第 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'
    

    在执行相同优化的情况下,与顶级递归版本的性能没有明显不同。

  3. 严格。 GHC 的严格度分析器不错,但并不完美。它无法通过算法看到足够远的距离来确定该函数是

    • 如果 n &gt;= 1p 严格(假设加法 - (+) - 在两个参数中都是严格的)
    • 如果n &gt;= 2 也严格在a 中(假设两个参数中(*) 的严格性)

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

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

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

    x `rem` 2 == 0
    

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

    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) 除了一个例外 ;)

【讨论】:

  • 我还会从 worker 中删除 a 参数,因为它保持不变。还是故意的?
  • 我希望它尽可能靠近起点。否则我也会删除它。不过,单个(严格)Float 参数的静态参数转换并没有太大区别,因此将其保留并没有真正的危害。
【解决方案2】:

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

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)

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

【讨论】:

  • 对于不熟悉 FP 的人来说,转换到递归版本并非易事,而且它是学习过程的重要组成部分,值得为他做更多的事情。 (我个人更喜欢回答者指导练习)
【解决方案3】:

这是我在 Haskell 中解决这个问题的方法。首先,我观察到这里有几个循环合并为一个:我们是

  1. 形成一个几何序列(其因子是 p 的适当负数)
  2. 取序列的前缀
  3. 对结果求和

所以我的解决方案也遵循这个结构,加入了一点点 sp 以作为很好的衡量标准,因为这就是您的代码所做的。在从头开始的版本中,我可能会完全放弃这两个参数。

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

【讨论】:

  • 两个小缺陷,1. 你的start 有错误的符号,2. factor 只是a
  • 不幸的是,列表没有融合,sum 不是专门为Float (wtf?)所以它不是有效的和堆栈(或堆,我有一个“堆筋疲力尽”,呃?)大n溢出。可惜,因为它真的很好。
  • @DanielFischer 嗯,我想既然他是交替加减的,那么这个因子最好肯定是负数。不过,我对我弄错了其他事情并不感到惊讶,因为我根本没有测试它。我只是想把转型的想法放在那里。 =)
  • @DanielWagner 哦,factor 应该是无条件的-a,我也弄错了。
  • @DanielFischer 我觉得你还是错了,应该是-abs a。我现在已经做了一些测试并修复了这个帖子。 =)
【解决方案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

【讨论】:

    【解决方案5】:

    查看g 函数的签名( float g (int n, float a, float p, float s))您知道您的Haskell 函数将接收4 个元素并返回一个浮点数,因此:

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

    现在让我们看看循环,我们看到n &gt; 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;。如果ns += pp *= an-- 更奇怪。在 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)
    

    如果ns-=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)
    

    【讨论】:

      【解决方案6】:

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

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

      这会更快,因为通过 repeated squaringother clever methods 可以比 O(n) 更快地完成求幂运算,现代编译器很可能会自动将其用于整数幂。

      【讨论】:

        【解决方案7】:

        您可以使用 Haskell Prelude 函数 until :: (a -&gt; Bool) -&gt; (a -&gt; a) -&gt; a -&gt; 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))
        

        bang-patterns !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))
        

        在存在tail call optimization 的情况下,第一个伪代码等同于第二个伪代码(untilactually implemented),这就是为什么在存在 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
        

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

        【讨论】:

          猜你喜欢
          • 2011-04-21
          • 2011-09-05
          • 1970-01-01
          • 2011-02-24
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-03-21
          • 2014-01-23
          相关资源
          最近更新 更多