世界上最快,最安全的PNG解码器

2021-04-07 03:13:03

摘要:Waffs的PNG图像解码器是内存安全的,但也可以比Libpng更快地增加1.22倍和2.75倍,广泛使用的开源Cimblementation。它也比Libspng,Lodepng和STB_Imagec库以及最流行的Go和Rust PNG库更快。通过SIMD-Acceleration,8字节的宽输入和副本,当禁止 - Twiddling和Zlib-Depumprumpting整个映像(进入Onelarge中间缓冲区)而不是一行时(进入较小,可重复使用缓冲区)。 All-are-曾经需要更多的中间内存,但允许在zlib解压缩器的最快代码路径中解码的图像。

便携式网络图形,是一种无处不在的无损图像文件格式,系列Zlib压缩格式。当16位计算机和64个KibMemory限制仍然是一个激活的时,它在1990年代中发明了它。较新的ImageFormats(如WebP)和较新的压缩格式(如zStandard)可以以可比的解码速度产生MALLER文件,但仍有很多惯性的现有PNG图像的数量。通过oneMetric,PNG仍然是Web上的最常用的图像格式。 Mozilla遥测Image_decode_speed_xxx示例计数从2021-04-03(Firefox桌面夜晚89)将PNG分在JPEG之后:

Libpng是一个广泛使用的开源实现PNG图像格式,在ZLIB(库)上构建,广泛使用的ZLIB的开源实现(格式)。

Waffs是一个21世纪的编程语言,它用这种语言编写的标准图书馆。在中档X86_64Laptop上,尽管在允许的小样本集中,Wuffs可以比Libpng(我们定义为1.00xbaseline速度)来解码PNG照片1.50X和2.75倍。

libpng_decode_19k_8bpp 58.0MB / s的±0%1.00xlibpng_decode_40k_24bpp 73.1MB / s的±0%1.00xlibpng_decode_77k_8bpp 177MB / s的±0%1.00xlibpng_decode_552k_32bpp_ignore_checksum 146MB / s的±0%(†)libpng_decode_552k_32bpp_verify_checksum 146MB / s的±0%1.00xlibpng_decode_4002k_24bpp 104MB /秒±0 %1.00xlibpng 1.00倍1.00倍至---- wuffs_decode_19k_8bpp / clang9 131MB / s的±0%2.26xwuffs_decode_40k_24bpp / clang9 153MB / s的±0%2.09xwuffs_decode_77k_8bpp / clang9 472MB / s的±0%2.67xwuffs_decode_552k_32bpp_ignore_checksum / clang9 370MB /秒±0 %2.53xwuffs_decode_552k_32bpp_verify_checksum / clang9 357MB / s的±0%2.45xwuffs_decode_4002k_24bpp / clang9 156MB / s的±0%1.50xwuffs_decode_19k_8bpp / gcc10 136MB / s的±1%2.34xwuffs_decode_40k_24bpp / gcc10 162MB / s的±0%2.22xwuffs_decode_77k_8bpp / gcc10 486MB /秒±0 %2.75xwuffs_decode_552k_32bppp_ignore_checksum / gcc10 388mb / s±0%2.66xwuffs_decode_552k_32bpp_verify_checksum / gcc10 373mb / s±0%2.55xwuffs_decode_4002k_24bpp / gcc10 164mb / s±0%1.58xwuffs 1.50x至2.75x

(†):Libpng的“简化API”不提供忽略校验和的方法。 WECOPY为1.00x基线的verify_checksum编号。

例如,77K_8BPP源图像是160像素宽,120像素高于其颜色模型是每像素的8位(1字节;调色板索引)。解码为32BPP BGRA产生160×120×4 = 76800字节,缩写为77K。测试图像:

在104MB / s或164MB / s的产生4002k字节意味着Libpng或Waffs需要大约38毫秒或24ms,以解码1165×859图像。

其他PNG实现(Libspng,Lodepng,STB_Image,Go的Image / Png和Rust的PNG)在附录(基准标Numbers)中测量。

下面进一步的命令行示例参考Wuffs目录。通过CLONING此存储库获取它:

对于简洁起见,已经省略或以下面的一些命令行输出。

