GameHollywood 面試筆記 原 薦

GameHollywood 面試筆記

Intro

面試的職位是 C++開發工程師,主要聊的還是C++。在過程中自我感覺面得還行,至少沒上次那麼蠢。

聊的內容主要集中在STL和線程安全、資源管理的層面。

慣例的,填完面試信息表並簡歷一起上交,然後等面試官來客套完,就開始聊技術了。

注意,面試官的提問並非原話,有修飾和腦補。

0. 預熱:你用哪個版本的C++?

客套話什麼的就略了。

面試官:...行,那我們就聊聊C++吧。你常用哪個版本的C++?

我:我比較常用的是C++11。

C++版本這個問題面試裏應該不多見,不過作爲引入的話題還行,標準之神會瞑目的。

對於C++版本這個詞,很大概率上大家說的應該就是C++標準委員會WG21制定的C++標準了,最新版本的標準文檔是C++17定稿N4659,制定中的C++20標準文檔可以訪問WG21/docs/papers/2018查閱。

需要注意的是,如果答成了我用VC6之類的騷話,很大概率會留下不好的映像——或者對方也是忠實的VC6神教教徒的話,達成共識也說不定。

閒話少敘。

1. 起手式:std::shared_ptr

面試官:說說std::shared_ptr是怎麼實現的?一般怎麼去使用它?

答:shared_ptr是通過引用計數實現的,它可以作爲容器元素,在程序裏傳遞blabal.....而且shared_ptr不是線程安全的,它不能跨線程傳遞,要額外做一層包裝blabla......

正巧最近有想寫一篇智能指針相關的博客,面試官的第一問就提到了。

說到智能指針,就必須提一下RAII了。

1.1 異常安全和RAII

std::shared_ptr和其他智能指針類型都在<memory>頭文件裏定義,主要的作用是實現自動化的資源管理,基於RAII的理念設計和實現。

RAII指的是獲取資源即初始化,英文全寫是Resource Acquisition Is Initialization,屬於一種面向對象編程語言中常見的慣用法。

它的思路是這樣子的:初始化即獲取資源,離開作用域就自動銷燬。

RAII解決的問題是,當異常發生時,如何確保資源釋放。這是個異常安全的問題。

常見的非RAII風格代碼裏,如果要確保資源被正確釋放,就要用try {} catch() {} finally {}塊捕獲異常,然後執行資源釋放的代碼,再將異常重新拋出。

而RAII的理念是,讓資源的生命週期和一個棧上的對象嚴格綁定,確保棧上對象被析構的時候,資源也就被一同釋放了。

在C++中,有大量的代碼都是以RAII風格進行設計的,其中智能指針也是。

1.2 std::shared_ptr的實現

引用計數,大概瞭解過智能指針的人都能回答得出來。

雖然說實現方式並沒有規定只能是引用計數,但實際上大家都是這麼寫的,萬一哪天有個GC實現的std::shared_ptr也別太震驚。

實現思路也挺簡單。

所有指向同一實例的std::shared_ptr應當持有同一個引用計數,來保持所有std::shared_ptr計數同步,所以它們共同擁有一個計數器指針long *p

在複製時,shared_ptr管理的對象指針和引用計數器指針被同時複製,然後引用計數器指針保存的引用計數+1——銷燬同理,減少引用,直到刪除。

1.3 std::shared_ptrCopyAssignable

std::shared_ptr滿足CopyContructiableCopyAssignableLessThanComparable這些標準庫的具名要求,因此可以作爲STL容器的元素。

順便一提 Concept 有很大可能出現在 C++20 標準裏。

1.4 線程安全性

std::shared_ptr不是線程安全的,不然不滿足C++對Zero Cost Abstraction的要(吹)求(逼)。

依據官方說法,多線程訪問不同的std::shared_ptr實例是沒問題的(大多容器也是);多線程訪問同一個std::shared_ptr實例,但是隻調用const方法,那麼也是沒問題的(多線程讀);多線程訪問同一個std::shared_ptr實例,調用非const方法,那麼會產生數據競爭(多線程讀寫)。

