2

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

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

4

7 回答 7

15

Lee 的翻译已经很不错了(好吧,他混淆了奇偶案例(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. modrem. 后者映射到机器划分,前者执行额外的检查以确保非负结果。因此mod比 , 慢一点rem,如果其中一个满足需求 - 因为它们在两个参数都是非负的情况下产生相同的结果;或者因为结果仅与 0 进行比较(这里两个条件都满足) -rem是可取的。更好,而且更惯用的是使用evenrem出于上述原因而使用)。不过,差异并不大。

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

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

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

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

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

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

    {-# 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 的严格度分析器不错,但并不完美。它无法通过算法看到足够远的距离来确定函数是

    • 严格的pif n >= 1 (假设加法 - (+)- 在两个参数中都是严格的)
    • aif也很严格n >= 2(假设(*)两个参数都严格)

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

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

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

    x `rem` 2 == 0
    

    而且,当循环体的其余部分和这里一样便宜时,这会花费很多时间。LLVM 的优化器已经被教导用 type 的位掩码替换它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)除了一个例外;)

于 2012-11-28T15:39:22.297 回答
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)

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

于 2012-11-28T13:29:20.230 回答
5

这是我在 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
于 2012-11-28T14:56:32.497 回答
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
于 2012-11-28T13:16:21.260 回答
4

查看g函数的签名(即, 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

to ,你在循环中n > 0有另一个条件。if (!(n % 2)) s += p; else s -= p;如果n是奇怪的比你会做s += 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)

If nis even than you will do s-=p, p*=a;and 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)
于 2012-11-28T15:28:42.670 回答
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))

bang 模式!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))

在存在尾调用优化的情况下,第一个伪代码等效于第二个伪代码(实际上是如何until实现) ,这就是为什么在存在 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 的答案中的流处理代码是如何产生的,

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

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

于 2012-11-29T10:48:30.670 回答
3

扩展@Landei 和@MathematicalOrchid 在问题下方的评论:解决手头问题的算法总是O(n)。但是,如果您意识到您实际上是在计算几何级数的部分总和,则可以使用著名的求和公式:

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

这将更快,因为通过重复平方其他聪明的方法可以比 O(n) 更快地完成幂运算,现代编译器可能会自动将其用于整数幂。

于 2012-11-30T03:12:14.150 回答