[演講筆記][UnrealEngine] HPG 2022 Keynote: The Journey to Nanite

posted in: UnrealEngine | 0

HPG 2022 Keynote: The Journey to Nanite – Brian Karis, Epic Games transcription of the talk: https://www.highperformancegraphics.org/slides22/Journey_to_Nanite.pdf 整個演講跟下來,發現這場演講比較偏向文獻回顧,講者提到了很多創造nanite過程中試過的方法以及為何最後捨棄該方法的原因。 有很多名詞跟技術對我來說都是第一次聽到,講者也不會特別花時間講解那些名詞到底是什麼東西,也因此花了蠻多時間試圖搞懂作者到底想表達什麼東西;作者沒多解釋的部份,也有試著去找文獻理解並寫了一些註解,寫著寫著沒想到總字數居然來到了2萬7千個字。 大部份內容都是經過我自己消化過後再試著重新用文字表達,很多地方或多或少都會混入自己的看法在裡面,只是由於能力有限,可能理解有誤或偏離原義的部份還請見諒。 借由這次的分享,也希望自己能夠好好的將一些沒搞懂的觀念補齊,也希望能夠拋磚引玉,希望能引出專家出來幫忙指正或補足內容不足的地方。 另外我也有做pdf的版本,比起網頁應該會比較好閱讀,需要的可以從這邊取得 那麼,就讓我們開始這次的旅程吧。 本文開始: Introduction 講者一開始先展示了使UE5中使用Nanite跟Lumen讀入Moana island dataset(*1),完整處理這組資料聽起來是這個領域中的惡夢,裡面的三角面數量非常的巨大,細到每細沙灘上的沙是都有三角面。 *1: Moana island dataset 查了一下資料來源,disney有放出很多這類的資料集供研究跟軟體開發使用,看他license重點是不能商用。This dataset has become notorious for being massive … Continued

[演講筆記][UnrealEngine] UE5 Deferred Technology Pipeline for Mobile Games

posted in: 未分類 | 0

花了一些時間把EpicGameChina關於Mobile Deferred Render的演講看完並做了一些筆記,分享給大家,也推薦大家去看影片,我是覺得收獲蠻多的。 影片在此:UE5 Deferred Technology Pipeline for Mobile Games 1. Mobile Deferred Render直到5.1才算比較成熟 2. Deferred的優勢在於可以減少shader permutation,因為在base pass不用算lighting,因此相關的permutation就可以從model material直接decouple出來,省下大量的shader數量。 3. mobile deferred vs mobile forward在關掉static lighting的比較:         a. Mobile Deferred只需要建出LMP_NO_LIGHTMAP一種caseb. Mobile Forward則需要建出LMP_NO_LIGHTMAP跟LMP_MOBILE_DIRECTIONAL_LIGHT_CSM搭上bEnableLocalLights開關,總共有四種case。         c. … Continued

[開發日記][UnrealEngine] 關於C++中的volatile變數的一些記錄

posted in: UnrealEngine, 開發日誌 | 0

