2012-11-04 36 views
4

請問,如何使g(fib)的評價完全嚴格? (我知道,這個指數的解決方案不是最優的。我想知道如何使遞歸完全嚴格 /如果可能的話/)天真的斐波納契C對哈斯克爾

哈斯克爾

g :: Int -> Int 
g 0 = 0 
g 1 = 1 
g x = g(x-1) + g(x-2) 
main = print $ g 42 

所以它運行大致一樣快幼稚℃溶液:

ç

#include <stdio.h> 

long f(int x) 
{ 
    if (x == 0) return 0; 
    if (x == 1) return 1; 
    return f(x-1) + f(x-2); 
} 

int main(void) 
{ 
    printf("%ld\n", f(42)); 
    return 0; 
} 

注意:此fibs遞歸只用作超級示例。我完全知道,有幾十個更好的算法。但肯定有遞歸算法,它不具備如此簡單和更有效的替代方案。

+1

'g'已經嚴格的(因爲它的圖案在其唯一的參數相匹配)。你的意思是讓它使用unboxed的'Int's? – AndrewC

+0

@AndrewC是的!這聽起來更可能。 – Cartesius00

+0

嗯,我不明白你的意思,一個完全嚴格的Hello World,你可以在另一個環境中使用這個技巧?如果是這樣,你的批准答案不是真正的答案你的問題。 – Jonke

回答

16

答案是,GHC使得評估完全嚴格(當你通過優化編譯給它機會時)。原代碼產生核心

