我是如何把5萬行C++代碼移植到Go的?

我是如何把5萬行C++代碼移植到Go的?

導讀:Go 語言的創始人之一 Rob Pike 曾表示,他希望 Go 能夠被 C++ 程序員所接受,但結果差強人意。最近,在作者就職的 HFT 公司裡,一個團隊成功地把一些對速度不太敏感的基礎設施代碼從 Python 移植到了 Go,這也促使他們決定嘗試用 Go 對複雜冗餘的 C++ 服務端程序進行重構,這些代碼有 5W 行之多,並且對吞吐量有一定的要求。

這個服務端程序使用了跟公司核心交易軟件相同的技術和庫,不同地是交易軟件對系統的延遲更敏感,幾乎每一微秒都很重要,而 C++ 服務端並不需要這種程度的性能。

因此,使用 Go 自帶的調度程序完全可以滿足要求,沒有必要使用交易系統實現的超優化 C++ 框架,雖然損失了一些性能但獲得了更好的可維護性。需要一提的是,本文作者負責了整個代碼的重寫工作。

我是如何把5萬行C++代碼移植到Go的?

從商業角度來看,這個項目是成功的:重寫工作提前完成;性能在可接受的範圍之內;並且整體代碼量不超過 1W 行(代碼量的劇減主要是因為重寫團隊刪除了一些過時的或者不需要的特性)。但從開發者的角度來看,作者認為結果並不是最優的。Go 並不支持參數多態,作者因此使用了兩到三倍的代碼來實現類似功能。其中一部分是為了保障類型安全:Go 強制開發者在類型修飾和類型安全之間做出取捨,作者選擇了一個比較均衡的實現。總的來說,如果需要一般的類型安全,那麼相對少的代碼就可以實現,而如果需要更好的類型安全,則需要更多的代碼。

接下來讓我們對比一下 Go 語言的優缺點。

優點:

1、Emacs 開發平臺

藉助自動完成、跳轉到定義、保存時的錯誤檢查、智能重構和 GoTest 集成等插件,Emacs 成為了 Go 語言環境下最好的 IDE 工具。另外,它也可以很方便地通過 Elisp 進行定製和擴展。如果你本人恰好是 Emacs 的愛好者,這絕對是一個大大的加分項。

附上一些Go的教程,適合剛入門級別看看,算是福利。關注頭條號,私信回覆“資料”獲取。
我是如何把5萬行C++代碼移植到Go的?

2、Goroutines(協程)

Go 實現了基於消息傳遞的併發,作者認為這是最簡單的併發形式,使用也超級方便。通過將 GOMAXPROCS 設置為 1,Go 還允許開發者通過使用與併發代碼完全相同的方式來編寫並行 / 異步代碼。與其它提供內置輕量級線程調度器的語言 Erlang/Elixir 和 Haskell 相比:前者缺乏靜態類型,後者在實際開發中很少被管理人員採用。

3、沒有繼承

在很多情況下,基於繼承的 OO(面向對象)是一種反模式,這些冗餘和模糊的代碼幾乎沒有什麼好處,Go 則直接取消了這類代碼。這有可能也是 Rob Pike 等人設計 Go 的初衷:谷歌內部有一大堆類似於企業版本 Fizzbuzz 的 Java/C++ 代碼,他們希望能從這些代碼中徹底解放出來。也就是說,儘管在舊的 C++ 服務端遺留代碼中使用繼承是合理的,但最好還是使用更現代的風格來重寫代碼,而且重寫過程也並不複雜。

4、更好的可讀性

