所以,我想将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;
}
有人有任何想法/解决方案吗?
答案 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
他使用mod
代替rem
。后者映射到机器分区,前者执行额外检查以确保非负结果。因此mod
比rem
慢一点,并且如果满足需要 - 因为它们在两个参数都是非负的情况下产生相同的结果;或者因为结果只与0比较(这里满足两个条件) - rem
是优选的。更好,更习惯的是使用even
(出于上述原因使用rem
)。但差别并不大。
没有类型签名。这意味着代码是(类型 - 类)多态,因此不可能进行严格性分析,也不进行任何特化。如果代码在特定类型的同一模块中使用,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
,因为-1
和0*x = 0
不适用于NaN。)对于所有其他(-1)*x = -x
,w / w + SATed
a
与执行相同优化的顶级递归版本的执行效果不同。
严。 GHC的严格分析仪很好,但并不完美。通过该算法无法看到足够的确定函数
{-# 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 -> Float
和Float
的框a
s。因此,在每次迭代中,您将获得两次拆箱和重新装箱。这花费了(相对)大量的时间,因为除此之外,它只是一些简单的算术和测试。
稍微帮助GHC,并在p
(例如爆炸模式)中使工人(或g
本身,如果你不做工人/包装变换)严格。这足以让GHC在整个过程中使用未装箱的值来生产工人。
使用除法来测试奇偶校验(如果类型为p
且使用LLVM后端,则不适用)。
GHC的优化工具还没有达到低级别的位,因此本机代码生成器会发出
的除法指令Int
并且,当循环体的其余部分与此处一样便宜时,这会花费大量时间。已经教会LLVM的优化器用x `rem` 2 == 0
类型的位掩码替换它,因此对于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
的结果的测试值没有明显不同(对于测试值),其中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中的这个问题。首先,我观察到这里有几个循环合并为一个:我们是
所以我的解决方案也遵循这个结构,只需要一点点s
和p
,因为这就是你的代码所做的。在一个从零开始的版本中,我可能完全放弃这两个参数。
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 += p
,p *= 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-=p
,p*=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标准高阶函数break
和iterate
这可以写成流处理代码,
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
答案 6 :(得分:2)
在@Landei和@MathematicalOrchid的评论下面扩展问题:提出解决手头问题的算法总是O(n)。但是,如果你意识到你实际在做的是计算geometric series的部分和,你可以使用众所周知的求和公式:
g n a p s = s + (-1)**n * p * ((-a)**n-1) / (-a-1)
由于repeated squaring或other clever methods可以比O(n)更快地进行取幂,所以这将更快,这可能会被现代编译器自动用于整数幂。