首頁 > 遊戲

不喜歡 D 和 C++,程式設計師將 58000 行程式碼移植到 Jai 語言?

由 CSDN 發表于 遊戲2023-01-02

簡介根據這些特性,我的移植計劃如下:將一小段程式碼從 D 或 C++ 複製到 Jai,然後編譯

你怎麼理解喜歡這個詞

不喜歡 D 和 C++,程式設計師將 58000 行程式碼移植到 Jai 語言?

摘要:將已有的上萬行程式碼遷移至另一種程式語言,從來就不是一件容易決定的事情,而本文作者卻信心滿滿地要將 5。8 萬行程式碼全部用另一種不那麼主流的語言重寫,這是為什麼呢?

連結:https://www。yet-another-blog。com/porting_the_game_to_jai_part0/

宣告:本文為 CSDN 翻譯,未經允許禁止轉載。

作者 | Simon van Bernem

譯者 | 彎月 責編 | 鄭麗媛

出品 | CSDN(ID:CSDNnews)

在這篇文章中,我將分享把一款正在開發的遊戲移植到 Jai 語言的經過。遊戲本身主要是用 D 和 C++ 編寫的,總共有 58,620 行程式碼(不包括庫)。

不喜歡 D 和 C++,程式設計師將 58000 行程式碼移植到 Jai 語言?

原因

你可能想問,為什麼要將如此大規模的程式移植到另一種程式語言?你完全可以等到新專案再用新語言嘛!

我之所以移植這些程式碼,原因主要有以下幾個:

目前這些語言給我的日常工作帶來了無盡的痛苦。

我有合適的系統來幫助我移植程式碼,所以我認為此次移植能夠順利進行。

對於我來說,Jai 似乎比 C++ 或 D 更具吸引力。

最重要的原因是:我喜歡 Jai!

為什麼放棄 C++

網上有很多文章訴說了 C++ 的缺點,所以我不打算在此贅述。簡單來說,C++ 幾十年的發展積累了很多錯誤決定,我們沒有任何方法去擺脫它們。標準庫是一場災難,使用其他人的程式碼也非常困難,而且不知何故,C++ 每次新增的新功能都有陷阱。

雖然 C++ 的這些缺點也不至於糟糕到讓人避之不及,但它確實給我帶來了很多痛苦。此外,這些年來 C++ 的發展似乎不盡如人意,我不覺得這門語言會越來越好,所以我還是想逃離 C++ 生態系統。

為什麼放棄 D 語言

2019 年,當我開始開發這款遊戲時,已決定放棄 C++,但我不確定應該選用何種語言。最終,我選擇了 D 語言,因為這門語言與 C++ 類似,但沒有 C++ 的那些缺點。然而不幸的是,事實證明 D 語言也有一些 C++ 中不存在的缺點。

雖然 D 語言有一些優點,比如更強大的超程式設計、不需要標頭檔案、沒有未初始化的值等,但相較於缺點而言,這些優點不值一提。

我在 Windows 的兩款 D 編譯器(dmd 和 ldc2)之間來回折騰了 4 年之久,到頭來卻發現在 Windows 上編寫 D 程式碼,只適合個人的業餘專案或早期不成熟的軟體,該語言完全不像有 20 多年的發展經歷。

就目前的情況來看,我不建議任何人使用 D 語言在 Windows 開發正式專案。相較而言,繼續使用 C++ 才是更好的選擇。根據我多年的經驗來看,在 Windows 編寫 D 程式碼所面臨的最大問題在於,其除錯資訊千瘡百孔:

90% 的情況下不會給出 this 指標,或給出錯誤的 this 指標;

函式堆疊上的變數經常不完整、丟失或有誤;

變數的值有時會報告錯誤,卻看不到任何其他問題;

靜態 foreach 擴充套件的處理不當,甚至會導致偵錯程式紊亂;

mixins(相當於 D 語言的宏)生成的除錯資訊會導致偵錯程式找不到正確的檔案(因此你需要逐步反彙編);

在 visual studio 中,將指令指標移到上一行常常會導致程式在下一條指令上崩潰。

