一、为什么做测试?
FFmpeg libswscale和libyuv是最常用的两个做图像缩放、格式转换的开源库。做视频图像处理经常会用到它们,但很少有人对它们做过详细的比较。我想尝试回答以下几个问题:
1. libyuv比FFmpeg libswscale快几倍?
2. libyuv什么情况下比libswscale快,什么情况下优势不明显,或者更慢?
3. libswscale慢在哪里?如何提速?
4. libyuv的劣势是什么?
有了具体的测试数据,我们就能回答,某个场景适合用libyuv还是libswscale。
需要提前声明,因时间精力有限,我只能挑选最有代表性的场景做少量的测试,数据难免有偏颇。如果读者有更多场景数据信息,欢迎补充。
二、测试环境
拿来做测试的硬件设备是macbook pro M1款。之所以选择ARM架构CPU做测试
- 因为在ARM架构上有libyuv vs libswscale的技术选型问题,x86特别是服务端,往往沿用FFmpeg全家桶同属ARM架构的大量
- ARM架构的CPU,一定程度上可以用来推测在Android/iOS移动端的表现
x86平台也简单运行了一下,和libswscale ARM优化后的数据基本一致。
三、测试结果
1. RGB转YUV:测试4096×4096分辨率RGB转同分辨率YUV
- libyuv:耗时5 ms
- libswscale:耗时65 ms
libyuv的速度是libswscale的11倍。
2. RGB下采样:测试4096×4096分辨率RGB转2048×2048分辨率RGB
- libyuv:耗时11 ms
- libswscale:耗时66 ms
libswscale依旧很慢。
libyuv RGB下采样耗时有点久,因为libyuv先把RGB转成BGRA,再对RGB做下采样,最后把BGRA转回RGB。如果直接用BGRA做下采样,速度会更快。
3. RGB转YUV并下采样:测试4096×4096分辨率RGB转2048×2048分辨率YUV
- libyuv:耗时12 ms
- libswscale:耗时47 ms
libyuv的耗时增加了,因为libyuv缩放和格式转换是分开的操作,需要用户控制。我先做下采样,用RGBScale把分辨率缩小一半,再对2048×2048分辨率图像做格式转换。后一步操作libyuv耗时只有1 ms左右,前一步下采样占了11 ms。
由此可见,libyuv做了优化的地方,速度非常快。而像RGB缩放先转BGRA的操作,属于糊弄事。关于缩放,后面还有更详细解读。
libswscale的速度变快了,因为libswscale格式转换和缩放是合并到一起的操作,主要有三个操作,对应源码的libswscale/input.c, libswscale/hscale.c, libswscale/vscale.c。这里非常晦涩,暂时不展开来讲了。
4. 多线程加速:测试4096×4096分辨率RGB转2048×2048分辨率YUV
- libyuv不支持多线程加速,耗时不变(12 ms)
- libswscale 4线程:耗时14 ms
libswscale终于靠堆算力,达到了和libyuv接近的性能。继续堆算力,libswscale的速度可以超过libyuv,比如8线程耗时9 ms多。线程过多收益递减。在服务器上,算力充足而libswscale单线程处理成为瓶颈时,别忘记多线程加速的杀手锏。
四、详细解读与libswscale提速
libswscale比libyuv慢,首先是libswscale缺少ARM架构的汇编优化。与之相反,libyuv在ARM上的汇编优化覆盖全面。
插个题外话,libyuv用的内联汇编的方式,我在x86平台遇到了GCC把内敛汇编给优化掉了,导致程序空跑没有对数据做处理的情况。clang编译没问题。大概Google只在意clang。
我最近给libswscale补了一点aarch64的优化,见https://github.com/FFmpeg/FFmpeg/tree/master/libswscale/aarch64 。优化前后的耗时对比。
RGB转YUV | RGB转YUV + 下采样 | 多线程 | |
优化前 | 65 ms | 47 ms | 14 ms |
优化后 | 29 ms | 17 ms | 5 m |
可见速度提高了一倍以上。
但是,简单的RGB转YUV的速度,libswscale还是远远落后于libyuv。libyuv的操作,基本上就是你能想到的最简单的操作。libswscale就是魔法了,下面的方法是libswscale RGB转YUV的其中一个步骤。
static void hScale16To15_c(SwsContext *c, int16_t *dst, int dstW,
const uint8_t *_src, const int16_t *filter,
const int32_t *filterPos, int filterSize)
{
const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(c->srcFormat);
int i;
const uint16_t *src = (const uint16_t *) _src;
int sh = desc->comp[0].depth - 1;
if (sh<15) {
sh = isAnyRGB(c->srcFormat) || c->srcFormat==AV_PIX_FMT_PAL8 ? 13 : (desc->comp[0].depth - 1);
} else if (desc->flags & AV_PIX_FMT_FLAG_FLOAT) { /* float input are process like uint 16bpc */
sh = 16 - 1;
}
for (i = 0; i < dstW; i++) {
int j;
int srcPos = filterPos[i];
int val = 0;
for (j = 0; j < filterSize; j++) {
val += src[srcPos + j] * filter[filterSize * i + j];
}
// filter=14 bit, input=16 bit, output=30 bit, >> 15 makes 15 bit
dst[i] = FFMIN(val >> sh, (1 << 15) - 1);
}
}
注意这里的filter本身并不是用来做RGB转YUV的。如果有下采样操作,这个filter才真正起作用,没有下采样就空算一遍。循环体内部不是顺序直接访问内存,要索引来索引去,导致汇编优化效率变低,不好批量顺序的加载和计算。
最终结果是,libswscale复杂设计的副作用,导致在简单场景的性能无论如何追不上libyuv(多线程加速除外)。如果是下采样加格式转换这样的操作,libswscale设计上的overhead会降低下来。libswscale甚至允许你塞个自定义的kernel进去,属实超前设计了,牺牲的是性能。
libswscale提速,最简单粗暴的是加线程,其次是增加汇编优化,最难的是架构上的优化设计。最难的这一步,社区在推进中了。
五、libyuv的劣势
在缩放算法上,libyuv支持的缩放模式很少:
typedef enum FilterMode {
kFilterNone = 0, // Point sample; Fastest.
kFilterLinear = 1, // Filter horizontally only.
kFilterBilinear = 2, // Faster than box, but lower quality scaling down.
kFilterBox = 3 // Highest quality.
} FilterModeEnum;
libswscale支持的就多了。对比性能的时候,注意要用同样的算法来对比。
在计算方式上,libyuv精度差。libswscale设计上,对于8 bit的格式,用15 bit来保存中间的计算结果(计算本身可能是16 bit或32 bit的)。其他高精度格式用更高精度来保存中间结果。15 bit是保证不会整型溢出的情况下的最高精度(这里又是libswscale的黑魔法)。
看下简单RGB转YUV的PSNR对比:
R | G | B | avg | |
libyuv | 37.58 | 41.98 | 34.50 | 37.04 |
libswscale | 37.31 | 41.98 | 35.45 | 37.49 |
测试图片:
六、结论
- 复杂操作,两者性能差异较小,越简单的操作,性能差异越大
- 算力充足、特别在意图像质量时,选择libswscale
- 算力敏感的情况下,选择libyuv
作者:quink
来源:Fun With FFmpeg
原文:https://mp.weixin.qq.com/s/tAJvoTUh0VlP4jfBbp1G6w
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。