如果希望在線程間傳遞 std::shared_ptr 得靠 STL 提供的原子操作庫std::atomic

std::atomic可以快速幫助包裝一個線程安全的對象或者指針,不過這東西對std::shared_ptr的特化是目前還在制定的C++20標準的一部分,所以能不用則不用,直到標準制定完成穩定,並且各編譯器支持完善後再行考慮。

除此之外,如果確實有這方面的考慮,引入boost是一個不錯的選擇。

無論如何,跨線程使用std::shared_ptr我不怎麼支持。

跨線程傳遞std::shared_ptr本身就是個非常危險的行爲。std::shared_ptr作爲標準庫的一員,揹負了C++的歷史包袱,它隨時可能被取出裸指針使用,或者意外複製了一次或幾次,而這些對線程安全幾乎就是意味着作死的行爲卻沒有任何管束。

1.5 其他智能指針

  • std::auto_ptr
  • std::weak_ptr
  • std::unique_ptr

其中std::auto_ptr已經被掃進歷史的垃圾堆了,作爲替代者,std::unique_ptr有更明確的語義和更高的可定製性。

std::weak_ptr是對於std::shared_ptr的補充,對於希望使用std::shared_ptr作爲使用了指針的數據結構之間的連接方式,又不希望產生循環引用惡劣情況的一個解決方案。弱指針的存在不影響引用計數工作。

最後是std::unique_ptr,它的語義是明確唯一持有某一資源,依照約定,被std::unique_ptr持有的資源不應該再有第二人持有,std::unique_ptr是唯一訪問該資源的入口。

這些智能指針都有一個共同點:爲了兼容C代碼,所以它們隨時可以被取出裸指針而不影響自身的工作,但這種使用方式造成的一切後果自負。

2. std::vector

面試官:...知道std::vector吧?講講它是怎麼實現的。

我:vector保存了一個一定長度的buffer,當插入時可以避免插入一次就分配一次空間blabla...當插入長度超過了buffer長度,buffer會依照內部算法來重新分配一次內存,擴張長度。

回答不全對。其實面試官之後又強調了一次,但面試時沒有聽出來。

面試官:那之前分配的buffer呢?

我:之前分配的buffer先複製到新的buffer裏,然後舊buffer會被釋放。

這裏對於釋放舊buffer的說法其實是有問題的,可以具體看看下面。

2.1 內存佈局

std::vector內存佈局是連續的,這一點除了幾乎每個人都有所瞭解之外(...),標準給出的要求也可以看出點端倪。

26.3.11.1 Class template vector overview

A vector is a sequence container that supports (amortized) constant time insert and erase operations at the end; insert and erase in the middle take linear time. Storage management is handled automatically, though hints can be given to improve efficiency.

關鍵點集中在這裏:

... constant time insert and erase operations at the end;

末端插入和刪除是常數時間

... insert and erase in the middle take linear time.

中間插入和刪除需要線性時間(就是 O(n))。

典型的數組插入和刪除的特徵,不同的是std::vector可以變長,所以真正插入大量數據的時候會有多次重新分配內存和複製的操作。

2.2 CopyAssignable的約定

std::vector要求儲存的對象滿足DefautConstructibleCopyContructiableCopyAssignable的具名要求,文檔參考26.3.11.1第2節。

26.3.11.1

A vector satisfies all of the requirements of a container and of a reversible container (given in two tables in 26.2), of a sequence container, including most of the optional sequence container requirements (26.2.3), of an allocator-aware container (Table 86), and, for an element type other than bool, of a contiguous container (26.2.1).

其中提到的Table 86中列出了DefaultConstructibleCopyAssignableCopyConstructiable

發揮一下腦洞,這些要求完美符合了之前對於重新分配內存的猜測對不對?

對象要可以被默認構造,因爲vector的實現可能是new了一個新的對象數組(更可能是字節數組,到時候再placement new);對象要可以被複制構造,因爲對象可能被從舊數組移動到新數組;對象要可以被複制構造.....

當然更可能的原因是vector本身是可複製的,上面的就當我吹逼吧。