網上有人告訴我,一直以來 DMD 的除錯資訊就存在很多質量上的問題,但不幸的是,上述大部分問題不僅限於 DMD,Windows 的兩個編譯器都有這些問題。除了除錯之外,超程式設計的核心部分還存在其他問題和缺點:

不同的編譯階段以奇怪的方式互動,常常導致超程式設計出現意外,同時還會產生具有誤導性的錯誤;

ldc2 的編譯速度非常慢,但有時這款編譯器是唯一的選擇,因為 DMD 有 bug;

D 提供了一種 betterC 模式,其中包括禁用垃圾收集。然而,在使用這種模式時,標準庫不會被編譯,而且超程式設計也會遇到重大問題;

缺少文件;

此外,還有一堆小問題。

總之,雖然從某些方面來看,D 確實比 C++ 略勝一籌,但其他方面的小問題非常多,累積起來導致使用 D 語言程式設計也非常痛苦。糟糕的除錯資訊和垃圾收集的需求很致命,我的整體感受是,D 的創始人對 C++ 的看法似乎與我截然不同。我只是希望改進 C++,而不是用一些 C++ 的問題來換取 D 的其他問題。現在我對偵錯程式有嚴重的信任問題。

為什麼選擇 Jai

Jai 是 Jonathan Blow 於 2014 年開發的一款程式語言,而編譯器一直到 2019 年 12 月才開始內測。如今封閉測試仍在進行中,大約兩個月前,我應邀參加了這項測試。

Jai 的設計初衷也是希望改進 C++,但與 D 語言不同,Jai 正在對 C++ 做出有意義的改進。在我看來,最重要的改進包括編譯速度更快,以及允許透過無限制的編譯時執行來實現超程式設計。

請注意,這裡我所說的編譯速度提升可不止 20%,而是 10~100 倍;而且你能在編譯期間執行任何操作,不僅限於超程式設計。尤其是,超程式設計與編譯時編譯器 API 的結合使用具有深遠的影響,例如你不再需要構建系統,或啟用複雜的自定義檢查。除此之外,Jai 還對 C++ 進行了其他方面的改進,比如更好的預設值、更簡單的語法、更實用的標準庫、命名函式引數、上下文、using 等等。我希望我的遊戲從 D 移植到 Jai,能夠獲得以下提升:

編譯時間從現在的 60 秒減少到 5 秒以下,爭取能縮短至 1 秒左右;

偵錯程式能夠正常工作;

用 Jai 程式碼替換構建指令碼;

使用超程式設計引入自定義編譯檢查,以抓取更多錯誤;

用更簡單的命令式程式碼替換複雜的超程式設計程式碼;

各種語法改進,減少程式碼中的繁瑣部分;

刪除使用多種語言時不可避免的重複。

我希望透過這篇文章記錄我的期望,將來可以回過頭來檢查有多少期望真的實現了。

為什麼不是其他語言

如上所述,我不喜歡 C++ 和 D 語言,而 Jai 看起來很不錯。那麼,其他程式語言呢?似乎 Rust 也是一個很好的備選。這門語言風頭正盛,而且還有一個熱心的社群,但我個人認為 Rust 並沒有做出正確的權衡。

它的支持者都是唯“記憶體安全”是論者,考慮到如此多的漏洞都是記憶體安全引發的,我可以理解這種心態,但他們忽略了其他高安全性、高質量的方法。例如,我相信如果 C 和 C++ 沒有零終止字串、預設初始化值,那麼大部分漏洞都不會存在;再加上合適的指標+長度型別,就可以用邊界檢查程式碼取代 90% 的指標計算;然後再建立一種文化,不鼓勵大家自己想辦法自行管理記憶體。

除此之外,我認為,我們在尋找“安全的”程式碼時完全忽視了我們擁有如此多漏洞的最主要的因是,我們的文化對複雜性的容忍,甚至是鼓勵。總之,忍受 Rust 慢吞吞的編譯並接受借用檢查器是一種極端的解決方案,並沒有解決最重要的問題,這是一個文化上的問題。另一方面,Jai 非常在意複雜性,並努力建立正確的文化。

對於其他不太受歡迎的語言,例如 Zig,我只能說雖然它們可能具有巨大的潛力,但我並無法相信它們是正確的選擇。我不是說這些語言不好,只是它們不適合我。