大家應該或多或少在engine中都會看到volatile這個關鍵字,通常這個詞都是跟某個系統上的cache機制有緊密的關係,被宣告成volatile,代表的是他是隨時會被各種不受控制的外部系統改變,因此系統不會對該資料進行cache的動作。 在C++中,對某個變數i宣告volatile這個關鍵字又代表著什麼意思呢?對cpu而言,他對於變數的cache機制不難想像,就是他身上的暫存器 (Register)。 基本上不管編譯時期的compiler或運行時期的cpu,他們會盡一切手段去優化執行的效率,例如把變數變成常數,reorder指令的順序或者是把某個值i從記憶體中讀進cpu的register中,然後接下來的指令就直接從該register位置中拿值而不再去從記憶體中要。雖然這些優化手段在使用上都會是保守的手段,理論上會被保存在register中的數值,對於接下來運行的指令而言應該是可信的,例如: int i=10; int a = i; int b = i;  i=10在這三條指令的範圍內應該要是是可信的。 但還是有些例外,例如該值i是被其他外部程序直接更改記憶體中的值的時候呢(*1)?比如a先assign了10之後,i指馬上被外部程式改成了11,這時候由於cpu register沒機會去刷新,因此b還會維持10的結果,但理論上應該要是11才對。這時候volatile這個關鍵字就派上用場了,前面也提到,被宣告成volatile,我們的系統就不會去cache他,因此我們每次都重新去從記憶體中拿值而不是從register中。 *1:這邊通常是硬體搭配Memory mapping I/O使用。利用系統保留的記憶體區間對應一個裝置上的幾個register數值,在程式中操作記憶體等同於操作該裝置。 不過在multithreading的使用情境中,volatile並不等同於thread-safe。雖然compile time時的優化被volatile指令阻止了,包括禁止cache跟reorder指令。然而,雖然你的指令順序有乖乖照你希望的被編譯出來,但cpu可沒那麼聽話,他run time的reorder還是會照常運行。 因此要thread-safe我們還是要搭配有同步(synchronization)機制的正規手段才行,例如atomic、mutex或memory fence,換句話說,在multi-thread的環境下,指令的執行順序很重要,因此上面這幾個工具就是設計來確保程式能夠叫compiler跟cpu都能夠乖乖照我的邏輯執行,不要亂做奇怪的優化並籍此防止race condition。 另外volatile的實際行為,要不要reorder以及怎麼reorder也硬體自身的實作有關,因此太依賴這個東西的話可能會在跨平台的應用上踩到某些雷。 不過在unreal的搜原始碼中,我們會看到有一些multi-thread用的變數除了有用Atomic相關工具之外,還是被加上了volatile,就我的認知中,他最好只在使用MMIO跟硬體存取搭配時使用。關於這點我思考了很久,會不會是因為歷史因素照成的?或是某些平台,單純使用上面提到的幾個同步工具時不可靠?這點我還沒有答案,也有可能是我的認知還不夠完善,有人知道原因的話也歡迎提出來討論。

[開發日記][UnrealEngine] MemoryMapped File

posted in: UnrealEngine, 開發日誌 | 0

前幾天在看資料看到MemoryMapped File這個東西,好奇unreal目前對於MemoryMapped File支援度如何,因此花了一些時間看了一下並做一些實驗。由於iOS上可用的記憶體實在太少,想說如果沒開的話可以試著把他開起來。 簡單來說,這東西能夠讓我們直接把硬碟上的檔案直接map到我們的virtual memory中,直接用記憶體相關操作做檔案IO,這不僅可以少掉記憶體壓力,也能夠少掉把檔案讀寫進記憶體時間。由於他佔用的是virtual memory的空間,因此在64位元的系統,他的上限可以對應到2^64次方,就是8EB;相對於32位元的系統只有4GB。雖然4.27在Android上還支援使用32位元的armv7,但UE5就只剩下arm64了。 MemoryMapped File有console command可以開關:mmio.enable。 mmio這個名詞用在這裡其實還蠻讓人困惑的,他跟MemoryMapped File的概念不太一樣。 更進一步的把mmio這個關鍵字餵進去搜尋,會找到2個地方有出現這個名詞: MMIO:Memory mapping I/O,主要是用在跟外部硬體互動的機制,很久以前會需要發特殊的硬體指令,但現在(不確定這個現代是從什麼時候)喜歡透過正規的記憶體操作來跟硬體互動,現在的硬體大多支援這個機制。作業系統會保留一塊區間給外部硬體,一個記憶體位置對應一個硬體裝置,在程式中操作記憶體等同於操作裝置。細節我也沒玩過,google到的使用方法是做一個struct,然後針對某個係保留的記憶體位置強制做轉型,之後就可以在程式中對該物件做操作。1. data-in register2. data-out register3. status register4. control register例如手把controller的區間是200–20F(512-527) 結論來說,目前引擎只有iOS有開啟支援,且4.27限定在CompressedAnimation跟UnrealAudio這二個asset上、5.0則只支援UnrealAudio。看來UnrealEngine對於iOS這種記憶體有限的裝置,還是有想一些應對的方法來減緩記憶體的壓力。沒有全部asset都開的原因,可能是因為flash memory有寫入次數的問題。雖然目前我還不確定為什麼5.0要把CompressedAnimation關掉,可能跟上面的原因一樣。 有試著開其他type的asset,結果馬上就遇到了檔案alignment的check error;測試開了windows,也會同樣的error也會發生。註解掉error是能夠繼續運行,但就沒深入去研究有什麼副作用(side effect)就是。 各別Asset的開關需要去調整FGenericPlatformProperties中的SupportsMemoryMappedFiles、SupportsMemoryMappedAudio以及SupportsMemoryMappedAnimation。要針對各平台的話,例如IOS,就是在FIOSPlatformProperties中實作這三個function並回傳true。 要注意的是,在引擎中的用法是先檢查SupportsMemoryMappedFiles是不是true,再根據各別的Asset去看要不要開起來,也就是說二個條件都要符合,例如Animation的檢查如下: 註:想強制把功能開起來,需調整以下東西1. FBulkDataBase::Serialize把bAttemptFileMapping強制設成true,2. DefaultEngine.ini中的[MemoryMappedFiles]MasterEnable=trueAlignment=16384

