Euler #4 with bigger domain
考虑修改后的欧拉问题4--"找到最大回文数,它是100到9999之间两个数的乘积。"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
- 使用
-O2 和ghc 7.4.1 的haskell解决方案大约需要18个秒。 - 类似的
C 解决方案需要0.1秒。
所以haskell是180倍更慢的。我的解决方案有什么问题?我假设这种类型的哈斯克尔的问题解决得很好。
附录-模拟C溶液:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
号
The similar C solution
号
这是一个常见的误解。
列表不是循环!并且使用列表来模拟循环会影响性能,除非编译器能够从代码中消除列表。
如果你想比较苹果和苹果,写haskell结构或多或少相当于一个循环,一个尾部递归工作程序(使用严格的累加器,尽管编译器通常足够聪明,可以自己计算出严格性)。
现在让我们更详细地看看。比较而言,用gcc-o3编译的c在这里大约需要0.08秒,用ghc-o2编译的原始haskell大约需要20.3秒,用ghc-o2-fllvm大约19.9秒。相当可怕。
原始代码中的一个错误是使用
改变这一点,使用本机代码生成器编译时,运行时间变为~15.4秒,使用llvm后端编译时,运行时间变为~2.9秒。
这种差异是由于即使是机器除法运算也相当慢,LLVM用乘法和移位运算代替除法/余数。手工对本机后端进行相同的操作(实际上,更好的替代方法是利用我知道参数总是非负的这一事实),从而将时间缩短到约2.2秒。
我们的关系越来越近了,但与C还是有很大的差距。
那是由于名单的缘故。代码仍然构建回文列表(并遍历两个因素的
由于列表不能包含未绑定的元素,这意味着代码中存在大量装箱和拆箱操作,这需要时间。
因此,让我们消除这些列表,并看看将c转换为haskell的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | module Main (main) where a :: Int a = 100 b :: Int b = 9999 ispali :: Int -> Bool ispali n = go n 0 where go 0 acc = acc == n go m acc = go (m `quot` 10) (acc * 10 + (m `rem` 10)) maxpal :: Int maxpal = go 0 b where go mx i | i < a = mx | otherwise = go (inner mx b) (i-1) where inner m j | j < a = m | p > m && ispali p = inner p (j-1) | otherwise = inner m (j-1) where p = i*j main :: IO () main = print maxpal |
嵌套循环被转换成两个嵌套的工作函数,我们使用一个累加器来存储迄今为止发现的最大回文。用ghc-o2编译,运行时间约为0.18秒,用ghc-o2-fllvm编译,运行时间约为0.14秒(是的,llvm比本机代码生成器更擅长优化循环)。
仍然不是很好,但是大约2的因素也不错。
也许有些人会发现以下内容,其中循环被抽象出来更具可读性,生成的核心对于所有意图和目的都是相同的(模转换参数顺序),当然性能也是相同的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | module Main (main) where a :: Int a = 100 b :: Int b = 9999 ispali :: Int -> Bool ispali n = go n 0 where go 0 acc = acc == n go m acc = go (m `quot` 10) (acc * 10 + (m `rem` 10)) downto :: Int -> Int -> a -> (a -> Int -> a) -> a downto high low acc fun = go high acc where go i acc | i < low = acc | otherwise = go (i-1) (fun acc i) maxpal :: Int maxpal = downto b a 0 $ \m i -> downto b a m $ \mx j -> let p = i*j in if mx < p && ispali p then p else mx main :: IO () main = print maxpal |
。
@AxBlont至少部分正确;下面的修改使程序运行速度几乎是原来的三倍:
1 2 3 4 5 6 7 |
不过,这仍然会导致因子60的减速。
这更符合C代码所做的工作:
1 2 3 4 5 6 7 8 9 |
号
在我的盒子里,这需要2秒,而C版则需要5秒。
haskell可能正在存储整个列表[x*y x<-nums,y<-nums,pali(x*y)],其中c解决方案会实时计算最大值。我对此不确定。
此外,C解决方案仅在产品超过之前的最大值时计算ispali。我敢打赌,不管x*y是否可能是最大值,haskell计算的都是回文积。
另一种方法是使用两个折叠,而不是扁平列表上的一个折叠:
1 2 3 4 5 6 7 8 9 10 11 12 13 | -- foldl g0 0 [x*y | x<-[b-1,b-2..a], y<-[b-1,b-2..a], pali(x*y)] (A) -- foldl g1 0 [x*y | x<-[b-1,b-2..a], y<-[b-1,b-2..a]] (B) -- foldl g2 0 [ [x*y | y<-[b-1,b-2..a]] | x<-[b-1,b-2..a]] (C) maxpal b a = foldl f1 0 [b-1,b-2..a] -- (D) where f1 m x = foldl f2 m [b-1,b-2..a] where f2 m y | p>m && pali p = p | otherwise = m where p = x*y main = print $ maxpal 10000 100 |
似乎运行得比
1 2 3 4 5 6 7 8 9 |
号
1 2 3 4 5 6 7 8 9 10 11 | {- foldl g2 0 [ [x*y | y<-[b-1,b-2..a]] | x<-[b-1,b-2..a]] (C) foldl g2 0 [ [x*y | y<-[x, x-1..a]] | x<-[b-1,b-2..a]] (C1) foldl g0 0 [ safehead 0 . filter pali $ [x*y | y<-[x, x-1..a]] | x<-[b-1,b-2..a]] (C2) fst $ until ... (\(m,s)-> (max m . safehead 0 . filter pali . takeWhile (> m) $ head s, tail s)) (0,[ [x*y | y<-[x, x-1..a]] | x<-[b-1,b-2..a]]) (C3) safehead 0 $ filter pali $ mergeAllDescending [ [x*y | y<-[x, x-1..a]] | x<-[b-1,b-2..a]] (C4) -} |
当子列表中的标题
在我看来,你有一个分支预测问题。在C代码中,有两个嵌套循环,一旦在内部循环中看到回文,内部循环的其余部分将很快被跳过。
您提供产品列表而不是嵌套循环的方式,我不确定GHC是否在做任何预测。