1 回答

TA貢獻1851條經驗 獲得超5個贊
在我看來,調用 C 函數的開銷必須盡可能低,就像設置寄存器 rcx、rdx、rsi、rdi、執行一些快速調用和獲取 rax 值一樣。但我聽說cgo 的開銷很大<...>
你的意見是沒有根據的。
從 Go 到 C 的調用具有明顯開銷的原因是由于以下原因。
我們首先考慮 C
雖然語言沒有任何要求,但由典型編譯器編譯并在典型操作系統上作為常規進程運行的典型 C 程序嚴重依賴操作系統來執行其運行時環境的某些方面。
據說最明顯和最重要的方面是堆棧:內核負責在加載和初始化程序映像之后以及在將執行轉移到新生進程代碼的入口點之前對其進行設置。
另一個關鍵點是,雖然不是嚴格要求,但大多數 C 程序依賴于 OS 本地線程來實現多個通過程序代碼同時執行的流程。
在 C 代碼中執行的函數調用通常使用與操作系統和硬件實現的目標組合相同的ABI進行編譯(當然,除非程序員明確設法告訴編譯器以其他方式執行 - 例如,標記特定的函數具有不同的調用約定)。
C 沒有自動管理非堆棧內存(“堆”)的方法。
這種管理通常是通過 Cmalloc(3)
系列的標準庫函數來完成的。這些函數管理堆并將通過它們分配的任何內存視為“他們的”(這是非常合乎邏輯的)。
C 不提供自動垃圾收集。
讓我們回顧一下:從 C 編譯的典型程序:使用操作系統提供的線程并在這些線程中使用操作系統提供的堆棧;函數調用大部分時間遵循平臺的 ABI;堆內存由特殊的庫代碼管理;沒有GC。
現在讓我們考慮 Go
任何一點 Go 代碼(包括您的程序和運行時的代碼)都在所謂的 goroutine 中運行,這些 goroutine 就像超輕量級線程。
Go 運行時提供的 goroutine 調度程序(編譯/鏈接到任何用 Go 編寫的程序)實現了所謂的 M×N 的 goroutine 調度——其中 M 個 goroutine 被多路復用到 N 個 OS 提供的線程上,其中 M 通常是遠高于N。
Go 中的函數調用不遵循目標平臺的 ABI。
具體來說,AFAIK 當代版本的 Go 傳遞堆棧上的所有調用參數1。goroutine 總是在操作系統提供的線程上運行。
等待 Go 運行時管理的某些資源(例如通道上的操作、計時器、網絡套接字等)的 goroutine 不占用 OS 線程。
當調度程序選擇一個 goroutine 執行時,它必須將它分配給一個空閑的 OS 線程,該線程由 Go 運行時擁有;雖然調度器會努力將 goroutine 放在它在掛起之前正在執行的同一線程上,但這并不總是成功,因此 goroutine 可以在不同的 OS 線程之間自由遷移。
以上幾點自然會導致 goroutine 擁有自己的堆棧,這些堆棧完全獨立于操作系統為其線程提供的堆棧。
與 C 不同,這些堆棧是可增長的和可重新分配的。
堆內存由 Go 運行時自動管理,并且直接完成,不使用 C 標準庫。
Go 有 GC,并且這個 GC 是并發的,因為它與執行程序代碼的 goroutine 完全同時運行。
讓我們回顧一下:goroutines 有自己的堆棧,使用的調用約定既不兼容平臺的 ABI 也不兼容 C 的調用約定,并且可能在不同的 OS 線程上執行它們的不同執行點。
Go 運行時直接管理堆內存并具有完全并發的 GC。
現在讓我們考慮從 Go 到 C 的調用
正如您現在應該看到的那樣,Go 和 C 代碼運行的運行時環境的“世界”大不相同,以至于在執行FFI時需要一定的網關——成本不為零。
特別是,當 Go 代碼即將調用 C 時,必須執行以下操作:
goroutine 必須鎖定到它當前運行的操作系統線程(“固定”)。
由于目標 C 調用必須根據平臺的 ABI 完成,因此必須保存當前的執行上下文——至少是那些將被調用丟棄的寄存器。
機器必須以
cgo
遞歸方式驗證任何即將傳遞給目標 C 調用的內存不包含指向 Go 管理的其他內存塊的指針——這是為了允許 Go 的 GC 繼續并發工作。執行必須從 goroutine 堆棧切換到線程堆棧:必須在后者上創建一個新的堆??蚣?,并且必須根據平臺的 ABI 將目標 C 調用的參數放置在那里(和寄存器中)。
呼叫完成。
返回時,必須將執行切換回 goroutine 的堆棧——再次通過網關將任何返回的結果返回到正在執行的 goroutine 的堆棧幀。
正如您可能看到的,存在不可避免的成本,將值放入某些 CPU 寄存器是這些成本中最可以忽略不計的。
對此可以做些什么
一般來說,有兩個向量來解決這個問題:
使對 C 的調用不頻繁。
也就是說,如果對 C 的每次調用都執行冗長的 CPU 密集型計算,則可以推測執行這些調用的開銷與使這些調用執行的計算更快的收益相形見絀。
在匯編中編寫關鍵函數。
Go 允許直接在目標硬件平臺的程序集中編寫代碼。
一個可以讓您兩全其美的“技巧”是利用大多數工業編譯器的能力來輸出他們編譯的函數的匯編語言形式。因此,您可以使用 C 編譯器提供的核心工具,例如自動矢量化(用于 SSE)和積極優化,然后抓取它生成的任何內容并將其包裝在一個薄的匯編層中,該層基本上使生成的代碼適應本機Go 的 ABI。
有許多 3rd-party Go 包可以做到這一點(比如this和that),顯然 Go 運行時也可以做到這一點。
- 1 回答
- 0 關注
- 197 瀏覽
添加回答
舉報