[演講筆記][UnrealEngine] UE5中的渲染技術 2022台北遊戲開發者論壇

posted in: UnrealEngine, 開發日誌 | 0

花了一些時間把EpicChina在2022台北遊戲開發者論壇的演講看完並做了一些筆記。 題目是關於UE5中的渲染技術。 只是看完影片後覺得資訊量太多,腦袋呈現完全爆炸狀態……然後影片結尾聽到講者說這只是很粗淺的介紹後我就崩潰了。 雖然以下筆記很多內容我都還有點消化不良而且可能也有理解錯誤的地方,但不管怎麼樣先分享出來給需要的人。 想更進一步知道細節的推薦大家去看演講,不能只有我崩潰。 https://www.twitch.tv/videos/1533064361 1:54:37秒 開始 1. 在nanite中大小三角形會分別用軟硬體做,大的用硬體做,小的為了避開overshading的問題,他自己用compute shader寫了rasterization。為什麼呢?在三角型在算pixel的顏色時,由於頂點不會剛好在pixel的中間,因此他必須相鄰的4個pixel拉進來,用一個Quad為單位來考慮。當你的三角型很小而且緊密時,某些pixel會一直反覆被拉進去做計算,這情況稱之為Overshading或者是Quad OverDraw。而Nanite為了解決這個問題,因此大的三角型雖然還是靠硬體做,但小的三角型則是自己用軟體做,在Compute Shader中寫rasterization。小的三角型有一些條件需要達成才會被歸類為小三角型。 2. 整套技術的重點在Cluster生成、Culling跟LOD的選擇,為了高效的達成Culling跟LOD的選擇,引擎從4.22開始就把整個render底層換成retained mode,就是說讓GPU去維護整個場景render object的一些狀態。 這邊是GDC2019演講。 3. LOD的生成機制,其實就是先做graph partition演算法做cluster分組,然後不斷的遞迴減半面數,最終生成一個DAG(Directed Acyclic Graph)。Graph partition的分群的給定條件是共享的邊盡可能的少,面積盡可能的均勻。這出來的結果是每下一級LOD都會是最小的變化,也就是說外觀的變化最小。而衡量外觀變化的標準就是使用QEM(Quadric error metric)。 4. QEM這個衡量標準可以保證我們減面後的error會越來越大,從DAG的Root到Leaf Node,記錄在節點中的error值可以幫助我們做LOD的選擇。 5. 每個Cluster的BoundingBox都會再各別對每級的LOD個別生成BVH(BoudingVolumeHierarchy)後,再掛到一個大的BVH root下面,因為這樣gpu的入口會比較一致。每4個Cluster會組成一個cluster group,最終這個nanite模型會是一個cluster的BVH結構。 6. Culling會利用分群後的資訊進行,經過以下步驟: — a. Instance Culling:用上一個frame的HZB做跟BoundingBox來做剔除,得到一個粗略的可能還可見對象的instance。— b. … Continued

