大圖秒開技術
看圖軟體給人的第一印象是首屏延遲——從按兩下到看見圖片之間那幾十到幾百毫秒。這段時間幾乎全花在「解碼」上:把磁碟上壓縮過的位元組,還原成螢幕能繪製的像素。
果核看圖沒有直接套用某個現成解碼函式庫了事,而是依格式選擇最快的路徑,並對最常見的 JPEG / TIFF 做了自研的平行最佳化。本文只談解碼這一件事。
一張圖片是怎麼變成像素的
以 JPEG 為例,解碼大致分三步,像一條生產線:
後兩步(IDCT、色彩轉換)是逐像素區塊的獨立運算,天生適合用 SIMD 指令平行處理;難點在第一步熵解碼——它是一條變長位元流,下一筆資料的位置取決於上一筆解出多長,看起來只能一位一位依序讀取。後面會看到,這正是各家解碼器拉開差距的地方。
JPEG:三條解碼路徑
系統內建的 WIC
WIC(Windows Imaging Component)是 Windows 內建的影像編解碼框架,以 COM 元件形式提供。它的優點是零相依、什麼都能解(JPEG / PNG / GIF / BMP / TIFF…),呼叫一個系統 API 就能取得像素。
代價是「通用」:它要照顧所有格式、所有情境,無法為某一種格式做極致最佳化,也不會主動用滿你的多核心 CPU。所以在果核看圖中,WIC 只作為後備——那些沒有專用快速路徑的格式(BMP、GIF、ICO 等)交給它,省心可靠。
libjpeg-turbo:用 SIMD 加速
libjpeg-turbo 是業界標準的 JPEG 函式庫,相較原始 libjpeg 的關鍵改進是:把 IDCT、升採樣、色彩轉換這些逐像素的熱點用 SIMD 指令(SSE2 / AVX2)重寫——一條指令同時處理 8 個、16 個資料,而不是一次一個。
但 libjpeg-turbo 的 SIMD 只加速了生產線的後兩步。熵解碼仍是單執行緒序列的——除非 JPEG 檔案中寫入了 RST(restart)重啟標記,把位元流切成若干獨立段落。問題是絕大多數 JPEG(手機、相機、截圖)都不帶 RST,於是熵解碼成了多核心時代無法分攤的瓶頸。
自研 pjdec:把序列的熵解碼也平行化
這是果核看圖解碼引擎中最核心的一塊。我們想回答一個問題:沒有 RST 標記的一般 JPEG,熵解碼能不能用滿多核心?
直覺上不能——位元流是變長的,而且每個資料區塊的亮度(DC 係數)是相對前一個區塊的差值,環環相扣。但我們用了一種「投機平行」的方法:
它依賴 Huffman 編碼一個奇妙的性質——自同步:即使從錯誤的位置開始解碼,只要往後讀幾個碼字,解碼器就會自動「對齊」到真正的碼字邊界。所以每個核心從自己段首的位元組邊界盲目開始解碼,先得到一串「位置可能不對」的結果,並記下每個資料區塊的起始位置(檢查點)。
接著做一次輕量的序列驗證:上一段真實解碼的結束位置,如果和下一段某個檢查點完全相等,由編碼的確定性可知——從這裡往後兩者逐位元一致,於是直接採用下一段的成果。至於環環相扣的 DC 差值,盲解時從 0 起算,和真值只差一個常數,同步後整體補上一個差值即可(利用 16 位元整數的溢位迴繞特性,零成本)。
萬一某段始終對不上(極少見),就依序補解那一小段——正確性永遠有保底,只是少平行一點。最終效果:一張普通的大 JPEG,熵解碼也能用滿多核心,首屏明顯更快。
TIFF:兩條解碼路徑
libtiff
libtiff 是處理 TIFF 的事實標準函式庫,格式涵蓋最全(各種壓縮、CMYK、多頁)。果核看圖以它打底,確保相容性——任何能被 libtiff 讀取的 TIFF,我們都能開。它的侷限同樣是以單執行緒依序處理一個個資料區塊(strip / tile)。
自研 tiff_mt:分塊平行
TIFF 的結構其實天生適合平行——影像被切成許多條帶(strip)或圖塊(tile),彼此獨立。我們的 tiff_mt 解碼器就是在這之上做多執行緒:
它比「直接開多執行緒」多想了兩步:
- 自適應執行緒數:不是無腦開滿。無壓縮的 TIFF 受記憶體頻寬限制,執行緒多了反而搶頻寬;工作量太小則不值得開執行緒——依核心數、壓縮方式和資料量動態決定。
- 兩種平行聯動:當 TIFF 使用 JPEG 壓縮、但只有很少幾個大區塊時,單純的「區塊級平行」會讓多數核心閒置。這時 tiff_mt 會切換到串流內平行——直接沿用前面 pjdec 的投機平行,去拆解那個大 JPEG 串流。兩個自研解碼器在這裡接上了。
我們的策略
果核看圖內部有一張解碼器登記表,每個解碼器宣告自己認得的格式特徵(檔頭 magic)和優先順序。開啟檔案時,引擎依檔頭精準比對、挑選優先順序最高的專用解碼器:JPEG 走 pjdec、TIFF 走 tiff_mt,都沒命中才落到 WIC 後備。