The world at your fingertips — 天涯明月刀幕後12(新的旅程)

這個版本暴露出了一些問題,我們整理了一下,開始一一解決。為了第一次見玩家做準備。

一個是規模越來越大,開發迭代變慢了。光用ssd,已經不足以滿足我們的胃口,我們還希望編譯過程更快更好。

二是地圖開發成為一個巨大的問題。傳統的開發流程,已經很難支撐這個項目的規模,我們想做更大的地圖,更多的內容。而且地圖的質量也亟待提高。

三是性能也需要逐漸納入考慮,技術評審亮了警告,在這個時刻,分明將開發過程分為兩段。往前看,是預研,可以任性的隨意開發,往後看,要見玩家,該收斂的特性,也需要逐漸收斂。

後面幾回展開討論這幾個話題。

編譯提速

正如影響策劃、美術開發效率的是編輯器質量,影響程序開發迭代時間的一個重要因素是編譯速度。項目規模略大,影響大家的迭代速度了,我想花點時間處理一下這個事情,提高一下編譯速度,給團隊加點迭代效率的Buff。

這又是個不在關鍵路徑上的任務,不影響團隊主體任務,便於用碎片時間開發和研究,搞定最好,失敗也無妨的事情,比較適合我來做。

先研究Compile階段。

這個階段相對還好,可以用Incredibuild之類的分散式編譯工具來提速。Incredibuild的基本原理,是在開發團隊每個人的機器上都裝一個Incredibuild的客戶端,然後有一個Server來協調同步,把所有的Incredibuild客戶端都登記起來。當有電腦需要編譯的時候,所有的編譯任務通過Incredibuild來進行,它的客戶端會將編譯單元分發到不同的客戶端,共同編譯,最後把編譯結果匯總回來,在本機完成最後的Link。

我做了點試驗,發現這裡還是有巨大的坑。我做了詳細的編譯時間統計,看了使用或者不使用incredibuild。我注意到,整個工程中的多個項目,情況不一,有些能從Incredibuild中得到巨大的收益,有些不能,反而會更慢。這個就讓人非常不能理解。

深入看了一下,做了點研究。Incredibuild的工作原理,是把每一個CPP的編譯單元,發送到不同的機器上去編譯,在我的理解裡面,一個編譯單元包括了一個需要編譯的CPP,以及所有的Include的頭文件,這是相當巨大的數據,大型項目錯綜複雜的Include關係,可以輕易讓這個編譯單元多到數十M。另一方面,大項目為了加速編譯,都會有一個PCH預編譯頭文件,原理就是把這個項目中每一個CPP文件需要的公共頭文件放在一起,一次編譯生成一個PCH文件,供後續所有CPP編譯的時候直接使用,避免重新編譯這部分公共頭文件。這本可以帶來巨大的提升,可惜和Incredibuild合作的時候就不那麼完美了。

Incredibuild為了讓每個編譯單元可以獨立編譯,要把這個預編譯PCH文件發送到每一台機器上去,然後才能開始編譯。一部分項目的預編譯頭文件比較小,這個過程很快就結束了。另一些項目比較大,這個傳輸PCH文件的過程,本身就會佔用很長的時間,偏偏我們的開發用網路還是百兆網,更是雪上加霜,導致後續通過分散式編譯節約下來的時間,無法彌補因為傳輸PCH文件帶來的開銷,反而就變得更慢了。

解決方法就不太好辦了。要麼我給IT寫個郵件,鼓勵他們把全公司的內網都升級成千兆網,但想想自己的影響力似乎也沒有那麼大,就放棄了這個念頭。另一個思路是把預編譯頭文件拆開變小,PCH小了自然初始開銷也小,雖然會給後續每一個編譯單元帶來更多的編譯時間,但因為這個時間分散在很多不同的電腦上,所以綜合看來還是可以賺的。我做了些嘗試,把PCH拆散拆細,果然是有效的,就是太累了,需要手工調整很多項目。

仔細看看本機編譯,其實多數時間裡Incredibuild能夠帶來的收益也不算太大。高速CPU+SSD帶來的吞吐量和計算量提升,很多時候能彌補Incredibuild帶來的分散式編譯提升。很多年前,CPU還不是那麼快,硬碟還是HDD,那時候分散式編譯的確有巨大的好處,但目前這個項目規模下,本機編譯應該是更靠譜的一個選擇。