移植方法

在本文開頭,我曾說過我認為此次移植能夠順利進行,原因是我的遊戲中有兩個系統,對此次移植有很大的幫助:

我的遊戲可以將遊戲過程中的輸入(HID、載入的檔案、網路等)記錄到一個檔案中,之後進行回放。在回放的過程中,系統可以將記錄下來的輸入傳遞給遊戲迴圈,從而重現一模一樣的遊戲狀態。

玩遊戲期間,系統可以在各個時間點,對遊戲狀態進行雜湊處理,並將這些雜湊值儲存到不同的檔案中。在回放整個遊戲過程時,系統可以利用這些檔案檢查遊戲的狀態是否與原來匹配。

實際的功能和上面的描述有一些細微的差別,但不會影響整體的邏輯。根據這些特性,我的移植計劃如下:

將一小段程式碼從 D 或 C++ 複製到 Jai,然後編譯;

呼叫這段 Jai 程式碼;

回放錄製的遊戲會話;

如果回放出現分歧,則說明移植引入了 bug,修復 bug;

如果回放沒有出現分歧,則說明移植成功;

重複以上操作。

關於該方法是否有效,我需要考察兩個關鍵問題:

移植引入的 bug 是否會導致遊戲狀態出現明顯的分歧?

能否以少量、漸進式的方式移植程式碼,這樣在得知存在 bug 時,更容易找到bug?

第一個問題取決於狀態雜湊覆蓋了多少程式碼。一部分程式碼需要判斷遊戲是否正在回放,這部分程式碼在回放時有不同的行為,因此無法完成真正的雜湊處理。例如,寫入檔案的功能在回放時會直接丟棄所有資料,因此如果移植在寫入檔案的程式碼時引入 bug,則不會被雜湊處理注意到。幸運的是,大多數程式碼不屬於這一類。

最初,只有很小一部分程式碼使用了雜湊處理,例如物理模擬,但最近我設法進行了擴充套件,在向動態陣列插入資料時,用雜湊來記錄其大小和容量。這意味著,插入動態陣列的程式碼中的 bug 很快就會被發現。由於動態陣列的使用在我的程式碼中隨處可見,所以對於龐大且複雜的數學演算法來說,即便是一個很小的變化,也能帶來立竿見影的效果。

第二個問題是一個經典的程式設計問題:程式碼的解耦性如何?這個問題非常有趣,因為在移植的過程中,我將親眼目睹我的程式碼庫中究竟封存了多少不為我所知的複雜性。一個明顯的問題是模板函式,這些程式碼無法直接移植,因為函式的定義和呼叫必須在同一個編譯器中,模板才能發揮作用,除非你手動例項化模板。我的程式碼庫中有大量的模板化程式碼,但大多不依賴於容器或序列化,所以我希望不會引起太大的麻煩。

移植過程

下面這張圖是移植前的程式碼庫狀態:

不喜歡 D 和 C++,程式設計師將 58000 行程式碼移植到 Jai 語言?

整個程式碼庫有 45,701 行 D 程式碼和 12,919 行 C++ 程式碼,總共 58,620 行。 編譯時間如下:

不喜歡 D 和 C++,程式設計師將 58000 行程式碼移植到 Jai 語言?

在除錯模式下,ldc2 需要 3 分鐘才能完成編譯,記憶體使用量峰值約為 8GB,如果開啟瀏覽器,我膝上型電腦的 16GB 記憶體很容易就飽和了。釋出模式更糟糕,約為 11。5GB。

為了記錄移植進度,我繪製瞭如下程式碼庫的示意圖:

不喜歡 D 和 C++,程式設計師將 58000 行程式碼移植到 Jai 語言?

不喜歡 D 和 C++,程式設計師將 58000 行程式碼移植到 Jai 語言?

如果一切按計劃進行,上面兩張圖中的顏色都會改變,我非常期待!

最後,我來說一下我期待的效果:

整個移植的過程需要 160 小時,每週工作 40 小時,一共需要一個月。

編譯時間從 1 分鐘縮減到 5 秒以下,理想值為 1 秒左右。

Tags:C++程式碼移植Jai編譯