Performance problem with Euler problem and recursion on Int64 types
我目前正在学习哈斯克尔,将项目欧拉问题作为我的操场。我惊讶于我的haskell程序和类似程序相比有多慢用其他语言编写的程序。我想知道我是否已经放弃了一些东西,或者这是在使用haskell时人们所期望的那种性能惩罚。
下面的程序受到331问题的启发,但我在发布之前已经更改了它,所以我不会为其他人破坏任何东西。它计算在2^30 x 2^30网格上绘制的离散圆的弧长。这是一个简单的尾部递归实现,我确保了保持弧长跟踪的累积变量的更新是严格的。然而,几乎需要一分半钟才能完成(用GHC的-o标志编译)。
1 2 3 4 5 6 7 8 9 10 11 | import Data.Int arcLength :: Int64->Int64 arcLength n = arcLength' 0 (n-1) 0 0 where arcLength' x y norm2 acc | x > y = acc | norm2 < 0 = arcLength' (x + 1) y (norm2 + 2*x +1) acc | norm2 > 2*(n-1) = arcLength' (x - 1) (y-1) (norm2 - 2*(x + y) + 2) acc | otherwise = arcLength' (x + 1) y (norm2 + 2*x + 1) $! (acc + 1) main = print $ arcLength (2^30) |
这里是爪哇的一个相应的实现。完成大约需要4.5秒。
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 | public class ArcLength { public static void main(String args[]) { long n = 1 << 30; long x = 0; long y = n-1; long acc = 0; long norm2 = 0; long time = System.currentTimeMillis(); while(x <= y) { if (norm2 < 0) { norm2 += 2*x + 1; x++; } else if (norm2 > 2*(n-1)) { norm2 += 2 - 2*(x+y); x--; y--; } else { norm2 += 2*x + 1; x++; acc++; } } time = System.currentTimeMillis() - time; System.err.println(acc); System.err.println(time); } |
号
}
编辑:在评论中讨论之后,我对haskell代码做了一些修改,并做了一些性能测试。首先,我将n改为2^29以避免溢出。然后我尝试了6种不同的版本:用Int64或Int,用Bangs在norm2或两者之前,用norm2和acc在声明
1 | ghc -O3 -prof -rtsopts -fforce-recomp -XBangPatterns arctest.hs |
结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | (Int !norm2 !acc) total time = 3.00 secs (150 ticks @ 20 ms) total alloc = 2,892 bytes (excludes profiling overheads) (Int norm2 !acc) total time = 3.56 secs (178 ticks @ 20 ms) total alloc = 2,892 bytes (excludes profiling overheads) (Int norm2 acc) total time = 3.56 secs (178 ticks @ 20 ms) total alloc = 2,892 bytes (excludes profiling overheads) (Int64 norm2 acc) arctest.exe: out of memory (Int64 norm2 !acc) total time = 48.46 secs (2423 ticks @ 20 ms) total alloc = 26,246,173,228 bytes (excludes profiling overheads) (Int64 !norm2 !acc) total time = 31.46 secs (1573 ticks @ 20 ms) total alloc = 3,032 bytes (excludes profiling overheads) |
。
我正在64位Windows7(Haskell平台二进制发行版)下使用ghc 7.0.2。根据注释,在其他配置下编译时不会出现问题。这使我认为Int64类型在Windows版本中已损坏。
嗯,我为7.0.3安装了一个新的haskell平台,并为您的程序大致获得了以下核心(
1 2 3 4 5 6 | Main.$warcLength' = \ (ww_s1my :: GHC.Prim.Int64#) (ww1_s1mC :: GHC.Prim.Int64#) (ww2_s1mG :: GHC.Prim.Int64#) (ww3_s1mK :: GHC.Prim.Int64#) -> case {__pkg_ccall ghc-prim hs_gtInt64 [...] ww_s1my ww1_s1mC GHC.Prim.realWorld# [...] |
号
所以ghc已经意识到它可以解包整数,这很好。但这个
1 2 3 4 5 | pushl %eax movl 76(%esp),%eax pushl %eax call _hs_gtInt64 addl $16,%esp |
号
因此,这看起来非常类似于
嗯,这很有趣。所以我编译了你的两个程序,并尝试了一下:
1 2 3 4 5 6 7 8 | % java -version java version"1.6.0_18" OpenJDK Runtime Environment (IcedTea6 1.8.7) (6b18-1.8.7-2~squeeze1) OpenJDK 64-Bit Server VM (build 14.0-b16, mixed mode) % javac ArcLength.java % java ArcLength 843298604 6630 |
因此,Java解决方案大约需要6.6秒。接下来是具有一些优化的GHC:
1 2 3 4 5 6 | % ghc --version The Glorious Glasgow Haskell Compilation System, version 6.12.1 % ghc --make -O arc.hs % time ./arc 843298604 ./arc 12.68s user 0.04s system 99% cpu 12.718 total |
号
对于GHC-O来说不到13秒
尝试进一步优化:
1 2 3 4 | % ghc --make -O3 % time ./arc [13:16] 843298604 ./arc 5.75s user 0.00s system 99% cpu 5.754 total |
通过进一步的优化标志,haskell解决方案花费了不到6秒的时间。
了解您使用的是什么版本的编译器是很有意思的。
你的问题有几个有趣的地方。
您应该主要使用
其次,您的Haskell与Java不一样(它执行不同的测试和分支)。和其他人一样,在我的Linux设备上运行代码会导致大约6秒的运行时间。看起来不错。
确保它与Java相同
一个想法:让我们用相同的控制流程、操作和类型对Java进行文字转录。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import Data.Bits import Data.Int loop :: Int -> Int loop n = go 0 (n-1) 0 0 where go :: Int -> Int -> Int -> Int -> Int go x y acc norm2 | x <= y = case () of { _ | norm2 < 0 -> go (x+1) y acc (norm2 + 2*x + 1) | norm2 > 2 * (n-1) -> go (x-1) (y-1) acc (norm2 + 2 - 2 * (x+y)) | otherwise -> go (x+1) y (acc+1) (norm2 + 2*x + 1) } | otherwise = acc main = print $ loop (1 `shiftL` 30) |
。
窥视核心
我们将使用
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 31 32 33 34 35 36 37 38 39 | main_$s$wgo :: Int# -> Int# -> Int# -> Int# -> Int# main_$s$wgo = \ (sc_sQa :: Int#) (sc1_sQb :: Int#) (sc2_sQc :: Int#) (sc3_sQd :: Int#) -> case <=# sc3_sQd sc2_sQc of _ { False -> sc1_sQb; True -> case <# sc_sQa 0 of _ { False -> case ># sc_sQa 2147483646 of _ { False -> main_$s$wgo (+# (+# sc_sQa (*# 2 sc3_sQd)) 1) (+# sc1_sQb 1) sc2_sQc (+# sc3_sQd 1); True -> main_$s$wgo (-# (+# sc_sQa 2) (*# 2 (+# sc3_sQd sc2_sQc))) sc1_sQb (-# sc2_sQc 1) (-# sc3_sQd 1) }; True -> main_$s$wgo (+# (+# sc_sQa (*# 2 sc3_sQd)) 1) sc1_sQb sc2_sQc (+# sc3_sQd 1) |
号
也就是说,所有的都是未装箱的。那个圈看起来不错!
性能很好(Linux/x86-64/GHC 7.03):
1 | ./A 5.95s user 0.01s system 99% cpu 5.980 total |
号
检查ASM
我们也得到了合理的组装,作为一个很好的循环:
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 | Main_mainzuzdszdwgo_info: cmpq %rdi, %r8 jg .L8 .L3: testq %r14, %r14 movq %r14, %rdx js .L4 cmpq $2147483646, %r14 jle .L9 .L5: leaq (%rdi,%r8), %r10 addq $2, %rdx leaq -1(%rdi), %rdi addq %r10, %r10 movq %rdx, %r14 leaq -1(%r8), %r8 subq %r10, %r14 jmp Main_mainzuzdszdwgo_info .L9: leaq 1(%r14,%r8,2), %r14 addq $1, %rsi leaq 1(%r8), %r8 jmp Main_mainzuzdszdwgo_info .L8: movq %rsi, %rbx jmp *0(%rbp) .L4: leaq 1(%r14,%r8,2), %r14 leaq 1(%r8), %r8 jmp Main_mainzuzdszdwgo_info |
号
使用
所以这个看起来不错!
正如上面的评论中提到的,我的怀疑与您在32位Windows上使用的
广泛地确认,在64位整数的32位模拟中进行这些C调用的成本确实是如此,我们可以用
教训:尽可能使用64位硬件。
性能相关代码的正常优化标志是
与O2相比,我获得了与Java竞争的能力:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | tommd@Mavlo:Test$ uname -r -m 2.6.37 x86_64 tommd@Mavlo:Test$ ghc --version The Glorious Glasgow Haskell Compilation System, version 7.0.3 tommd@Mavlo:Test$ ghc -O2 so.hs [1 of 1] Compiling Main ( so.hs, so.o ) Linking so ... tommd@Mavlo:Test$ time ./so 843298604 real 0m4.948s user 0m4.896s sys 0m0.000s |
。
Java的速度大约是1秒(20%):
1 2 3 4 5 6 7 | tommd@Mavlo:Test$ time java ArcLength 843298604 3880 real 0m3.961s user 0m3.936s sys 0m0.024s |
但是关于GHC,有趣的是它有许多不同的后端。默认情况下,它使用本机代码生成器(NCG),我们在上面计时。还有一个llvm后端,它通常做得更好…但不在这里:
1 2 3 4 5 6 7 8 9 | tommd@Mavlo:Test$ ghc -O2 so.hs -fllvm -fforce-recomp [1 of 1] Compiling Main ( so.hs, so.o ) Linking so ... tommd@Mavlo:Test$ time ./so 843298604 real 0m5.973s user 0m5.968s sys 0m0.000s |
。
但是,正如评论中提到的fuzzxl,当您添加一些严格的注释时,llvm会做得更好:
1 2 3 4 5 6 7 8 9 | $ ghc -O2 -fllvm -fforce-recomp so.hs [1 of 1] Compiling Main ( so.hs, so.o ) Linking so ... tommd@Mavlo:Test$ time ./so 843298604 real 0m4.099s user 0m4.088s sys 0m0.000s |
。
还有一个旧的"via-c"生成器使用C作为中间语言。在这种情况下效果很好:
1 2 3 4 5 6 7 8 9 10 11 12 13 | tommd@Mavlo:Test$ ghc -O2 so.hs -fvia-c -fforce-recomp [1 of 1] Compiling Main ( so.hs, so.o ) on the commandline: Warning: The -fvia-c flag will be removed in a future GHC release Linking so ... ttommd@Mavlo:Test$ ti tommd@Mavlo:Test$ time ./so 843298604 real 0m3.982s user 0m3.972s sys 0m0.000s |
希望在移除后端之前,NCG将得到改进,以便与VIA-C匹配。
1 2 | alias ggg="ghc --make -O2" alias gggg="echo 'Glorious Glasgow for Great Good!' && ghc --make -O2 --fforce-recomp" |
我已经玩了一些代码,这个版本似乎比我的笔记本电脑上的Java版本快(3.55秒vs 4.63s):
1 2 3 4 5 6 7 8 9 10 11 12 | {-# LANGUAGE BangPatterns #-} arcLength :: Int->Int arcLength n = arcLength' 0 (n-1) 0 0 where arcLength' :: Int -> Int -> Int -> Int -> Int arcLength' !x !y !norm2 !acc | x > y = acc | norm2 > 2*(n-1) = arcLength' (x - 1) (y - 1) (norm2 - 2*(x + y) + 2) acc | norm2 < 0 = arcLength' (succ x) y (norm2 + x*2 + 1) acc | otherwise = arcLength' (succ x) y (norm2 + 2*x + 1) (acc + 1) main = print $ arcLength (2^30) |
。
:
1 2 3 4 5 6 7 8 9 10 | $ ghc -O2 tmp1.hs -fforce-recomp [1 of 1] Compiling Main ( tmp1.hs, tmp1.o ) Linking tmp1 ... $ time ./tmp1 843298604 real 0m3.553s user 0m3.539s sys 0m0.006s |