值得一提的是,在不同規模的項目下,是否使用Incredibuild是一個需要經常重新評估的事情。天刀在那段時間的開發中,的確是本機編譯更快,但在更後期的開發中,隨著規模進一步變大,逐漸Incredibuild也顯示了一定的優勢,所以還需要綜合考慮。當然如果有千兆網甚至萬兆網的話,Incredibuild應該是一個最好的選擇了。

看完Compiling,我再看Link時間。

Link時間其實是更影響日常開發效率的一個因素。對於大規模分布編譯,一般只在項目需要完整才有價值,而程序員的日常開發,其實都是修改幾個CPP文件就要編譯一次。那個情況下,都是增量編譯,Link時間才是影響迭代的大頭。特別是對GPP程序員,經常需要做細微的調整,迭代速度會被Link時間影響。

Link時間的優化方法並不好找,之前也沒有太多的積累。正好由於機緣巧合,看到一篇文章【1】很有意思。 文章提到了做了一個Python腳本,利用Dumpbin來掃描解析所有生成的Obj文件,從中檢查有多少重複的導出函數,然後試圖去掉這些函數。這樣做後,Link程序要處理的導出函數更少,需要Resolve的函數少了自然就更快。這個思路非常棒,我也在我們項目中做了點實驗。

照例先樹立測試標杆。我挑選了一個編譯最慢的項目,在什麼優化都沒做前,編譯一次,用了9.2s,掃描後發現一共導出了36400個函數。

仔細檢查了函數表,大量函數都是我們在生成對象描述元信息的時候,在宏定義裡帶入的。相當多的Inline函數,其實並不是為了提高性能,只是在寫類定義的時候,覺得隨手可以寫到頭文件里的,本身也沒多少語句,那就順手這麼做了。既然這是一個影響效率的因素,那我就認真重新寫一下生成對象信息的宏語句,只保留函數的申明,把具體函數分散到CPP中。

這個改動有一定的效果。Link效率有了一定的提高,快了0.7s,變成8.5s的Link時間,導出函數變少,只有25700個了。雖然進步不大,不過這個方法有效,也讓我很高興,沒做過的事情,總是很有樂趣。

繼續優化。這個項目是一個GPP項目,需要根據策劃配置數據,生成大量技能表格,供伺服器和客戶端共享。但這又不是一個必須的模塊,並不經常變化,我就把這個模塊移到一個獨立的項目,嚴格說這個事情並沒有減少總共所需的編譯量,但還是會有幫助,把經常變化的和不常變化的分開,那麼多數時間我們就不用處理那些不常變化模塊帶來的編譯時間開銷。

又快了2s,Link時間順利縮短到6.5s,只有13200個函數被導出了。

我們進一步努力,做一些同一數量級上的優化,移動任務表格、Buff表格等數據源到公共項目中,進一步縮短了Link時間,5.9s,10900個函數被導出。

最終看見剩下的導出函數大頭,都是在Speedtree裡面,Speedtree的頭文件,被大家偷懶加入了公共的頭文件,而事實上使用Speedtree函數的文件並不多。我毫不猶豫的從公共頭文件中去掉了Speedtree的頭文件,然後所有調用Speedtree函數的Cpp文件編譯就出錯了。我根據項目編譯出錯情況一一修正編譯錯誤,在那些Cpp文件中,單獨include speedtree的頭文件。這樣做完,我們就避免了把speedtree頭文件中的inline函數在多個不同的地方被調用,也就順利減少了導出函數。經此一役,導出函數大幅減少到700個,時間縮短了1.4s,只需要5.1秒就能Link完。

忙了1天多,終於把一個庫的link時間降低了大約4s。後續我又用碎片時間,陸陸續續把其他一些link較慢的庫也做了一些相關的調整。

Link時間的優化對整體效率有著隱性微妙的提升。提高4秒看似微不足道,但如果一個程序員每小時編譯10次,一天就是80次,省320s,整個團隊30程序,省9600s,已經是相當可觀的一個時間。更不要說編譯過程如果夠快,迭代過程夠順利,有助於程序員更集中注意力,程序員也不至於在編譯中開小差,團隊程序員總在上網、上廁所、看手機往往是有原因的,說不定就是迭代太慢編譯速度不夠。

下次我們再說地圖的方案,那是早期測試中的重要技術亮點。

【1】Speeding C++ links


推薦閱讀:

《飢餓鯊:進化 》遊戲體驗怎麼樣?
網頁遊戲的開服表都怎麼看?
【戰艦世界】金幣船個人點評(6級篇下)
劍網3好玩嗎?
歷史上有哪些經典的MMORPG網遊作品,其成功因素是什麼?

TAG:天涯明月刀遊戲 | 軟體開發 | 網路遊戲 |