[開發日誌][UnrealEngine] 關於在手機上使用MallocBinned的一些研究記錄

posted in: 開發日誌 | 0

在Unreal中MallocBinned總共有3個版本:MallocBinned、MallocBinne2、MallocBinned3。這類的memory pool機制,主要的設計原則就是希望盡量的將大小相同的物件放在一塊連續的記憶體上,然後根據物件的大小,一個蘿蔔一個坑,在分配的時候再從對應的Pool中拿出空間出來使用。通常我們只會對小物件做這件事,過大的記憶體物件需求通常會直接呼OS的allocator。 例如FMallocBinned所定義的小物件是以下幾個size:16, 32, 48, 64…到32768,也就是說至多到32KB。 看了一下android的memory pool機制,預設arm64是用MallocBinned2跑,而要不要換到MallocBinned3取決於各別專案會不會用爆目標最低機型的記憶體,會的話,由於MallocBinned3有Virtual Memory,因此可以突破記憶體上限。 另外IOS預設是使用MallocBinned,因為只有他有實作Apple系統內建的的nano allocator。系統的nano allocator預設會保留512MB的pool給small object用,看起來有點像是MallocBinned2在做的事;相對於MallocBinned的Pool,它是在該size被使用到的時候再去跟OS要一大塊記憶體回來。 關於nano allocator,詳細可以看Engine\Source\Runtime\Core\Private\Apple\ApplePlatformMemory.cpp中NanoMallocInit 的註解。以下是Copy出來的註解: 可以想見能在OS層級做掉的機制,當然會比FMallocBinned2引擎自己實作的還要高效,因此也不難理解iOS會選擇使用FMallocBinned來搭配。 IOS要換到MallocBinned3是不可能的,因為根本就沒有實作這條路,我有試著硬開出來,但會直接crash,微稍深入研究了一下Apple官方文檔,裡面有提到,雖然Mac相關VirtualMemory的功能都是正常的,但是我們只能page-in但不能page-out到硬碟上,不排除未來官方未來會在iOS上支援swap memory,可以觀察每年WWDC看看有沒有提到相關的議題,但現況就是iOS不支援MallocBinned3。另外有發現蘋果在發表iPadOS16的時候,有提到支援Virtual memory swap這個feature,這表示現況我們是能在iPad上使用MacllocBinned3的(待測)。 Apple官方文檔 iPadOS16 Feature介紹 只是為什麼iOS不支援MallocBinned3而Android支援? 我想是為了延長手機的生命週期吧。 由於MallocBinned3是依賴於VirtualMemory的技術,他會需要大量進行PageOut/PageIn的動作,但由於手機系統大多是使用FlashMemory,其在設計上有寫入次數的限制,寫入次數越多,其跟記憶體之間的資料交換會越不穩定。 因此iOS只支援PageIn而不支援PageOut的根本原因在此,為了維持系統的穩定度,減少硬碟的寫入次數,因此Apple禁止了PageOut這條路;由於不支援PageOut,因此我們就沒辦法使用MallocBinned3。 這個限制其實在Android系統上也是存在,只是該限制只存在於boot disk上,其他另外的secondary storage,例如SD card則沒有這種限制,所以Android還是可以正常的使用MallocBinned3。

[UnrealEngine][開發日誌] CommonUI plugin 筆記與 Plugin ( HorizonFramework ) 的設計思路

posted in: UnrealEngine, 開發日誌 | 0

關於Plugin的官方介紹: https://docs.unrealengine.com/5.0/en-US/common-ui-plugin-for-advanced-user-interfaces-in-unreal-engine/ 本篇文章主要是記錄目前我在CommonUI plugin中所看到的一些東西,可能分析的不是很完整,但就是一個簡單的筆記。 版本:Unreal 5.0.3 目前plugin裡面主要有以下幾個功能: 各別平台的Input以及 Icon Mapping:例如xbox跟switch的A跟B是相反的,他有機制處理輸入跟icon的對應。 Styling: 例如CommonText可以做一個Style BP給大家用,之後調整遊戲文字就只需要調該style BP就行了 從GameViewportClient上做Input Rereroute,根據目前是ECommonInputMode::Menu、Game或All決定裡面的行為。在功能面上看起來跟InputMode在機制上類似,Menu對應UIOnly、Game對應GameOnly而All對應GameAndUI,不確定之後官方會想怎麼發展。 UI Manager的機制。 我自己看是覺得整套機制還不是很成熟,在使用上你會覺得缺少很多細節的實作。不過也對,他現在是放在experimental plugin下面,要使用在production上要有心理準備自己需要去補完很多缺少的實作。 關於UI Manager,他實現的機制比較特別,我自己的Plugin是用Actor,但他是拿你第一個AddToViewport的主Widget來當管理器,這時候你會需要在這個Widget上再刻一套需要的功能介面好讓其他系統呼叫。 整套機制主要分為2個元素構成: Widget Container容器:分為UCommonActivatableWidgetStack跟UCommonActivatableWidgetQueue,分別可以進行push跟pop操作,這二個容器的差別在於我們的設計是先進先出或後進先出。我們可以有多個Stack分別對應不同的需求,在Lyra中他是把Game跟Popup Modal拆成二個stack ActivatableWidget:他是一個UserWidget,我們需要繼承下來進行設計的各種UI頁面,用來push進Widget Container的元素。。 他Push跟Pop時內建transition動畫,但他動畫效果是寫死在code中,而不是去呼叫UMG上的動畫,所以不是很符合實際產品製作的需求。內建的Transition動畫效果有:FadeOnly、Horizontal、Vertical、Zoom (ECommonSwitcherTransition)。 由於我以前有解決過UI Manager相關的問題,因此就再來多來聊聊我Plugin的設計思路吧。 以下是我Plugin的連結: Horizon Framework 這個Plugin當初設計的目的是想要做一套通用的遊戲框架(GameFramework),並做為HorizonDialogue擴建的基礎。只是做到後來發現通用框架難以在一個Plugin裡面實現出來,所以目前就變成了專注在UIManager相關的功能上面。 我的UI管理器做叫HorizonSceneManager,而用來進行Push跟Pop的元素叫做HorizonScene,Scene這個名稱是借鏡於cocos2d,他們都繼承自Actor,因此都會在World上生成一份實體。HorizonScene是用來封裝UserWidget,因此我們在設計完UserWidget之後,需要另外創建一份BP繼承自HorizonScene,並把UserWidget的Class指派進Scene的參數中。如下圖: SceneManager裡面有一份SceneStack跟SceneEventList,裡面有設計一系統的API讓我們可以針對SceneStack進行操作,幾個重要的API為:ChangeScene、PushScene、PopScene、RemoveScene。這幾個Function會建立對應的SceneEvent放進SceneEventList中,SceneManager會在每個Tick的時候逐一拿出來執行,並在適當的時機點通知SceneManager,並把Scene加入進SceneStack中、呼叫對應的delegate通知使用者實作需要的遊戲邏輯。 每個Scene主要會經過以下幾個生命週期: Enter:視是不是處於VR模式決定初始化Widget的方式,並呼叫StartTransIn。 … Continued

[UnrealEngine][開發日誌] 關於打包最後會出現的Waiting for child processes to complete訊息

posted in: UnrealEngine, 開發日誌 | 0

分享一下之前專案遇到的一個打包速度優化手段。 之前專案在打包的時候,最後一定出現Waiting for child processes to complete(0/1)這個訊息,當專案越變越大之後,很明顯的卡在這一步的時間變長了。追了一下code,發現他是在做把東西打包進pak的動作,每一個pak只會開一條thread處理,而訊息中的(0/1),則表示了我們把所有的內容塞進了同一個pak中(completed/numPak);也就是說若我們沒有啟用Chunk機制的話 (見:https://docs.unrealengine.com/4.27/en-US/SharingAndReleasing/Patching/GeneralPatching/CookingAndChunking/ ),這邊會浪費不少時間在做單核cpu的等待,之前試過,隨便分個8個chunk,就讓整體打包時間加快大約20分鐘左右。 通常會關注到chunk議題,很多時候是因為在做手遊時需要去分第一包跟做patch;但其實分chunk之後帶來的好處還蠻多的,除了打包速度加快之外,我們還可以利用UAssetManager來做Preload相關的機制。 關於打包的細節有興趣可以去看UnrealEngine\Engine\Source\Programs\AutomationTool\Scripts\CopyBuildToStagingDirectory.Automation.cs中的RunUnrealPakInParallel這個function。

[UnrealEngine][開發日誌] Shadre pipeline:Usage Flags vs ShadingModel

posted in: UnrealEngine, 開發日誌 | 1

UsageFlag文檔:https://docs.unrealengine.com/4.27/en-US/RenderingAndGraphics/Materials/MaterialProperties/ ShadingModel文檔: https://docs.unrealengine.com/4.27/en-US/RenderingAndGraphics/Materials/MaterialProperties/LightingModels/ 本文章是基於4.27 之前有空想要比對一下mobile pass跟SM5 pass到底差在哪裡。有發現在windows editor中,只有SM5跟ES3二種,就算是選了蘋果icon也不是真的metal,真正的metal shader code只能在mac環境下編譯出來。 在追code的過程中相關些名詞一直出現,引起了我的好奇,因此決定仔細研究一下。想要追shader相關的程式碼,這幾個ini參數最好設一下會比較好debug: 在material上有usage跟shading model的選項,usage主要的用途是用來決定哪些shader組合是不是要compile permutation;ShadingModel主要是用來決定計算光源貢獻時要走哪條IntegrateBxDF的路。 在Unreal中shader code的generation主要是靠C++ Template的機制來大量產生各種組合(permutation),其中可以看到引擎大量的使用Policy-Based Design。我們知道template的機制是compile time產code,所以我們只要有n個base shader,再加上m種policy的話,我們就可以產生n*m種組合。 每個Policy都會實作ShouldCompilePermutation跟ModifyCompilationEnvironment這二個function;前者搭配Usage使用,替BaseShaderType決定要搭配哪些Policy組合的編譯,例如,NoLightMapPolicy,接著實作會呼叫ModifyCompilationEnvironment,他會替Shader加macro,決定某段shader code要不要留下來。 以bIsUsedWithStaticLighting為例,若該usage有勾選的話,在跑IMPLEMENT_SHADER_TYPE時,其相關連到一組的BasePassVertexShader.usf、BasePassTessellationShaders.usf以及BasePassPixelShader.usf(Vertex Shader, Hull and Domain Shader, Pixel Shader)就會被用搭配到的TLightMapPolicy建立出來。 例如在${PROJECT_ROOT}/Saved/ShaderDebugInfo/PCD3D_SM5/AmbientOcclusion/FLocalVertexFactory可以看到類似於下面中的資料夾。其中產生的permutation shader code,針對BasePassVertexShader.usf,產生NoLightMapPolicy以及NoLightMapPolicyAtmosphericFog這二組;針對BasePassPixelShader.usf,則產生了FNoLightMapPolicy跟FNoLightMapPolicySkylight。 這些Policy主要的功能是用來ModifyCompilationEnvironment,替shader code加入需要的macro define,之後每個permutation就會利用mcpp這個preprocess來幫我們濾掉不需要code,並把所有include到的shader結合成一個大檔之後,產生特製的shader code出來。 另外Usage除了決定permutation要不要產生出來外,有一些還會用來附加一些macro進shader中,例如OutEnvironment.SetDefine(TEXT(“USES_DISTORTION”), Material->IsDistorted()); 有發現Usage雖然有八成左右的功能都只是用來決定編譯組合;ShadingModel主要是用來決定計算光源貢獻的做法。但有看到有少部份會直接去加shader … Continued

[UnrealEngine][開發日誌] Shadre pipeline optimization: 關於VerifyGlobalShaders的實驗結果令我非常震驚

posted in: UnrealEngine, 開發日誌 | 0

之前在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 … Continued