二维滤波。对于一行像素,通常更好地(更小的情况)压缩残差(像素值之间的差异和上方和左边的其邻居和左边的可被驱动的总和)而不是原始值。

使用PCLMulqdqinecructionBy Gopal,Ozturk,Guilford,Wolrich,Febghali,Dixon和Karakoyunu的快速CRC计算是2009年使用X86_64 SIMD指令实现CRC-32的2009WHITE纸张。实际代码看起来很想法。ARM SIMD代码是EVENSIMPLER,因为有专用的CRC-32相关的内在内部。

至于性能,Waffs'examply / CRC32Program大致相当于Debian / Bin / CRC32,除了在此178 MIBFILE上的7.3xFaster(0.056瓦0.410s)之外。

$ ls -lh linux-5.11.3.tar.gz | awk' {打印5美元" " $ 9}}' 178M Linux-5.11.3.tar.gz $ g ++ -o3 waffs / emaly / crc32 / crc32.cc -o wcrc32 $ time ./wcrc32 / dev / stdin< Linux-5.11.3.tar.gz05b309fbreal 0m0.056s $ time / bin / crc32 / dev / stdin< Linux-5.11.3.tar.gz05b309fbreal 0m0.410s

SMHASHER是一种测试和基准套件,可以实现各种哈希函数实现。它可以提供索赔的数据“我们的新的Foo散列函数比广泛使用的酒吧,BAZ和Quxhash函数更快”。但是,在将foo与CRC-32进行比较时,请注意,ASIMD加速的CRC-32实现可以是47xFasterthan Smhasher的简单CRC-32实现。

没有关于它的白皮书,但Adler-32校验和也可以加速。这是armcodeand x86_64code。

Waffs的Adler-32实现速度大约6.4倍(11.3gb / s与1.76gb / s)比zlib-the-library的速度(称为'模拟库'),由Benchstat计划组织:

$ CD Waffs $#¿只是一个不寻常的角色,易于搜索。按$#约会,在Waffs' 。来源,它标志着建立相关的信息$ grep的¿测试/ C / STD / adler32.c //¿wuffs模仿CFLAGS:-DWUFFS_MIMIC -lz $ gcc的-O3测试/ C / STD / adler32.c -DWUFFS_MIMIC -lz $ #运行基准。$ ./a.out -bench | BENCHSTAT / DEV / STDINNAME SPEEDWUFFS_ADLER32_10K / GCC10 11.3GB / S±0%WUFFS_ADLER32_100K / GCC10 11.6GB / S±0%MIMIC_ADLER32_10K / GCC10 1.76GB / S±0%MIMIC_ADLER32_100K / GCC10 1.72GB / S±0%

截至极端,最快的校验和实现只是没有执行Thechecksum计算(并跳过PNG文件中的4字节预期的校验范围)。

The Ignore_Checksum与verify_checksum基准标记数字在The The The The The The The The The The Tably Tabled 1.04x性能差异。对于Waffs,这是Aone-LineChange.even如果您不使用Waffs的解码器,关闭PNG校验和验证仍然可以加快您的解码,如果PNGDecoder不使用SIMD加速校验和实现,则可能超过1.04倍。

如果这样做,请注意,关闭校验和验证是一个权衡:不太能够检测数据损坏,并偏离相关文件格式规范的严格读取。

散向压缩数据的大部分压缩数据包括一系列代码,文字代码或复制代码。有256个可能的文字代码,一个可能的解压缩字节。每个副本代码都包含一个长度(多个月来复制,3到258个包含的)和距离(历史历史的较早较早,以前解压缩的输出从1和32768包之间复制)。

从2字节前开始复制3个字节:“Ana”。是的,CopyInput的最后一个“A”也是复制输出的第一个“A”,并且在Thecopy启动之前未知。

代码是编码的霍夫曼,这意味着它们采用变量(但积分)位(在1到48之间),并且不一定启动或endon字节边界。

文字代码发出单个字节。复制代码最多可发出258个字节。因此,任何一个代码的输出字节的最大值是258.我们稍后会重新访问。

Waffs版本0.2对ZLIB-The-Library的实现相似,并且至少在x86_64上执行了相似。 Wuffs版本0.3增加了两个重要的优化Formodern CPU(具有64位未对准的负载和存储):8字节块输入和8字节块输出。

