之前在debug android的時候,有發現一開始的黑畫面特別久,特別在用Visual Studio attach debuger的時候,基本上第一畫面完全跑不出來。好奇追了一下,發現了整個流程是卡在VerifyGlobalShaders這個function上,仔細看了裡面,發現在package build的時候他完全沒做任何事,就只是把所有的shader的permutation撈出來for loop跑過一輪檢查而已,這個foreach的執行次數在4.27有8萬多個(85610),看起來非常浪費計算量,就算是跳過也不會影響後面的功能,說不定跳過之後能夠省個幾秒鐘的黑畫面時間。試了一下,這個想法是對的,跳過之後確實後面的畫面也能夠正常顯示出來,讓我的debugger能夠順利的往後執行。
但這時候我就好奇了,為什麼引擎要放一個明顯無用的ForLoop在那裡?這段code通常只會在Development build執行吧?為什麼連Shipping build也要跑?把他跳過之後我們能夠省下多少時間?
為了驗證這個更動能夠帶來更好的遊戲體驗,因此我決定計時看看跳過VerifyGlobalShaders 跟原本不跳過的版本差異在哪。使用的版本是Test並啟用了PGO優化。
但是,結果讓我非常的震驚,數據如下。
機型:Galaxy S21 5G SM-G9910
Skip | NoSkip | |
第一次開啟 | 12.52秒 | 7.9秒 |
第二次開啟 | 1.87秒 | 1.66秒 |
機型:Galaxy S21 ultra 5G
Skip | NoSkip | |
第一次開啟 | 10.28秒 | 7.98秒 |
第二次開啟 | 2.13秒 | 1.71秒 |
這個驚人反直覺的結果,讓我開始思考到底發生了什麼事。為什麼跳過無用的Loop,其執行結果反而比沒跳過的慢?這之間是不是有什麼硬體上的預熱效果在裡頭發揮了作用?思考到這裡,我突然想到,所有的CPU都有所謂的L1、L2 Cache,說不定就是藉由VerifyGlobalShaders的機制,把背景另外一條thread正在運行編譯建立PSO shader所需的相關的內容搬進了L1/L2 Cache中,因此後續的編譯就不需要再去另外花時間去跟記憶體取回需要的內容?
注:關於PSO Cache的建立流程可以參考官方文件 https://docs.unrealengine.com/4.27/en-US/SharingAndReleasing/PSOCaching/
接下來我拿了另一隻比較差的android手機來測試,基本上驗證了我這個想法:
機型:HTC_U-3u
Skip | NoSkip | |
第一次開啟 | 28.72秒 | 28.29秒 |
第二次開啟 | 3.93秒 | 3.88秒 |
機型:HTC_U-3u 重安裝重開機後跑第二次
Skip | NoSkip | |
第一次開啟 | 28.84秒 | 26.89秒 |
第二次開啟 | 3.64秒 | 3.64秒 |
查了一下,這隻Android手機使用的是Snapdragon 835,沒有L1 Cache,而L2 Cache是大核2MB/小核1MB,其跳過VerifyGlobalShaders跟沒跳過後的速度基本沒差(或是說差距縮小);而S21使用的是snapdragon 888,擁有8個大小等級不一的核心。Cache的大小跟等級確實跟執行的速度有差別。
不知道原本寫這段code的人有沒有意識到可以靠L1/L2 Cache來做「預熱」的機制,如果是有意而為之,那這腦袋的思考方式已經是別種境界。在看到這個實驗數據之前,我可能一輩子都不會想到還有這種操作手法:怎麼會想到在Shipping build放一個接近10萬次的For Loop迴圈單純只是做檢查?通常是用macro把他夾掉後限定在Debug或Development Build了吧?
以結果論來說,這個推論看起來是對的,但我也不敢百分之百肯定是否有別的我沒有想到的機制在背後運行,不過可以確定的是……epic裡寫這段code的大大你好歹也寫個註解解釋一下,我差一點就做了負優化。
==============================================
廣告時間
UnrealEngine Taiwan Discord非官方社群https://discord.gg/FanK6yc 大家一起來討論Unreal吧!
我的Unreal Marketplace: https://www.unrealengine.com/marketplace/en-US/profile/horizon-studio 歡迎選購
Leave a Reply