所以,我想将 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
。后者映射到机器除法,前者执行额外的检查以确保非负结果。因此 mod
比 rem
慢一点,如果两者都满足需要 - 因为在两个参数都为非负的情况下它们产生相同的结果;或者因为结果只与 0 进行比较(这里两个条件都满足) - rem
更可取。更好,更惯用的是使用 even
(出于上述原因使用 rem
)。不过,差异并不大。 (+)
等.),在这种情况下,一个人不支付多态惩罚。但是,如果使用站点位于不同的模块中,则不会发生这种情况。如果需要(类型类)多态代码,则应将其标记为 INLINABLE
或 INLINE
(对于 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'
与具有相同优化的顶级递归版本的性能没有明显不同。
p
中严格如果 n >= 1
(假设加法 - (+)
- 在两个参数中都是严格的)a
也是严格的 n >= 2
(假设两个参数中的 (*)
都严格)然后产生一个对两者都严格的 worker 。相反,您会得到一个 worker ,它使用未装箱的
Int#
n
和未装箱的 Float#
s
(我在这里使用类型 Int -> Float -> Float -> Float -> Float
,对应于 C),以及 Float
的 a
和 p
。因此,在每次迭代中,您会得到两次拆箱和一次重新装箱。这会花费(相对)很多时间,因为除此之外它只是一些简单的算术和测试。帮助 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/