除此之外還有CopyInsertableMoveInsertable的具名需求,就像其字面意義那樣,不多做解釋。

2.3 內存重新分配的方式

對C稍有經驗的人應該知道C語言有一個API叫做realloc,它做的事情是這樣的:

  1. 如果可能的話,擴張原先分配的內存的長度。
  2. 否則重新分配一塊內存,然後把舊的內存複製過去,釋放舊內存,返回新指針。
  3. 如果找不到足夠長度的連續內存,則返回NULL,不釋放舊內存。

C++自然不會少。

面試時沒有想起來,本來認爲是一種優化方案,但STL本身就算是優化方案了吧(...)。正確的解答應該是

用realloc的方式嘗試擴展buffer長度,如果無法擴展長度,則拷貝舊buffer到新buffer,再釋放舊buffer。

還行,失誤就是失誤,認錯複習一遍。

3. 比較三個容器:vector,map,list

面試官:說說看vectorlistmap有什麼不同,分別在什麼樣的上下文環境裏去使用它們吧。

我:vector可以被隨機訪問,支持隨機訪問迭代器,迭代器算法有些不適用在listmap上blabla...list通常是鏈表實現,在插入刪除的性能上有優勢blabla......

順便一提還沒說到map,面試官就換話題了。

這一題我大概又沒有 get 到面試官的 point,單談論容器的話可說的東西不少,我覺得面試官可能更想了解下我對這些容器的性能和內存方面的認知,可惜我答的有些太淺白了。

3.1 迭代器

先從迭代器的角度比較三個容器。

vector是個典型的隨機訪問容器,顯然支持forward iteratorreversible iteratorrandom access iterator。典型的實現是dynamic array

list是個線性結構容器,支持forward iteratorreversible iterator。典型的實現是鏈表。

map是個樹形容器,支持forward iteratorreversible iterator。典型的實現是紅黑樹。

3.2 內存佈局和訪問效率

討論常見實現。

vector是連續分配,訪問成本低,插入和刪除的成本高,會重分配內存。

list是不連續分配,訪問成本高,任意位置插入刪除成本相對低,插入刪除不會導致重新分配整塊內存。

map是不連續分配,插入刪除訪問成本不應和線性容器比較,畢竟它是關聯容器。插入刪除的成本都比較高,因爲需要重新平衡樹。訪問時間在標準中的要求是對數時間複雜度,插入時間懶得繼續翻標準文檔了。

3.3 使用上下文

顯而易見vector適合高頻讀,而list適合大量插入刪除,map和前面兩個迭代器都搭不上調,在需要複雜索引的地方再合適不過了。

3.4 線程安全性

這些容器都不是線程安全的。

依照標準,多線程訪問不同的容器實例一切都安好,訪問同一個實例的const方法也ok,但是非const方法就會引起數據競爭。

尤其注意迭代器的選擇,這玩意兒有時候不比指針好多少。

4. 如何管理內存資源

面試官:你在項目裏一般是怎麼管理內存的呢?

我:一個是儘可能用智能指針,然後是需要頻繁構造對象的場合下可以用placement new blabla...

內存管理是一個非常廣闊的話題,我的回答太過於淺顯了。常見的內存管理策略有很多,智能指針只能算是RAII這種常見的範式,placement new 算是內存池/對象池的一種寫法大概,還有其他很多策略我並不瞭解也未能涉及。

4.1 再論 RAII

RAII的範式可以確保異常安全,避免手賤忘記回收內存以及底層設計變更拋出的異常無法處理時導致意外的資源泄露。

諸如此類等等。

有一些約定可以關注一下。

4.1.1 獲取資源失敗拋異常

首先RAII的全寫是獲取資源即初始化,連資源都沒能獲取的話,構造理應失敗,而不是靜默給出一個無效的對象。

4.1.2 析構絕不拋異常

很好理解,如果析構又拋個異常出來的話,這個對象還析構不析構?父類還析構不析構?

4.2.3 常見設計

在STL裏除了智能指針以RAII設計以外,還有加鎖解鎖相關的內容也是:std::lock_guard