如上所述,放气码在1到48位之间占用。 zlib-the-library的“解码1放气码”实现读取循环中的输入位大气中的位置。 Hold + =(unsignedlong)(* In ++)存在7个实例。<+ + + ++)&lt;比特;比特+ = 8;在Inffast.c中,一次加载输入位1字节(8位)。

我们可以每循环发出一次64位负载。如果在禁止缓冲区中已经未加工的位,则将在地板上删除其中一些加载留下,但这没关系。消耗这些位将在零中转换,与零的位WiSeor是一个无操作,并且符号或输入位是幂等的。 Fabian“Ryg”Giesen的2018年博客文章讨论了Moredetail的阅读位。

对于Waffs,每内部循环一次读取64位,将其脱墨丝高达1.30倍加速。

考虑微通向的代码序列来压缩为或不成为。那是Isetc。第二个可以由长度5 anddistance 13的复制代码表示.5字节拷贝的简单实现是循环。如果您的cpuallows未对准加载和存储,则为五个指令序列(4字节加载; 4字节存储; 1字节加载; 1字节存储; OUT_PTR + = 5)可能是也可能不会避免,但仍然是正确的(给定足够的距离)。甚至更好(在它较少的指令中)是复制太多(8字节的负载; 8-bytestore; OUT_PTR + = 5)。

:to_be_or_not _ ????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????:^ ??????????:^ ^:out_ptr + = 5 ::: [)写入1字节:vv:to_be_or_not_to_be.or ??????????:^^:out_ptr + = 1

