簡體   English   中英

Ackermann與Haskell / GHC的效率非常低

[英]Ackermann very inefficient with Haskell/GHC

我嘗試計算Ackermann(4,1) ,不同語言/編譯器之間的性能差異很大。 以下是我的Core i7 3820QM,16G,Ubuntu 12.10 64bit的結果

C:1.6sgcc -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.6socamlopt (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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM