這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
翻譯原文連結 轉帖/轉載請註明出處
英文原文連結 發表於2014/06/07
函數調用不是免費的
一個函數調用有三個步驟。建立一個新的堆棧框(stack frame)並把調用者的詳細資料記錄下來。把任何會被被調用函數用到的寄存器內容儲存到堆棧。計算被調用函數的地址,並執行跳轉指令到那個新的地址。
因為函數調用是頻繁操作,CPU的設計者花費了很多精力來最佳化這個過程,但他們不可能消除所有的開銷。
根據被調用函數的功能,這個調用開銷可能是可以忽略不計的,也可能是非常顯著的。有一個降低調用開銷的最佳化技術叫內聯(inlining)。
Go語言編譯器通過把被調用函數代碼當作調用者代碼的一部分來實現內聯。內聯也是有代價的。它會增加編譯出來的二進位可執行檔的大小。只有在調用函數的開銷佔到被調用函數本身的工作量很大一部分的時候,內聯才有意義。所以只有簡單的函數才被考慮啟用內聯。調用函數的開銷往往不佔複雜函數的大頭,所以他們也就不會被內聯。
上面這個例子展示了函數Double對util.Max的調用。為了降低調用util.Max的成本,編譯器會把util.Max內聯到Double函數裡,產生如下內容:
內聯之後,util.Max將不會被調用,但是Double的行為並沒有改變。內聯並不是Go語言專屬的。幾乎所有編譯的或者即時編譯(JITed)的語言會提供這項最佳化。那麼Go語言裡的內聯是怎麼工作的呢?
Go語言的實現非常簡單。當一個包(package)被編譯的時候,任何適合內聯的小函數都被標記並且按正常情況編譯。然後將原始碼和編譯後的二進位同時儲存下來。
上面的圖片顯示了util.a的內容。原始碼被做了稍微的改動以方便編譯器的快速處理。當編譯器編譯Double的時候,它會發現util.Max是可以內聯的並且util.Max的原始碼也存在。這時編譯器會插入原函數的原始碼,而不是插入一個util.Max的調用。
儲存原始碼還使得其它最佳化成為可能。
比如上面這個例子,雖然Test函數總是返回false,Expensive在執行它之前是無法知道的。但是當Test被內聯的時候,我們就得到了如下的代碼:
這樣編譯器就能知道那塊代碼是不會被執行到的。
這樣不僅節省了調用Test函數的開銷,它還節省了編譯任何不會被執行的代碼。Go編譯器能夠自動在多個檔案或者包(package)之間實現函數內聯。如果某些代碼調用了來自標準庫的可內嵌函式,Go編譯器同樣可以將這些函數內聯進來。