后续代码的输出(例如,文字&#39;。&#39;字节)将覆盖ANDFIX过剩。或者,如果没有后续代码,则具有解压缩率为后条件,可以修改输出缓冲区中的任何字节,即使超过“解压缩字节数”返回。

请注意,ZLIB-The-Library的API不允许此优化。它的流量返回函数为双晶和输出使用单个缓冲区,因此8字节覆盖可能错误地修改历史记录(哪些调用滑动窗口),因此损坏的未来输出。

对于Waffs,将副本长度舍入到8个将其脱墨的倍数增加到1.48倍。

GZIP文件格式粗略地说,缩小压缩与CRC-32校验和组合。与emply / crc32一样,waffs的示例/ zcat编程大致相当于debian / bin / zcat,除了同一178个MIB文件的3.1x速度(2.680 vs 8.389s),也以自我强加的seccomp_mode_strictsandbox运行。

$ gcc -o3 waffs / example / zcat / zcat.c -o wzcat $ time ./wzcat&lt; Linux-5.11.3.tar.gz&gt; / dev / nullreal 0m2.680s $ time / bin / zcat&lt; Linux-5.11.3.tar.gz&gt; / dev / nullreal 0m8.389s

作为一致性检查,两个程序输出的校验和应该是chesame(并且0x750d1011 checksum值应该在.gz文件的最终字节中)。请注意,我们现在正在检查解压缩的内容。 robearlier示例/ crc32输出检查压缩文件。

$ ./wzcat&lt; Linux-5.11.3.tar.gz | ./wcrc32 / dev / stdin750d1011 $ / bin / zcat&lt; Linux-5.11.3.tar.gz | / bin / crc32 / dev / stdin750d1011 $ tail --bytes = 8 linux-5.11.3.tar.gz | HD00000000 11 10 0D 75 00 78 70 3F

从扁平轨道上的点A到P点B很简单:像YOSCAN一样快地运行。现在假设Point B位于悬崖的边缘,以便超越异常(如果不是从秋天,那么从鲨鱼)。赛车现在涉及一个零件(让您的颜色为蓝色),在那里您可以在您可以快速运行和备份部分(让我们的颜色为红色),在那里您可以在较慢,但具有更多控制。

解压缩偏转涉及写入目标缓冲区并写入缓冲区界限(经典的'缓冲区溢出'安全漏洞)是类似的卷曲。为避免这种情况,Zlib-the-library有两个解压缩模型:快速的“蓝色”(当其他一些条件)和慢速“红色”时(否则)。

另外,Libpng分配两个缓冲区(对于当前和先前的opixels),并调用进入zlib-library h次数,其中h是像素中的图像的isheight。每次,目标缓冲区都是onerow的大小(每像素的字节的宽度,加上一个过滤器concemberyByte,大致说话)而没有任何松弛,这意味着zlib-libraryspends的最后258或更多字节在慢速“红色”区域中的每一行。 FOREXAMPLE,这可以是大约300×200 RGB(每像素)图像的像素的四分之一,并且在CPU时间方面的比例越高。

Waffs的Zlib-Format解压缩器也使用这种蓝色/红色的DualPlingation技术,但Waffs的PNG解码器解压缩到一个灯节缓冲的全部,而不是一次行。几乎所有(例如,超过99%的像素的300×200 rgb图像的像素)现在在“蓝色”区域中。这比“红色”区域更快,但它还避免了任何指令高速缓存或分支预测慢下行版慢下行版交替的蓝色代码和红色代码。

All-AT-areDive需要O(宽度×高)中间内存(WhatWuffs调用“工作缓冲区”)而不是O(宽度)内存,但如果您无论如何都要将整个图像转换为RAM,那已经需要O(宽度×高度)内存。

此外,Waffs的图像解码API确实给出了来电者对内存使用的一些选择.Wuffs并没有说,“我需要M个字节的内存来解码此图像”,它是“在M0和M1之间的情况下(包括M1之间)。你给我的越多,我会更快。

Waffs的PNG解码器目前将M0设置为等于M1(别无选择;全遍是强制性的),但是未来的版本可以通过提供较低的M0来提供单行的AT-A-A-A-A-A-AT-A-A-AT-A-A-AT-A-AT-A-AT-A-AT-A-AT-A-A-AT-A-AT-A-A-AT-A-AT-A-AT-A-A-AT-A-A-AT-A-AT-A-AT-A-A-AT-A-TapeOption。额外的O(宽度×高度)内存成本(以表格成本)为关心的呼叫者。

Waffs-The-Library和Libpng(但不是所有其他PNG解码器)具有SIMD实现PNG的二维滤波器。例如,这是Waffs的x86filters。

Libpng实际上可以在此步骤稍微快速,因为它可以确保自分配的像素行缓冲区与SIMD-FriendliestBoundaries对齐。对齐可以影响SIMD指令选择和性能。 ARM和X86_64通常分别对其进行较小。

Waffs-the-Librature会使缓冲区对齐的承诺较少,部分原因是 - 语言没有对AlloCateMemory的能力,但主要是因为Zlib-Dexcrumpting全体at-are-are-on-on-mexable,因此需要放弃脱紫外线。 4字节 - 对齐每行的开始。尤其如此,均匀的RGBA像素为每个通道的8位为每像素4个字节,因为PNGFILE格式将为每行添加一个字节(用于过滤配置)。 zlib解压缩层每行看到奇数字节。

尽管如此,貌相表明,在PNG滤波中,在Zlib-decompressionthan中花费了更多时间,使得全部zlib-decompretchingouleweigh的Zlib-decompleplessoutweigh的增量。 Waffs的raspberry pi 4(32位aramv7l)比较的与libpng基准比率并不像x86_64比率那样令人印象深刻(见下面的硬件),但Waffs仍然来自于此。

切向,每行的一个过滤器配置字节,在滤波的像素数据的行之间交错,也使得它不可能直接进入目的地像素缓冲器zlib-decompressall。相反,我们对中间工作缓冲区(具有内存成本)并将其映像(和过滤)99%的映射到目标缓冲区。在后智,方面的文件格式设计不需要单独的工作缓冲区,但它现在正在更改PNG的契约。

上面描述的优化技术被应用于新代码:Waffs-用Waffs-语言编写的图书馆。他们也可以应用于外向代码,但有理由更喜欢新的代码。

libpng是用c编写的,其缺乏内存安全性很好地记录了.Furtheratore,它的错误处理API围绕SetJMP和Longjmp.Non-Local GotoS构建静态或正式分析更复杂。

尽管文件格式在很大程度上没有改变以来,自1999年以来(2003年版本规定; APNG是Anunofficial延期),Libpng C实施已从2002年到2018年的2002年到2021,9的94个Cverecords收集了74名Cverecords。

它的源代码有一个单行评论,详细说明“todo:警告:截断错误:危险Willrobinson”,但没有说其他任何事情。评论已在2013年添加到2021年,但代码本身较旧。

libpng也只是复杂。作为一个非常粗糙的度量,运行WC -L * .carm / *。C Intel / *。C在Libpng的存储库中计数35182行代码(不包括* .h头文件)。运行wc -l std / png / *。Waffs的Waffs repositoryCounts 2110行。前图书馆允许实现编码器,不静止解码器,但即使在减半之后,它仍然是8xratio。

我尝试修补zlib-the-liket几个久违,但它比我在第一个棘手的是棘手的,因为上面提到的充气API问题。

无论如何,其他人已经这样做了。 zlib-ng / zlib-ng和cloudflare / zlib都是zlib-the littrucyforks,具有性能补丁。这些补丁(以及Zlib-Library的Chromium'Scopy中的贴片)包括与此处呈现的那些类似的优化思路,以及编码器侧的其他技术。

使用test / c / std / png.c程序(请参阅下面的再现),运行ld_library_path = / the / path / to / zlib-ng / build ./a.out -bench显示带有zlib-ng的libpng(第二个下面的数字集)有点迅速,而不是与vanilla zlib(下面的第一个数字)快。

libpng_decode_19k_8bpp 58.0MB / s的±0%1.00xlibpng_decode_40k_24bpp 73.1MB / s的±0%1.00xlibpng_decode_77k_8bpp 177MB / s的±0%1.00xlibpng_decode_552k_32bpp_ignore_checksum 146MB / s的±0%(†)libpng_decode_552k_32bpp_verify_checksum 146MB / s的±0%1.00xlibpng_decode_4002k_24bpp 104MB /秒±0 %1.00xlibpng 1.00倍1.00倍至---- zlibng_decode_19k_8bpp / gcc10 63.8MB / s的±0%1.10xzlibng_decode_40k_24bpp / gcc10 74.1MB / s的±0%1.01xzlibng_decode_77k_8bpp / gcc10 189MB / s的±0%1.07xzlibng_decode_552k_32bpp_ignore_checksum / gcc10 skippedzlibng_decode_552k_32bpp_verify_checksum / gcc10 177MB / s±0%1.21xzlibng_decode_4002k_24bpp / gcc10 113mb / s±0%1.09xzlibng 1.01x至1.21x

CloudFlare / Zlib从Zlib-The-Library版本1.2.8叉。将ld_library_path指向它的libz.so.1 make ./a.out使用版本&#39失败; zlib_1.2.9&#39;找不到(按/llib/x86_64-linux-gnu/libpng16.so16)。

Go和Rust都成功,现代和内存安全的编程语言显着采用。但是,对于现有的C / C ++项目,它是Easierto合并Waffs-The-Library,它被转换为C(及其C表格被剥离到存储库中)。它就像使用任何其他第三方C / C ++库,它只是不是手写的C / C ++。相比之下,将Go Orrust代码集成到C / C ++项目中,最小地设置额外的递送器和其他构建工具。

尽管如此,基于本帖子中讨论的技术,Goor Rust的PNG实现可能很好。 FOREXAMPLE,既不是GO或RUST的Adler-32实现也是SIMD加速的。 ITMay也值得尝试尝试8字节块输入和8字节块outputTechniques。 Go的缩小实现在ATIME.RUST的MINIZ_OXIDE中只读一个字节,读取ATIMEAND FO的四个字节大于1,但八个仍然更大。据我康塞尔,既不是GO或RUST的PNG解码器ZLIB-DECOMPRESS ALL-AT-一次。

此外,与Go或Rust,Waffs的Memoryafetyis在编译时执行,而不是通过插入运行时检查,例如, IIN A [I]在界限内或(x + y)不会溢出U32。 Go Andrust编译器可以ELIDE中的一些检查,尤其是在迭代Aunign Access模式时,但是例如, 解码偏转代码每次迭代使用variablenumber字节。 运行时安全检查可能会影响性能。 我喜欢Zig的“表演安全:Choosetwo”座右铭,但联合国 ......