iT邦幫忙

2025 iThome 鐵人賽

DAY 5
0
Software Development

渲染與GPU編程系列 第 5

Day 4|GPU 記憶體架構與資料流:如何有效管理資源

  • 分享至 

  • xImage
  •  

今天要把「資料怎麼從你的程式一路跑到顯卡裡又跑回螢幕」講清楚。
記住三個關鍵字:容量(能放多少)頻寬(一次能搬多少)延遲(來回要多久)
GPU 的世界在乎「吞吐量」勝過「單次反應速度」,所以我們會用正確的地方存正確的資料,讓每一趟搬運都值得。


0. 先用生活比喻建立直覺

把整台電腦想成一家超商物流:

  • 硬碟 / 網路:遠端倉庫,容量超大但速度慢(今天不談)。
  • 主記憶體(RAM):超商後倉,容量中等、速度普通。
  • 顯示卡的顯存(VRAM:GDDR6/HBM):在門市前台旁的小冷藏櫃,專供前台(GPU)使用,取用超快
  • L2 / L1 / Shared Memory / 暫存器:前台員工手邊的小料盒,超短距離、超快但超小
  • PCIe 走道:後倉 ↔ 前台之間的走廊,一次能搬的量有限(頻寬有限)。

你要做的,就是把最常用、要快速反覆取的貨(貼圖、網格、常用緩衝)提前放到顯存;而前台員工(SM)運作時,盡量用到「手邊料盒」(暫存器 / Shared Memory / 快取),把一次搬很多、少走回頭路當成原則。


1. GPU 的記憶體階層是長這樣(從大到小、從慢到快)

主記憶體 RAM(CPU 在這)  ←→  PCIe/NVLink  ←→  顯存 VRAM(GDDR/HBM)
                                              │
                                              └─ L2 Cache(全卡共享)
                                              ├─ L1/Texture/Constant Cache(每個 SM 鄰近)
                                              ├─ Shared Memory(每個 SM 的「共用小倉庫」)
                                                     └─ Registers(每條執行緒的「口袋」)
  • VRAM(顯存):放模型、貼圖、各種 GPU 緩衝;頻寬高,可大量同時讀寫。
  • L2 Cache:全 GPU 共用的快取,減少回 VRAM 的次數。
  • L1 / Texture Cache / Constant Cache:靠近運算核心,對貼圖取樣與常數存取很友善。
  • Shared Memory(LDS):同一個 SM 內的工作群組共享的小空間,你可以手動安排資料暫存以省頻寬
  • Registers:每個執行緒的暫存變數,最快,但用太多會壓低並行度(occupancy)。

口訣:越靠近運算單元越快,但越小。寫 Shader/Compute 時,要想辦法讓資料在近的地方被重複使用,不要老是回大倉庫拿。

Memory hir

2. 「頻寬 vs 延遲」是兩種不同的速度

  • 延遲(Latency):一次取一口要多久。
  • 頻寬(Bandwidth):一分鐘能搬幾大箱。
    GPU 追求的是整體吞吐量(一秒內處理多少像素/資料),所以它用大量平行來「把等待時間藏起來」。
    但前提是:你的資料要能被整齊、成批地取。這就帶出兩個關鍵:局部性對齊/併讀(coalescing)

3. 兩個一輩子都受用的原則:局部性 & 併讀

3.1 局部性(Locality)

  • 空間局部性:附近的像素很可能用到附近的貼圖位置 → 開啟 Mipmaps、別亂跳坐標。
  • 時間局部性:同樣的資料一小段時間內會重複用 → 放在 Shared Memory 或盡量留在快取。

3.2 併讀(Coalesced Access)

  • 同一個 Warp/Wavefront 的執行緒,最好一起讀連續地址
  • 結構設計上,常把 struct of arrays(SoA) 取代 array of structs(AoS),讓每個欄位連續存放,方便一次併讀。

小示意

好(SoA):pos_x[] pos_y[] pos_z[]   ← 32 條線程可一次讀一整段
壞(AoS):struct {x,y,z} 重複      ← 每條線程跨很大步去抓 x

4. 圖形程式中的常見資料類型,要放哪裡?

類型 放哪裡 為什麼
網格(頂點/索引) VRAM 的 Buffer 會被反覆讀,放顯存快又穩
貼圖(顏色/法線/粗糙度) VRAM 的 Texture 有 Texture Cache 與硬體取樣器幫你加速
Uniform/Push 常數 小緩衝或 Push 常數 每幀/每 Draw 改變的少量參數
大量可讀寫的資料(骨骼、粒子、G-Buffer) Storage Buffer / Render Targets 容量大、頻寬夠
轉場上傳用 Staging/Upload Buffer(CPU 可見) 先放這,再一次搬到 VRAM 減少 PCIe 往返

原則:能常駐就常駐到 VRAM。上傳用 staging buffer 以批次拷貝,避免每幀碎片小拷貝。


5. 「從你的程式到畫面」的資料流(一步步看)

步驟 1:CPU 準備
載入模型/貼圖 → 建立 Buffer/Texture → 把資料批次拷到顯存(使用 staging buffer/映射)。

步驟 2:綁定與下命令
把要用的資源(Buffer/Texture/Sampler)綁到 Descriptor/BindGroup → 錄製命令(Command Buffer)。

步驟 3:GPU 取資料並運算
VS 讀頂點 → Raster 切片 → FS 取貼圖/算顏色;中間快取會幫你擋掉不少回 VRAM 的次數

步驟 4:寫回顏色/深度附件
寫入 Render Target(Color/Depth/Stencil);ROP/混色會盡量用硬體壓縮(例如顏色/深度壓縮),省頻寬

步驟 5:顯示
把最後的影像交給顯示系統(表面交換、垂直同步等),進入下一幀。

ASCII 示意

CPU資料 → [Staging] → VRAM(Buffer/Texture)
             │                  │
             └─Command/Bind─────┘
                   ↓
            VS → Raster → FS → ROP
                   ↓
               Framebuffer → 螢幕

6. 省頻寬的「神隊友」:貼圖壓縮與 Mipmaps

  • 壓縮格式(BC/ASTC/ETC 等):在 VRAM 就是壓縮狀態,取樣時硬體直接解碼部分像素,節省空間與頻寬。
  • Mipmaps:同一張貼圖存不同尺寸版本,遠處用小張 → 減少取樣跳動與頻寬
  • 過濾(Filtering)LINEARNEAREST 平滑;可搭配 anisotropic 提升斜面清晰度(但會更吃頻寬)。

新手務必:能壓縮就壓縮、能做 Mip 就做。這是最划算的優化。


7. 行動裝置的小差異:為什麼它更在意「帶寬」?

多數行動 GPU 用 Tile-Based Deferred Rendering(TBDR)
把畫面切成很多小瓷磚(tiles),在**片上(on-chip)**就完成深度測試與混色,最後一次性寫回 VRAM
這麼做能減少外部記憶體讀寫(省電又省帶寬)。
你的策略:減少 overdraw(重複畫同一像素)、善用 Early-Z、避免大量半透明疊加。


8. 同步與併發:避免「你在搬,我在等」

  • 雙/三重緩衝:讓 CPU 與 GPU 像接力賽,同步被攤平。
  • Persistent Mapping / Ring Buffer:把一塊 CPU 可見的上傳區持續映射,像滾動膠帶一樣往前貼新資料。
  • Barrier / Memory Scope:正確宣告「誰寫完,誰才能讀」,避免資料破圖或讀到舊值。
  • 避免 CPU↔GPU 小碎拷:小量改動集中起來一次搬,別逼 PCIe 當蚊香

9. Compute Shader 的記憶體心法(也適用很多 FS 場景)

  1. Global Memory 併讀:讓同一個工作群組的線程讀連續地址
  2. Shared Memory 分塊(tiling):先把一小塊資料搬到 shared,再在小倉庫裡反覆利用(例如卷積、模糊)。
  3. 避免 bank conflict:Shared Memory 有分「車道」,佈局要避免大家搶同一條。
  4. 減少分支發散:盡量用遮罩/插值/查表,不要在群組裡一半 if、一半 else。
  5. 暫存器壓力:變數太多會降低同時上線的工作數(occupancy);小心大型結構與過度 inline

10. 格式、對齊與 Pitch:小地方決定大效能

  • 對齊(Alignment):很多硬體要求 16 bytes 對齊(矩陣、向量);按規則排可減少隱形補齊(padding)。
  • Pitch(Row Stride):2D 資料每行可能有對齊補位,拷貝時要用正確的 pitch,不要以為是「寬×像素大小」就好。
  • 格式選擇:能用 R8G8B8A8 就別上 R32G32B32A32;法線可用兩通道壓縮(或 BC5/ASTC),深度選擇 D24/D32 看需求。
  • 半精度(FP16)/ 10-bit:在允許的範圍內用較小格式,省空間、加速讀寫。

11. 常見踩雷清單(和修法)

  1. 每幀大量小拷貝到 GPU

    • 症狀:CPU/GPU 同步卡住、PCIe 佔用高。
    • 修法:用 staging 合批、ring buffer 一次寫一段、把靜態資源一次上傳常駐。
  2. 貼圖沒壓縮、沒 Mip

    • 症狀:遠處閃爍、頻寬爆炸。
    • 修法:用 BC/ASTC 等格式 + 自動或離線產生 Mipmaps。
  3. FS 亂跳採樣

    • 症狀:Cache 命中低、取樣成本高。
    • 修法:UV 規畫連續、避免大步長跳躍;必要時先做重採樣到貼近使用方向的圖集。
  4. 大量半透明疊加

    • 症狀:Early-Z 失效、頻寬與 FS 負擔飆升。
    • 修法:排序合批、壓縮圖層數、嘗試加權混合 OIT、或改美術表現。
  5. Compute/FS 暫存器爆掉

    • 症狀:occupancy 下降、效能反而更差。
    • 修法:拆函式、減少大型 struct、觀察編譯器統計(暫存器使用量)。
  6. 畫面顛倒 / 顏色偏差

    • 症狀:Y 軸上下顛倒、Gamma 不一致。
    • 修法:檢查投影矩陣與座標系;確保 sRGB 流程一致(貼圖讀 sRGB、輸出正確 Gamma)。

12. 五分鐘「體感」實驗(不用工具也能看出差別)

  1. 開關 Mipmaps:把同一個場景切換「有 Mip / 無 Mip」,看遠處紋理的穩定與 FPS。
  2. 解析度 4K ↔ 1080p:如果 FPS 明顯跳升,代表像素階段與頻寬是主因。
  3. 把 4 張 2K 貼圖換成 1 張圖集:觀察載入時間與執行時的 stutter 是否變少。
  4. Compute:Shared Memory 版本 vs 直讀版本:做個 3×3 模糊,比較時間差,感受「分塊暫存」的威力。
  5. 把頂點格式精簡:例如位置保留 float3,顏色改 UNORM8x4,觀察載入與繪製成本。

13. 實務上的「放哪裡」小抄(可直接照抄)

  • 靜態網格/貼圖:一次上傳到 VRAM,長駐。
  • 每幀變一點點的常數Uniform/Push;量大改用 Storage
  • 超大資料(粒子、骨骼、G-Buffer、貼圖輸出)Storage/Render Targets
  • 上傳用Staging/Upload(CPU 可見),批次拷貝到 VRAM。
  • Compute/後處理:盡量用 Shared Memory(tiling) + 併讀,少回大倉庫。
  • 格式:貼圖壓縮 + Mipmaps、能小就小(FP16、R8G8B8A8、BC/ASTC)。

14. 一句話總結

把常用資料放近、一次搬多一點、重複利用、少往返。
這就是 GPU 記憶體與資料流的王道。當你會善用 VRAM、快取、Shared Memory 與正確的資源格式,你的畫面會更穩、效能更高,開發也更從容。


明日預告(Day 5)

我們將進入 PBR(物理式渲染)原理與實作:把「金屬」「粗糙」「法線」「環境反射」這些關鍵概念,一次講清楚,並寫出能跑的最小 PBR Shader。


上一篇
Day 3|Shader 是什麼?GPU 編程的靈魂
下一篇
Day 5|PBR(Physically Based Rendering)原理與實作
系列文
渲染與GPU編程7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言