Rec { 
Main.$wg [Occ=LoopBreaker] :: GHC.Prim.Int# -> GHC.Prim.Int# 
[GblId, Arity=1, Caf=NoCafRefs, Str=DmdType L] 
Main.$wg = 
    \ (ww_s1JE :: GHC.Prim.Int#) -> 
    case ww_s1JE of ds_XsI { 
     __DEFAULT -> 
     case Main.$wg (GHC.Prim.-# ds_XsI 1) of ww1_s1JI { __DEFAULT -> 
     case Main.$wg (GHC.Prim.-# ds_XsI 2) of ww2_X1K4 { __DEFAULT -> 
     GHC.Prim.+# ww1_s1JI ww2_X1K4 
     } 
     }; 
     0 -> 0; 
     1 -> 1 
    } 
end Rec } 

,正如你所看到的,如果你知道GHC的核心,完全是嚴格使用拆箱原始機器整數。

(不幸的是,本機代碼的gcc從C源產生的只是普通的更快。)

GHC的嚴格分析儀是相當不錯的,而在簡單的情況下,像在這裏,那裏沒有多態性參與和作用是不太複雜了,你可以依靠它發現它可以解開所有值來產生一個使用unboxed的工人Int# s。

但是,在這樣的情況下,生成快速代碼要比在機器類型上運行更多。本地代碼生成器以及LLVM後端生成的程序集基本上是將代碼直接轉換爲程序集,檢查參數是0還是1,如果不是兩次調用該函數並添加結果。兩者都產生一些我不明白的入口和出口代碼,並且在我的盒子上,本地代碼生成器生成更快的代碼。

對於C代碼,clang -O3產生直接的組裝用更少的冗餘代碼,並使用更多的寄存器,

.Ltmp8: 
    .cfi_offset %r14, -24 
    movl  %edi, %ebx 
    xorl  %eax, %eax 
    testl  %ebx, %ebx 
    je   .LBB0_4 
# BB#1: 
    cmpl  $1, %ebx 
    jne   .LBB0_3 
# BB#2: 
    movl  $1, %eax 
    jmp   .LBB0_4 
.LBB0_3: 
    leal  -1(%rbx), %edi 
    callq  recfib 
    movq  %rax, %r14 
    addl  $-2, %ebx 
    movl  %ebx, %edi 
    callq  recfib 
    addq  %r14, %rax 
.LBB0_4: 
    popq  %rbx 
    popq  %r14 
    popq  %rbp 
    ret 

(由於某種原因進行今天我的系統上更好的確要比昨天)。在Haskell源代碼生成的代碼與C之間的性能差異來自後一種情況下使用寄存器的情況,其中間接尋址用於前者,算法的核心在兩者中都是相同的。

gcc在沒有任何優化的情況下使用某些間接尋址產生本質上相同的結果,但低於使用NCG或LLVM後端生成的GHC。與-O1,同上,但更少的間接尋址。隨着-O2,你會得到一個變換後,使組件不容易映射回源,並與-O3,GCC產生相當驚人

.LFB0: 
    .cfi_startproc 
    pushq %r15 
    .cfi_def_cfa_offset 16 
    .cfi_offset 15, -16 
    pushq %r14 
    .cfi_def_cfa_offset 24 
    .cfi_offset 14, -24 
    pushq %r13 
    .cfi_def_cfa_offset 32 
    .cfi_offset 13, -32 
    pushq %r12 
    .cfi_def_cfa_offset 40 
    .cfi_offset 12, -40 
    pushq %rbp 
    .cfi_def_cfa_offset 48 
    .cfi_offset 6, -48 
    pushq %rbx 
    .cfi_def_cfa_offset 56 
    .cfi_offset 3, -56 
    subq $120, %rsp 
    .cfi_def_cfa_offset 176 
    testl %edi, %edi 
    movl %edi, 64(%rsp) 
    movq $0, 16(%rsp) 
    je  .L2 
    cmpl $1, %edi 
    movq $1, 16(%rsp) 
    je  .L2 
    movl %edi, %eax 
    movq $0, 16(%rsp) 
    subl $1, %eax 
    movl %eax, 108(%rsp) 
.L3: 
    movl 108(%rsp), %eax 
    movq $0, 32(%rsp) 
    testl %eax, %eax 
    movl %eax, 72(%rsp) 
    je  .L4 
    cmpl $1, %eax 
    movq $1, 32(%rsp) 
    je  .L4 
    movl 64(%rsp), %eax 
    movq $0, 32(%rsp) 
    subl $2, %eax 
    movl %eax, 104(%rsp) 
.L5: 
    movl 104(%rsp), %eax 
    movq $0, 24(%rsp) 
    testl %eax, %eax 
    movl %eax, 76(%rsp) 
    je  .L6 
    cmpl $1, %eax 
    movq $1, 24(%rsp) 
    je  .L6 
    movl 72(%rsp), %eax 
    movq $0, 24(%rsp) 
    subl $2, %eax 
    movl %eax, 92(%rsp) 
.L7: 
    movl 92(%rsp), %eax 
    movq $0, 40(%rsp) 
    testl %eax, %eax 
    movl %eax, 84(%rsp) 
    je  .L8 
    cmpl $1, %eax 
    movq $1, 40(%rsp) 
    je  .L8 
    movl 76(%rsp), %eax 
    movq $0, 40(%rsp) 
    subl $2, %eax 
    movl %eax, 68(%rsp) 
.L9: 
    movl 68(%rsp), %eax 
    movq $0, 48(%rsp) 
    testl %eax, %eax 
    movl %eax, 88(%rsp) 
    je  .L10 
    cmpl $1, %eax 
    movq $1, 48(%rsp) 
    je  .L10 
    movl 84(%rsp), %eax 
    movq $0, 48(%rsp) 
    subl $2, %eax 
    movl %eax, 100(%rsp) 
.L11: 
    movl 100(%rsp), %eax 
    movq $0, 56(%rsp) 
    testl %eax, %eax 
    movl %eax, 96(%rsp) 
    je  .L12 
    cmpl $1, %eax 
    movq $1, 56(%rsp) 
    je  .L12 
    movl 88(%rsp), %eax 
    movq $0, 56(%rsp) 
    subl $2, %eax 
    movl %eax, 80(%rsp) 
.L13: 
    movl 80(%rsp), %eax 
    movq $0, 8(%rsp) 
    testl %eax, %eax 
    movl %eax, 4(%rsp) 
    je  .L14 
    cmpl $1, %eax 
    movq $1, 8(%rsp) 
    je  .L14 
    movl 96(%rsp), %r15d 
    movq $0, 8(%rsp) 
    subl $2, %r15d 
.L15: 
    xorl %r14d, %r14d 
    testl %r15d, %r15d 
    movl %r15d, %r13d 
    je  .L16 
    cmpl $1, %r15d 
    movb $1, %r14b 
    je  .L16 
    movl 4(%rsp), %r12d 
    xorb %r14b, %r14b 
    subl $2, %r12d 
    .p2align 4,,10 
    .p2align 3 
.L17: 
    xorl %ebp, %ebp 
    testl %r12d, %r12d 
    movl %r12d, %ebx 
    je  .L18 
    cmpl $1, %r12d 
    movb $1, %bpl 
    je  .L18 
    xorb %bpl, %bpl 
    jmp  .L20 
    .p2align 4,,10 
    .p2align 3 
.L21: 
    cmpl $1, %ebx 
    je  .L58 
.L20: 
    leal -1(%rbx), %edi 
    call recfib 
    addq %rax, %rbp 
    subl $2, %ebx 
    jne  .L21 
.L18: 
    addq %rbp, %r14 
    subl $2, %r13d 
    je  .L16 
    subl $2, %r12d 
    cmpl $1, %r13d 
    jne  .L17 
    addq $1, %r14 
.L16: 
    addq %r14, 8(%rsp) 
    subl $2, 4(%rsp) 
    je  .L14 
    subl $2, %r15d 
    cmpl $1, 4(%rsp) 
    jne  .L15 
    addq $1, 8(%rsp) 
.L14: 
    movq 8(%rsp), %rax 
    addq %rax, 56(%rsp) 
    subl $2, 96(%rsp) 
    je  .L12 
    subl $2, 80(%rsp) 
    cmpl $1, 96(%rsp) 
    jne  .L13 
    addq $1, 56(%rsp) 
.L12: 
    movq 56(%rsp), %rax 
    addq %rax, 48(%rsp) 
    subl $2, 88(%rsp) 
    je  .L10 
    subl $2, 100(%rsp) 
    cmpl $1, 88(%rsp) 
    jne  .L11 
    addq $1, 48(%rsp) 
.L10: 
    movq 48(%rsp), %rax 
    addq %rax, 40(%rsp) 
    subl $2, 84(%rsp) 
    je  .L8 
    subl $2, 68(%rsp) 
    cmpl $1, 84(%rsp) 
    jne  .L9 
    addq $1, 40(%rsp) 
.L8: 
    movq 40(%rsp), %rax 
    addq %rax, 24(%rsp) 
    subl $2, 76(%rsp) 
    je  .L6 
    subl $2, 92(%rsp) 
    cmpl $1, 76(%rsp) 
    jne  .L7 
    addq $1, 24(%rsp) 
.L6: 
    movq 24(%rsp), %rax 
    addq %rax, 32(%rsp) 
    subl $2, 72(%rsp) 
    je  .L4 
    subl $2, 104(%rsp) 
    cmpl $1, 72(%rsp) 
    jne  .L5 
    addq $1, 32(%rsp) 
.L4: 
    movq 32(%rsp), %rax 
    addq %rax, 16(%rsp) 
    subl $2, 64(%rsp) 
    je  .L2 
    subl $2, 108(%rsp) 
    cmpl $1, 64(%rsp) 
    jne  .L3 
    addq $1, 16(%rsp) 
.L2: 
    movq 16(%rsp), %rax 
    addq $120, %rsp 
    .cfi_remember_state 
    .cfi_def_cfa_offset 56 
    popq %rbx 
    .cfi_def_cfa_offset 48 
    popq %rbp 
    .cfi_def_cfa_offset 40 
    popq %r12 
    .cfi_def_cfa_offset 32 
    popq %r13 
    .cfi_def_cfa_offset 24 
    popq %r14 
    .cfi_def_cfa_offset 16 
    popq %r15 
    .cfi_def_cfa_offset 8 
    ret 
    .p2align 4,,10 
    .p2align 3 
.L58: 
    .cfi_restore_state 
    addq $1, %rbp 
    jmp  .L18 
    .cfi_endproc 

這是比什麼都測試快得多。 gcc將該算法展開到了非常深入的層次,而GHC和LLVM都沒有做到這一點,這在這裏產生了巨大的影響。

+0

哦,太棒了。那麼,爲什麼Haskell解決方案甚至使用LLVM和'-O2' 2x比'C'慢? – Cartesius00

+4

如果我知道,它不會是:/。至於GHC本身,這並不是它特別擅長的那種代碼,它的努力(仍然)更多地針對更高級別的優化,並且只有很少的人攻擊它才能進行這種低級轉換不久。至於LLVM,我懷疑部分原因是需要將正確的選項傳遞給它的優化器,部分原因是GHC的輸出沒有那麼習慣,LLVM在所有情況下確實擅長優化它。 –

+0

很好的答案,非常感謝。 – Cartesius00

4

首先使用更好的算法!

fibs = 0 : 1 : zipWith (+) fibs (tail fibs) 

fib n = fibs !! n-1 

fib 42會給你一個更快的答案。

這是更重要的是使用更好的算法比做微小的速度調整。

用這個定義(它的長度爲25801位),你可以快樂和快速地計算出ghci中的fib 123456(即解釋,甚至不編譯)。你可能會得到你的C代碼來更快地計算,但你會花一段時間寫它。這幾乎讓我幾乎沒有任何時間。我花了很多時間寫這篇文章!

道德:

  1. 使用正確的算法!
  2. Haskell允許您編寫乾淨的代碼版本,簡單地回憶答案。
  3. 有時候定義無限的答案列表並獲取你想要的答案比寫一些更新值的循環版本更容易。
  4. Haskell太棒了。
+9

請,那是*不是問題。我知道比這更好的對數解決方案。但想知道,如何使這個遞歸嚴格。 – Cartesius00

+0

我很抱歉,但這個答案是無關緊要的。 – Cartesius00

+2

@馬丁我很抱歉,但你的問題的措辭使我誤以爲你有興趣,你給實際的代碼。你問:「請問,如何使g(fib)的評估完全嚴格?」但它已經是了,嚴格性與加速代碼無關,拆箱也沒有多大幫助。你繼續說「因此,它的運行速度與天真的C解決方案大致相同」,我敢打賭,我的C代碼是從水裏吹出來的。正如你所看到的,我處理的問題是給你提出問題的人提供最好的建議。您稍後添加了所有大膽的內容。 – AndrewC

3

這是完全嚴格的。

g :: Int -> Int 
g 0 = 0 
g 1 = 1 
g x = a `seq` b `seq` a + b where 
    a = g $! x-1 
    b = g $! x-2 
main = print $! g 42 

$!是不同之處在於它是在函數參數嚴格相同$(低優先級功能的應用程序)。

你也想用-O2來編譯,雖然我很好奇你爲什麼不想使用更好的算法。

+0

但是你嚴格評估'x's,這是'42,41 ...,0'中的數字。不是函數值。這不正確嗎? – Cartesius00

+0

對不起,我意識到在撰寫評論時,我只會讓它變得嚴格。 – dave4420

1

該功能已經完全嚴格。

函數的嚴格定義是嚴格的,如果給它定義未定義的輸入,它本身就是未定義的。我從上下文中假定你正在考慮一種不同的嚴格性概念,即如果一個函數在產生結果之前評估它的參數,它就是嚴格的。但通常檢查一個值是否未定義的唯一方法是對其進行評估,因此這兩者通常是等價的。

根據第一個定義,g當然是嚴格的,因爲在知道定義的哪個分支要使用之前它必須檢查參數是否等於零,所以如果參數未定義,那麼g本身在嘗試時會窒息閱讀它。

根據一個更加非正式的定義,那麼g會出現什麼錯誤?前兩個條款顯然很好,並且意味着當我們到達第三個條款時,我們必須已經評估了n。現在,在第三個條款中,我們增加了兩個函數調用。更完全,我們有以下任務:

  1. 減去1從n
  2. 減去2從n
  3. 通話g與1結果
  4. 通話g與2
  5. 結果
  6. 將3.和4.的結果加在一起。

懶惰亂用這些操作的小的訂單,但因爲這兩個+g需要它們的參數值纔可以運行他們的代碼,真的沒有什麼可以被任何顯著量延遲,肯定編譯器是免費的嚴格順序執行這些操作,如果它只能說明+嚴格(它內置的,所以應該不會太硬)和g是嚴格的(但它顯然)。因此,任何合理的優化編譯器不會有這樣太麻煩了,而且任何非優化編譯器不會做完全幼稚的事情產生任何顯著的開銷(這當然不是喜歡foldl (+) 0 [1 .. 1000000]的情況)。

的教訓是,當一個函數立即反對的東西它的參數比較,該功能已經是嚴格的,任何像樣的編譯器將能夠利用這個事實,消除懶惰的平時開銷。這並不意味着它能夠消除其他開銷,比如啓動運行時系統所花費的時間,這些開銷往往會使Haskell程序比C程序慢一點。如果你只是看性能數字,那麼比你的程序是嚴格的還是懶惰的還要多。