諸如此類的guard模式也在其他語言中有出現:比如說C#的using (var file = File.Open(...)) {}

4.2 內存池和對象池

內存池和對象池算是常見的設計範式,基本考慮到大量對象的構造刪除的情況都會考慮到使用這兩個模式,因爲真的很好用(

內存池的模式主要是預先分配內存,然後在這片內存上構造對象,主要的適用場景是大量頻繁構造小對象,構造成本低,生命週期短,內存分配成本居高不下的情況。當然,不僅是這裏提到的場景,根據具體業務邏輯可能還會有不同的理由去選擇內存池模式。

對象池區別於內存池的地方在於,對象池的對象構造成本要更高,頻繁構造和析構是無法接受的,這種時候就需要一個候選備用的對象池,對象池實現需要對象本身允許被複用在不同的地方,一般來說性能會比較好。內存池則沒這個顧慮:反正你需要就構造一個唄。

這兩個池都可以用factory模式來提供構造對象的服務,而工廠的消費者不需要了解對象是怎麼構造出來的。結合RAII的話,內存池、對象池裏的對象還可以用一層RAII設計的“智能指針”封裝,使其完成使命後能自動返還資源,等待下一個工廠訪客。

5. 玩過哪些遊戲,對遊戲製作流程瞭解多少?

面試官:喜歡玩遊戲嗎?都玩過哪些遊戲?

我:我的話...主要玩的是音遊,和貴公司業務可能並沒有太多關聯。

面試官:除了音樂遊戲,有玩過RPG、ARPG類型的遊戲嗎?

我:像是輻射啊,老滾啊這些...開放世界類型的遊戲遊戲性沒那麼好,比起來我更喜歡電影式的遊戲,比如說最近比較火的《底特律:變人》。

面試官:......(你丫來搗亂的是吧)

面試官:說說你對遊戲行業的看法吧。

我:遊戲行業前景好啊blablabla...娛樂崛起blabla...經濟增長blabla....

面試官:......(????)

面試官:你上一家公司也是製作遊戲的吧?就是說,你們遊戲製作啊,都有哪方面的人在負責做什麼東西,大概是怎麼個分工合作的樣子。(提醒+強調) 我:哦!哦哦,大概就是一個人負責策劃整個遊戲的玩法和系統,設計每個細節,然後程序負責去實現,自動測試blabla...內部試玩blabla...

還行,這波操作其實我也是挺佩服自己的。

5.1 陷阱:玩過哪些遊戲

我注意到一件事:在多次面試遊戲行業的職位時,都提到這這個問題:

你玩過哪些遊戲?

也許形式上有所區別:

你玩過的遊戲裏,有哪些特別喜歡的?

換位思考,如果我是面試官,我爲什麼要問這個問題?我想知道什麼?

熟悉遊戲嗎?

知道遊戲有哪些元素嗎?

能理解(我們招你進來要做的遊戲)要你做什麼嗎?

不必太過刻意地表達出對遊戲行業的崇拜或者擡高之類的,這一關主要的目的還是引出下文,聊聊對遊戲製作流程的理解。如果對面試的公司出的產品有所瞭解的話可能算是加分項。

但是,從一個遊戲玩家的角度出發,表現出不好的情緒容易留下壞映像——特別是,絕對不要明顯地表達出對國產網遊、手遊、頁遊的鄙視!!

從一個玩家的角度出發,我也不喜歡大部分國產的頁遊手遊,但是當着遊戲行業公司的面試官的面,表現出我看不起你的態度,知道什麼叫作死嗎?

更何況並不是所有國產遊戲都是屎,舉例來說我現在超喜歡 MUSE DASH 這款國產音遊的,手感比蘭空 voze、節奏大師之類的好得多,界面也沒有像節奏大師那樣糊成屎,要不是我的 Unity3D 水平太差我真想給這家 pero pero game 工作室(公司?)投個簡歷看看。

除此之外還有就是抱着拯救國產遊戲的想法或者態度,又或者勞資教你們什麼纔是真正的遊戲這樣的想法或者態度,作死無極限啊。

比較穩妥的回答方案應該是常見的幾個網遊,比如說LOL,DNF,王者榮耀,諸如此類。實際上玩過沒玩過.....咳,不被戳穿就無所謂了。

5.2 遊戲行業

加班是家常便飯,好像所有遊戲行業的公司都會這麼說。

大概瞭解下幾個術語,算是加班界的黑話吧。

一個是996。什麼意思呢?上午9點上班,晚上9點下班,一週上6天,加班費不用考慮了,不存在的,最多給調休。

再有一個是大小周。一週上6天,一週上5天,如此循環。同樣,大周加班不算加班費,給調休。

另外就是調休。如果加班一天,將來某天就可以不扣工資休息一天,直白吧。攢下半年的調休然後一口氣給自己放6個月假這種事情還是做夢比較好,調休基本上就等於無償加班了,忙起來的時候勸你別休,不然人手就不夠了;那閒下來的時候還能讓你一週休6天?你敢休公司也不敢讓你隨便休啊,其他員工怎麼看。

發薪日。網上有人總結,發薪日越接近月中的,或者超過月中的,大多都是怕員工流失的公司,而這些公司往往都不是什麼好公司。聽起來還是挺有道理的(

當然,最後還是要靠自己的眼睛去確認這一點。

5.3 遊戲的製作流程

之前待得確實是一家小公司,甚至算得上工作室級別的超小初創公司,遊戲製作方面的知識儲備不算充足,寫這篇博客的時候又去補習了一下。

主要的工種分爲策劃、美術、程序。

細分的話,策劃可能有數值方面的,世界背景人物背景方面的,對話文本方面的,甚至可能有長篇幅的資料啊故事啊這方面的需求。

美術有UI方面的,人物、場景的原畫師,3d模型製作,動畫製作,骨骼製作,特效製作,等等方面的。程序經常需要和美術方面的溝通交流。

程序的話主要分前後端和測試,再加上運維和DBA之類的角色。

細分的話前端根據開發平臺不同也有不同的技術棧,圖像特效上可能會有更專業的大牛負責,team leader帶隊設計架構,分配工作,諸如此類。後端也一樣,根據不同的技術抉擇,可能整體的人員配置也有所區別,但大家都是程序嘛。

測試算是比較獨立的,編寫測試代碼是一件很痛苦的事情(

所以這份疼痛有專人負責承受了:)

持續集成啊什麼的也被承包了,測試或者運維會去負責的。

DBA一般公司也用不到,運維多少會兩手SQL,規模更大的公司可能會設置這個專門職位。

流程上來說,策劃給出遊戲方案,美術可能會配合做個初稿效果圖之類的(更可能是策劃自己做個簡單的效果圖之類的方便說明),程序瘋狂實現(崩潰-爆發-認命 循環),測試則配合給出反饋,讓程序的脫髮狀況持續惡化,最後發佈,項目黃了。

哦不是,我是說項目火了,程序們一躍成爲CTO,迎娶白富美,走上人生巔峯。

(並沒有)

6. 尾聲

其實這次面試的自我感覺還是不錯的,沒有犯下太蠢的錯誤,但是可以改進的地方依然很多,語言組織能力需要進一步提高。

這篇博客的目的是自我反省,但是這次自我反省的效果並不算好,因爲面試官的問題基本上都戳在我懂,但又沒真正去深入挖掘的領域。日常使用自然沒有問題,但理解卻談不上了。

如果面試官在細節上稍作追究:比如說placement new和user-defined new 之類的話題上深入,異常安全,或者問個map用紅黑樹實現,紅黑樹什麼原理,那麼這次我基本又要掛了。

關於給出的待遇的問題......我其實很好奇......

因爲我真的才工作一年,不懂啊...

一年工作年限,C++我也不知道算什麼水平,不知道怎麼去橫向對比,要8k是要多了麼...

初級職位的意思是待遇初級還是能力初級啊...

還有主程一般指的是 team leader 對嗎,遊戲行業程序是不是幹到 team leader 就算到頭了...只能轉管理崗了...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章