簡體   English   中英

C尾調用優化

[英]C tail call optimization

我經常聽到人們說C不執行尾部呼叫消除。 雖然標准不能保證,但是無論如何,它是否在實踐中通過任何體面的實現來執行? 假設你只針對成熟的,實現良好的編譯器而不關心為模糊平台編寫的原始編譯器的絕對最大可移植性,那么在C中依賴尾調用是否合理呢?

此外,將尾部呼叫優化留在標准之外的理由是什么?

像“C不執行尾部調用消除”這樣的語句沒有任何意義。 正如你自己正確地指出的那樣,這樣的事情完全取決於實現。 是的,任何體面的實現都可以輕松地將尾遞歸轉換為[相當於]一個循環。 當然,C編譯器通常不會對優化程序以及每個特定代碼段中不會發生的優化提供任何保證。 你必須編譯它並親自看看。

雖然現代編譯器可以在啟用優化時進行尾部調用優化,但是調試版本可能會在沒有它的情況下運行,因此您可以獲得堆棧跟蹤和進入/退出代碼以及類似的奇妙事情。 在這種情況下,不希望尾調用優化。

由於尾調用優化並不總是令人滿意,因此將其強制命令編譯器編寫器是沒有意義的。

我認為只有在預期或需要大量遞歸的情況下才需要保證尾調用優化; 也就是說,在鼓勵或強制執行函數式編程風格的語言中。 (使用這些類型的語言,你可能會發現forwhile循環要么被強烈勸阻,要么被認為是不優雅的,或者甚至可能完全不在語言中,所以你會因為所有這些原因而訴諸遞歸,而且可能更多。)

C編程語言(恕我直言)顯然沒有考慮到函數式編程。 有各種各樣的循環結構通常用於支持遞歸: fordo .. whilewhile 在這種語言中,在標准中規定尾部呼叫優化沒有多大意義,因為並不是嚴格要求保證工作程序。

將此與一個沒有while循環的函數式編程語言進行對比:這意味着你需要遞歸; 這反過來意味着語言必須確保,經過多次迭代,堆棧溢出不會成為問題; 因此,這種語言的官方標准可能會選擇規定尾部呼叫優化。


PS:請注意我尾部調用優化參數中的一個小漏洞。 接近尾聲,我提到了堆棧溢出。 但誰說函數調用總是需要堆棧? 在某些平台上,函數調用可能以完全不同的方式實現,堆棧溢出甚至不會成為問題。 這將是另一個反對在標准中規定尾調用優化的論據。 (但不要誤解我的意思,即使沒有堆疊,我也可以看到這種優化的優點!)

為了回答你的上一個問題:標准絕對不應該做任何關於優化的陳述。 可能存在或多或少難以實施的環境。

語言標准定義了語言的行為方式,而不是如何實現編譯器。 優化不是強制性的,因為並不總是需要。 編譯器提供選項,以便用戶可以根據需要啟用優化,並且同樣可以將其關閉。 編譯器優化會影響調試代碼的能力(以逐行方式將C與匯編匹配變得更加困難),因此僅根據用戶的請求執行優化是有意義的。

在某些情況下,尾調用優化可能會破壞ABI,或者至少很難以語義保留的方式實現。 例如,可以考慮共享庫中與位置無關的代碼:某些平台允許程序動態鏈接到庫,以便在各種不同的應用程序都依賴於相同的功能時保存主內存。 在這種情況下,庫被加載一次並映射到程序的每個虛擬內存中,就好像它是系統上的唯一應用程序一樣。 在UNIX以及其他一些系統上,這是通過對庫使用位置無關代碼來實現的,因此尋址是相對於偏移而不是絕對的固定地址空間。 但是,在許多平台上,位置無關代碼不能進行尾調用優化。 所涉及的問題是導航程序的偏移量必須保存在寄存器中; 在Intel 32位上,使用%ebx ,這是被調用者保存的寄存器; 其他平台遵循這一概念。 與使用普通調用的函數不同,那些部署尾調用的函數必須在分支到子例程之前恢復被調用者保存的寄存器,而不是在它們自行返回時。 通常,這沒有問題,因為此時,最頂層的調用函數不關心存儲在%ebx的值,但是位置無關代碼依賴於每個跳轉,調用或分支命令的該值。

其他問題可能是等待面向對象語言(C ++)中的清理,這意味着函數中的最后一次調用實際上並不是最后一次調用 - 清理工作。 因此,在這種情況下,編譯器通常不會進行優化。

當然, setjmplongjmp也是有問題的,因為這實際上意味着函數可以在實際完成之前多次完成執行。 在編譯時很難或不可能優化!

人們可以想到的技術原因可能更多。 這些只是一些考慮因素。

對於那些喜歡通過構造證明的人來說,這里是Godbolt做一個很好的尾部調用優化和內聯: https ://godbolt.org/z/DMleUN

但是,如果您將優化調整為-O3(或者如果您等待幾年或使用不同的編譯器,則毫無疑問),優化將完全消除循環/遞歸。

這是一個示例,即使使用-O2也可以優化為單個指令: https//godbolt.org/z/CNzWex

編譯器通常會在調用另一個函數后識別函數不需要執行任何操作的情況,並用跳轉替換該調用。 許多可以安全地進行安全檢查的案例很容易識別,而且這類案件有資格成為“安全低懸的果實”。 然而,即使在可以執行此類優化的編譯器上,它應該或將要執行時也可能並不總是顯而易見的。 各種因素可能使尾部呼叫的成本大於正常呼叫的成本,並且這些因素可能並不總是可預測的。 例如,如果函數以return foo(1,2,3,a,b,c,4,5,6);結尾return foo(1,2,3,a,b,c,4,5,6); ,將a,b和c復制到寄存器中,清理堆棧然后准備傳遞參數可能是切實可行的,但可能沒有足夠的寄存器來處理foo(a,b,c,d,e,f,g,h,i); 同樣。

如果一種語言有一個特殊的“尾調用”語法,要求給出的編譯器盡可能進行尾調用,否則拒絕編譯,代碼可以安全地假設這些函數可以任意嵌套。 然而,當使用普通的調用語法時,沒有一般的方法來知道編譯器是否能夠比“普通”調制器更便宜地執行尾調用。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

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