[英]Ackermann very inefficient with Haskell/GHC
我嘗試計算Ackermann(4,1)
,不同語言/編譯器之間的性能差異很大。 以下是我的Core i7 3820QM,16G,Ubuntu 12.10 64bit的結果 ,
C:1.6s , gcc -O3
(gcc 4.7.2)
int ack(int m, int n) {
if (m == 0) return n+1;
if (n == 0) return ack(m-1, 1);
return ack(m-1, ack(m, n-1));
}
int main() {
printf("%d\n", ack(4,1));
return 0;
}
OCaml:3.6s , ocamlopt
(ocaml 3.12.1)
let rec ack = function
| 0,n -> n+1
| m,0 -> ack (m-1, 1)
| m,n -> ack (m-1, ack (m, n-1))
in print_int (ack (4, 1))
標准ML:5.1s mlton -codegen c -cc-opt -O3
(含mlton 20100608)
fun ack 0 n = n+1
| ack m 0 = ack (m-1) 1
| ack m n = ack (m-1) (ack m (n-1));
print (Int.toString (ack 4 1));
球拍:11.5s racket
(帶球拍v5.3.3)
(require racket/unsafe/ops)
(define + unsafe-fx+)
(define - unsafe-fx-)
(define (ack m n)
(cond
[(zero? m) (+ n 1)]
[(zero? n) (ack (- m 1) 1)]
[else (ack (- m 1) (ack m (- n 1)))]))
(time (ack 4 1))
Haskell:未完成 ,在22s
ghc -O2
之后被系統殺死
(ghc 7.4.2)
Haskell:1.8s ajhc
(ajhc 0.8.0.4)
main = print $ ack 4 1
where ack :: Int -> Int -> Int
ack 0 n = n+1
ack m 0 = ack (m-1) 1
ack m n = ack (m-1) (ack m (n-1))
Haskell版本是唯一一個無法正常終止的版本,因為它占用了太多內存。 它會凍結我的機器並在被殺之前填充交換空間。 如果不對代碼進行冗長的處理,我該怎么做才能改進它?
編輯 :我欣賞一些漸近智能的解決方案,但它們並不是我要求的。 這更多的是關於查看編譯器是否以合理有效的方式(堆棧,尾調用,拆箱等)處理某些模式而不是計算ackermann函數。
編輯2 :正如幾個回復所指出的,這似乎是最近版本的GHC中的一個錯誤 。 我使用AJHC嘗試相同的代碼並獲得更好的性能。
非常感謝你 :)
注意:高內存使用問題是GHC RTS中的一個錯誤 ,在堆棧溢出和堆上的新堆棧分配時,沒有檢查垃圾收集是否到期。 它已在GHC HEAD中修復。
通過CPS轉換ack
我能夠獲得更好的性能:
module Main where
data P = P !Int !Int
main :: IO ()
main = print $ ack (P 4 1) id
where
ack :: P -> (Int -> Int) -> Int
ack (P 0 n) k = k (n + 1)
ack (P m 0) k = ack (P (m-1) 1) k
ack (P m n) k = ack (P m (n-1)) (\a -> ack (P (m-1) a) k)
您的原始功能會消耗我機器上的所有可用內存,而這個內存會在恆定的空間內運行。
$ time ./Test
65533
./Test 52,47s user 0,50s system 96% cpu 54,797 total
然而,Ocaml仍然更快:
$ time ./test
65533./test 7,97s user 0,05s system 94% cpu 8,475 total
編輯:使用JHC編譯時,您的原始程序與Ocaml版本一樣快:
$ time ./hs.out
65533
./hs.out 5,31s user 0,03s system 96% cpu 5,515 total
編輯2:我發現的其他東西:使用更大的堆棧塊大小( +RTS -kc1M
)運行原始程序使其在恆定空間中運行。 不過,CPS版本仍然有點快。
編輯3:我設法通過手動展開主循環來生成一個運行速度幾乎與Ocaml一樣快的版本。 但是,它僅在使用+RTS -kc1M
運行時才有效(Dan Doel 已提交有關此行為的錯誤 ):
{-# LANGUAGE CPP #-}
module Main where
data P = P {-# UNPACK #-} !Int {-# UNPACK #-} !Int
ack0 :: Int -> Int
ack0 n =(n+1)
#define C(a) a
#define CONCAT(a,b) C(a)C(b)
#define AckType(M) CONCAT(ack,M) :: Int -> Int
AckType(1)
AckType(2)
AckType(3)
AckType(4)
#define AckDecl(M,M1) \
CONCAT(ack,M) n = case n of { 0 -> CONCAT(ack,M1) 1 \
; 1 -> CONCAT(ack,M1) (CONCAT(ack,M1) 1) \
; _ -> CONCAT(ack,M1) (CONCAT(ack,M) (n-1)) }
AckDecl(1,0)
AckDecl(2,1)
AckDecl(3,2)
AckDecl(4,3)
ack :: P -> (Int -> Int) -> Int
ack (P m n) k = case m of
0 -> k (ack0 n)
1 -> k (ack1 n)
2 -> k (ack2 n)
3 -> k (ack3 n)
4 -> k (ack4 n)
_ -> case n of
0 -> ack (P (m-1) 1) k
1 -> ack (P (m-1) 1) (\a -> ack (P (m-1) a) k)
_ -> ack (P m (n-1)) (\a -> ack (P (m-1) a) k)
main :: IO ()
main = print $ ack (P 4 1) id
測試:
$ time ./Test +RTS -kc1M
65533
./Test +RTS -kc1M 6,30s user 0,04s system 97% cpu 6,516 total
編輯4 :顯然,空間泄漏在GHC HEAD中是固定的 ,因此將來不需要+RTS -kc1M
。
似乎涉及某種bug。 您使用的GHC版本是什么?
使用GHC 7,我會得到與您相同的行為。 該程序消耗所有可用內存而不產生任何輸出。
但是,如果我使用GHC 6.12.1使用ghc --make -O2 Ack.hs
編譯它,它可以很好地工作。 它在我的計算機上計算10.8秒的結果,而普通的C版本需要7.8秒 。
我建議你在GHC網站上報告這個bug 。
該版本使用了ackermann函數的一些屬性。 它不等同於其他版本,但速度很快:
ackermann :: Int -> Int -> Int
ackermann 0 n = n + 1
ackermann m 0 = ackermann (m - 1) 1
ackermann 1 n = n + 2
ackermann 2 n = 2 * n + 3
ackermann 3 n = 2 ^ (n + 3) - 3
ackermann m n = ackermann (m - 1) (ackermann m (n - 1))
編輯:這是一個帶有memoization的版本,我們看到很容易在haskell中記憶一個函數,唯一的變化是在調用站點:
import Data.Function.Memoize
ackermann :: Integer -> Integer -> Integer
ackermann 0 n = n + 1
ackermann m 0 = ackermann (m - 1) 1
ackermann 1 n = n + 2
ackermann 2 n = 2 * n + 3
ackermann 3 n = 2 ^ (n + 3) - 3
ackermann m n = ackermann (m - 1) (ackermann m (n - 1))
main :: IO ()
main = print $ memoize2 ackermann 4 2
以下是一個慣用的版本,它利用了Haskell的惰性和GHC對常量頂級表達式的優化。
acks :: [[Int]]
acks = [ [ case (m, n) of
(0, _) -> n + 1
(_, 0) -> acks !! (m - 1) !! 1
(_, _) -> acks !! (m - 1) !! (acks !! m !! (n - 1))
| n <- [0..] ]
| m <- [0..] ]
main :: IO ()
main = print $ acks !! 4 !! 1
在這里,我們懶洋洋地為Ackermann函數的所有值構建矩陣。 因此,隨后對acks
調用將不會重新計算任何內容(即評估acks !! 4 !! 1
再次不會使運行時間加倍)。
雖然這不是最快的解決方案,但它看起來很像天真的實現,它在內存使用方面非常高效,並且它重寫了Haskell的一個怪異特征(懶惰)作為一種強度。
我根本沒有看到這是一個bug, ghc
只是沒有利用它知道4和1是該函數將被調用的唯一參數的事實 - 也就是說,直言不諱,它不作弊。 它也不會為你做恆定的數學運算,所以如果你寫了main = print $ ack (2+2) 1
,它就不會計算出2 + 2 = 4直到運行時。 ghc
有更重要的事情要考慮。 如果您關心它,可以獲得后一種困難的幫助http://hackage.haskell.org/package/const-math-ghc-plugin 。
因此,如果你做一些數學運算, ghc
會有所幫助,例如,這至少比你的C程序快4倍,而4和1作為參數。 但嘗試4和2:
main = print $ ack 4 2 where
ack :: Int -> Integer -> Integer
ack 0 n = n + 1
ack 1 n = n + 2
ack 2 n = 2 * n + 3
ack m 0 = ack (m-1) 1
ack m n = ack (m-1) (ack m (n-1) )
這將給出正確的答案,所有~2,000位數字,在十分之一秒內,而gcc,與你的算法,將永遠給出錯誤的答案。
以類似於在C中編寫它的方式在Haskell中編寫算法的算法不同,因為遞歸的語義是完全不同的。
這是一個使用相同數學算法的版本,但我們用符號方式使用數據類型表示對Ackermann函數的調用。 這樣,我們可以更精確地控制遞歸的語義。
在使用優化進行編譯時,此版本在常量內存中運行,但速度很慢 - 在類似於您的環境中大約需要4.5分鍾。 但我相信它可以被修改為更快。 這只是為了提出這個想法。
data Ack = Ack !Int
ack :: Int -> Int -> Int
ack m n = length . ackR $ Ack m : replicate n (Ack 0)
where
ackR n@(Ack 0 : _) = n
ackR n = ackR $ ack' n
ack' [] = []
ack' (Ack 0 : n) = Ack 0 : ack' n
ack' [Ack m] = [Ack (m-1), Ack 0]
ack' (Ack m : n) = Ack (m-1) : ack' (Ack m : decr n)
decr (Ack 0 : n) = n
decr n = decr $ ack' n
在Apple XCode
更新到4.6.2
之后,這個性能問題(顯然除了GHC RTS bug)似乎在OS X 10.8上得到修復。 我仍然可以在Linux上重現它(我已經使用GHC LLVM后端進行了測試),但是在OS X上已經不再重復了。在我將XCode更新到4.6.2之后,新版本似乎影響了GHC后端代碼生成阿克曼基本上(從我記得從更新前的對象轉儲)。 我能夠在XCode更新之前重現Mac上的性能問題 - 我沒有這些數字,但它們肯定非常糟糕。 因此,似乎XCode更新改進了Ackermann的GHC代碼生成。
現在,C和GHC版本都非常接近。 C代碼:
int ack(int m,int n){
if(m==0) return n+1;
if(n==0) return ack(m-1,1);
return ack(m-1, ack(m,n-1));
}
執行ack的時間(4,1):
GCC 4.8.0: 2.94s
Clang 4.1: 4s
Haskell代碼:
ack :: Int -> Int -> Int
ack 0 n = n+1
ack m 0 = ack (m-1) 1
ack m n = ack (m-1) (ack m (n-1))
執行ack 4 1的時間(使用+ RTS -kc1M):
GHC 7.6.1 Native: 3.191s
GHC 7.6.1 LLVM: 3.8s
所有都使用-O2
標志進行編譯(對於RTS bug解決方法,GHC的-rtsopts
標志)。 盡管如此,這是一個令人頭疼的問題。 更新XCode似乎與GHC中Ackermann的優化有很大的不同。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.