Go 代碼更易於閱讀和理解。相比之下,很多 C++ 代碼需要幾個小時才能完全理解。Go 本身也促使開發者編寫可讀的代碼:這種語言完全避免了下面這種自做聰明的情形(https://www.reddit.com/r/programmingcirclejerk/comments/b0wkue/go_also_forced_me_to_write_readable_code_the/):

“嘿,這篇論文(基本上沒人讀得懂)中的>8=3 運算符可以讓我節省 10 行代碼,我最好把它寫進代碼裡,我的同事也不難理解這行代碼,因為它的意思已經在類型簽名中很清楚地表達出來了(反正我是沒看懂):(PrimMonad W, PoshFunctor Y, ReichsLens S) => W Y S((I -> W) -> Y) -> G -> Bool "。

5、簡單而規範的語法

當我們需要將一個封閉函數的名稱添加到每個日誌字符串的開頭時,如果使用 Emacs,一個簡單的 regexp find-replace(正則表達式) 命令就可以實現,而對於更復雜的語言則需要使用解析器。不論是通過 Emacs 宏或者是 Go 模板,簡單的語法可以更容易地生成代碼。

Emacs+Go== 參數多態:我們可以使用 Emacs 宏來加速生成 Go 所需要的"複製粘貼",而且,如果函數編寫正確,那我們也可以用 regex 命令來更新所有的"複製粘貼"函數。這樣,我們就可以很容易地更新 fooInt、fooFloat 和 fooDouble 等函數,對比支持參數多態的語言對 foo 函數的更新,整個過程沒有什麼太大區別。這樣做的缺點是,雖然 Emacs 宏和 regex 命令可以編寫和修改 Go 代碼,但它仍然不如真正的多態實現那樣簡潔和易讀;而且對於不熟悉 regex 以及可擴展編輯器(Emacs)的人來說,維護同樣也不容易。

6、有效的內置模板

通過 Go 的文本 / 模板包,我們可以很容易地生成新代碼。它還允許開發者在生成代碼時使用 IO:例如,有一個同某些特定服務交互的庫,它通過 XML Schema 生成。如果能夠用不同的函數來生成不同的數據類型,那麼就可以保證代碼的類型安全。

在 C++ 中,IO 不能在編譯時執行,因此不能使用上述模式來生成代碼。允許編譯時使用 IO 的語言有:

  • F#,通過 TypeProviders 實現。
  • Idris,也使有 TypeProviders。
  • Lisp,可以在宏中執行 IO。
  • Haskell,它有一個編譯期運行的函數 IO -> Q。
  • D,編譯時可以使用“import”來讀取文件。
  • Nimrod,有特殊的函數實現。
  • Elixir 或 Erlang,可以通過宏執行任意的 IO。
  • Rust,可以使用函數 libsyntax 在編譯時執行任意的計算和 IO。

缺點:

1、斯德哥爾摩綜合徵

前面已經提到,在允許使用 IO 的特性上,使用模板生成 Go 代碼要比用 C++ 元編程好得多,而 C++ 元編程在這裡顯然是多餘的,因為完全可以用另外一種可以支持 IO 的程序語言來生成代碼。

2、沒有實現參數多態

儘管很多人認為這在實踐中並不是一個問題,但在這裡,它是一個很嚴重的問題。如果把新的 Go 代碼再移植回 C++ 的話,考慮到 C++ 的函數多態和類型多態,代碼量可能會減少到目前的一半,並且具有更好的類型安全。如果用 Haskell 重寫的話,代碼量會更少,而使用 Clojure 的話,代碼量有可能控制到 1000 行以內,當然這些代碼可能很難被調試或維護。

3、犧牲了類型安全

針對服務器處理的各種 protobuffer messages(協議緩衝消息),我們使用了擴展屬性的方式,作者最初打算為每一種消息設置一種擴展屬性,這樣 FooExtensionAttribute 就不能用在 Bar 函數上。Go 並沒有實現參數多態和泛型,這意味著將會產生大量的重複代碼,所以最終只使用了一種 ExtensionAttribute,並且類型系統也沒有檢查它是否用於擴展合適的消息。

4、二進制文件太大

如果使用代碼來生成類型安全的 API,並確保每種數據類型都有明確的類型訪問器和諸如此類的東西,則很容易生成超過 10W 行的 Go 代碼以及 30MB 以上的二進制文件,編譯時間也會更長。在這種情況下,一般會超過 10 秒。當然,這不是一個很嚴重的問題,因為我們可以把代碼編譯成靜態庫,這隻需要一次,之後就可以通過靜態鏈接來訪問了。

5、內核兼容性有待提高

很多時候由於各種無奈的原因,需要把代碼部署到一箇舊內核上。而且,如果這個內核不支持最新的 Go 版本,就不得不換到一箇舊的、很慢的 Go 版本,這多少有些令人沮喪。

結語

Go 語言是一把雙刃劍:它禁止一切複雜的抽象,不管是優秀的抽象亦或是很差的抽象。如果你和你的同事正在使用很糟糕的抽象,那切換到不能使用抽象的 Go 語言自然很好,反之亦然。當然這也要取決於判斷抽象好壞的標準。

我是如何把5萬行C++代碼移植到Go的?


參考鏈接:

https://togototo.wordpress.com/2015/03/07/fulfilling-a-pikedream-the-ups-of-downs-of-porting-50k-lines-of-c-to-go/

本文轉自AI前線。


分享到:


相關文章: