大图秒开技术
看图软件的第一体验是首屏延迟——双击到看见图片之间那几十到几百毫秒。这段时间几乎全花在"解码"上:把磁盘上压缩的字节,还原成屏幕能画的像素。
果核看图没有直接套用某一个现成解码库了事,而是按格式选择最快的路径,并对最常见的 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 兜底。