FFmpeg内存对齐
  1bTlX33AWdQ4 2023年11月02日 27 0

内存对齐是计算机编程提高性能的一种方式,我们平时用的 ​​struct​​​,​​malloc()​​ 都会进行内存对齐,如下代码:

struct {
int x;
char y;
}s;

int main(){
printf("%d\n", sizeof(s)); // 输出8
return 0;
}

32位系统中,上面的代码中 ​​int​​ 占 4 字节,​​char​​ 占 1 字节,但是结果会输出 8 ,这是因为编译器自动帮我们做了内存对齐。

而 ​​malloc()​​ 函数也会进行内存对齐,32位系统里面,​​malloc​​ 函数返回的地址是以8字节对齐,在64位系统是以16字节对齐的。


为什么要做内存对齐呢?为了提高内存的访问效率,举个例子。

因为某些32位的 CPU,每个总线周期都是从偶地址开始读取32位的内存数据的,假设一个 ​​int​​ 变量的地址是 ​​0x01​​,当 执行汇编指令 ​​mov %eax,0x01​​ 的时候,CPU 内部需要做比较多的额外的功能,首先CPU 从 ​​0x00​​ 开始读取 4 字节数据,然后把第一个字节丢弃,然后从 ​​0x04​​ 开始再读取 4 字节,把后面 3个字节丢弃,然后把第一次 读到的 3 字节数据 跟第二次读到的 1 个字节数据合并在一起,然后再 ​​mov​​ 到 ​​eax​​ 寄存器。

不过这个过程是 CPU 内部做的,你在汇编代码是看不见这个过程的。不过由于内存不对齐,CPU 干了这么多额外的时间,肯定会有性能损耗,这是可以测试出来的。

扩展知识:目前 x86 和 arm 默认都允许不对齐了,arm 可以启用不对齐抛异常。

下面演示一个 xxxx 的例子,如果不对齐内存,性能有多少损耗。

TODO:后面补充一个例子。读者有例子提供,欢迎留言。


虽然编译器会自动做内存对齐,malloc 也会做内存对齐,但是由于 FFmpeg 里面用了一些汇编优化,所以 FFmpeg 对内存对齐做了一些扩展,就是 ​​av_malloc​​ 函数。

因为 malloc 函数只能是 16 字节对齐,而如果用到 AVX 指令,需要对齐到更大的字节,AVX512 支持 64 字节的内存操作。下面看一下 ​​av_malloc​​ 的内部实现,如下:

FFmpeg内存对齐_内存对齐

可以看到,用的是 ​​posix_memalign​​ 函数来自定义对齐大小,ALIGN 宏的定义如下:

#define ALIGN (HAVE_AVX512 ? 64 : (HAVE_AVX ? 32 : 16))

可以看到,就是根据 是否有 AVX 指令来判断对齐的。


除了 ​​av_malloc​​ 有内存对齐之外,还有很多的函数,都有一个对齐大小参数,例如 ​​av_image_get_buffer_size​​ 函数,定义如下:

/**
* Return the size in bytes of the amount of data required to store an
* image with the given parameters.
*
* @param pix_fmt the pixel format of the image
* @param width the width of the image in pixels
* @param height the height of the image in pixels
* @param align the assumed linesize alignment
* @return the buffer size in bytes, a negative error code in case of failure
*/
int av_image_get_buffer_size(enum AVPixelFormat pix_fmt, int width, int height, int align);

主要的是最后一个参数 ​​align​​ ,对齐参数,剧透一下,​​align​​ 实际上只对 ​​width​​ 进行对齐,不会对 ​​height​​ 对齐。

下图的代码是 ​​av_image_get_buffer_size​​ 函数的使用例子。

FFmpeg内存对齐_数组_02

可以看到,这是一个 YUV420p 格式的 300 x 300 的一个图片,未对齐占用 135000 字节 。由于是 YUV420 的格式,所以一个像素占 1.5 字节。FFmpeg内存对齐_内存对齐_03如果按 16 字节对齐,就会占用 139200 字节。这个 1392000 是如何计算出来的呢?下面就探索一下

先放一张 ​​av_image_get_buffer_size​​ 函数的内部流程图,如下:

FFmpeg内存对齐_内存对齐_04

下面来分析一下 ​​av_image_get_buffer_size​​ 函数的代码实现,如下:

提醒:读者可以用 clion 直接断点进去 ​​av_image_get_buffer_size​​ 函数跟踪流程。推荐阅读​​《用Ubuntu18与clion调试FFmpeg》​

FFmpeg内存对齐_内存对齐_05

可以看到,他内部会先检查一下 ​​width​​ 跟 ​​height​​ 是否正确,他是怎么检查的呢?再来看一下 ​​av_image_check_size​​ 的内部实现。

FFmpeg内存对齐_数组_06

FFmpeg内存对齐_内存对齐_07

可以看到,实际上就是检查 ​​width​​ 跟 ​​height​​ 不能小于0 ,然后还有一些最大像素检查,那个 ​​stride​​ 代表 步幅,实际上就是 ​​width​​ + 填充字节,请阅读​​《图像步幅》​


回到 ​​av_image_get_buffer_size​​ 的代码,注意下面这两句代码。

// do not include palette for these pseudo-paletted formats
if (desc->flags & FF_PSEUDOPAL)
return FFALIGN(width, align) * height;

​pseudo-paletted​​ 这种伪格式,会立即对齐返回,只是对 宽度 ​​width​​ 进行了对齐,​​FFALIGN​​ 函数的实现如下:

#define FFALIGN(x, a) (((x)+(a)-1)&~((a)-1))

FFmpeg内存对齐_字节对齐_08

可以看到,​​FFALIGN​​ 就是一个对齐的函数。


接下来到 ​​av_image_fill_linesizes​​ 函数的调用,如下,注意:这个函数只传了 宽度,没有管高度

FFmpeg内存对齐_字节对齐_09

FFmpeg内存对齐_字节对齐_10

上图中有两个重点:

1,​linesizes[]​​ 数组变量,这里面存储的其实是 ​​stride​​ 值,并不是分量的样本数,因为他没管高度。后面会详细讲解,推荐阅读​​《如何使用FFmpeg的解码器》​

2, ​​av_image_fill_max_pixsteps()​​ 跟 ​​image_get_linesize()​​ 这两个函数。

​av_image_fill_max_pixsteps​​ 函数的作用就是计算出 每一个 ​​plane​​ 的 最大 ​​step​​ 信息,​​step​​ 是 ​​AVPixFmtDescriptor​​ 结构里面的字段。

FFmpeg 里面有两个术语,​​component​​ 跟 ​​plane​​。​​component​​ 代表 分量,Y就是一个分量 ,U 也是一个分量,V 也是一个分量。而 ​​plane​​ 代表平面,行的意思,​​plane​​ 代表的是存储方式,虽然 YUV 有 3 个 分量,但不是一定会有 3 个 ​​plane​​,因为有些像素格式,UV 是混合存在同一个 ​​plane​​ 里面的。

相关知识推荐阅读​​《AVPixFmtDescriptor结构》​


​av_image_fill_max_pixsteps​​ 函数的代码实现如下,函数虽然接受的是参数看起来是一个数组,但其实是指针,编译器会把数组转成指针传进去的。

FFmpeg内存对齐_字节对齐_11

下面用个实际的例子来演示一下 ​​av_image_fill_max_pixsteps​​ 函数的作用,代码如下:

int align_size = av_image_get_buffer_size(AV_PIX_FMT_YUV420P ,300 ,300 ,16);
int align_size2 = av_image_get_buffer_size(AV_PIX_FMT_UYVY422 ,300 ,300 ,16);

下面两张 debug 调试图 比较重要,需要时常翻阅。

FFmpeg内存对齐_数组_12

FFmpeg内存对齐_字节对齐_13

上图中,有两个重要的数组变量,​​max_step​​ 跟 ​​max_step_comp​​ 。先来逐个解析。

int max_step[4];  /* max pixel step for each plane */

这句英文注释的意思是,​​max_step​​ 数组存储的是 每个 ​​plane​​ 中最大的 ​​step​​,​​AV_PIX_FMT_YUV420P​​ 的 YUV 是独立存放的,也就是放在 3 个 plane 里面,所以 最大 ​​step​​ 就是自己,没有其他的 分量需要比较。如下:

FFmpeg内存对齐_内存对齐_14

如果是 ​​AV_PIX_FMT_YUYV422​​ ,他只有 一个 ​​plane​​,也就是 plane 0 ,所以最大值是 4 。如下:

FFmpeg内存对齐_字节对齐_15

前面的 debug 调试图中,420p 的 ​​max_step​​ 数组是 1,1,1,0。而 422 的 ​​max_step​​ 数组是 4,0,0,0。就是这么算出来的。


再来讲一下 ​​max_step_comp​​ 数组变量。

int max_step_comp[4];  /* the component for each plane which has the max pixel step */

这句注释的意思是这样的,因为可能有 多个 ​​component​​ 存储在 一个 plane 里面,例如 UV 合在一个 plane,他需要知道,最大的那个 step 是来自 U ,还是来自 V 的。也就是 ​​max_step_comp​​ 数组是用来确定 最大的 step 是来自哪个 ​​component​​ 的。

这也就是为什么 上面的 debug 图里面,​​420P​​ 的 ​​max_step_comp​​ 数组是 0 1 2 0,而 ​​422​​ 的 ​​max_step_comp​​ 数组是 1 0 0 0,因为 ​​422​​ 只有一个 plane,所以只用到 ​​max_step_comp​​ 数组里面的一个元素。

在 ​​422​​ 里面 3 个 ​​component​​ 都在 第 0 个 plane 里面,而且 第 0 个 ​​component​​ 的 ​​step​​ 是 2,第 1 个 ​​component​​ 的 ​​step​​ 是 2, 第 2 个 ​​component​​ 的 ​​step​​ 是 2。

因为 第一 跟 第二 ​​component​​ 的 ​​step​​ 是一样的,所以以第一个为准,所以 ​​422​​ 的 ​​max_step_comp​​ 数组为 1 0 0 0 ,就是这么算出来的。


​av_image_fill_max_pixsteps​​ 函数已经讲解完毕,接着讲 ​​image_get_linesize​​ 函数干了什么事情。代码如下:

FFmpeg内存对齐_字节对齐_16

上图的代码中有 3 段逻辑非常重要 :

1,获取 s 变量的值。

s = (max_step_comp == 1 || max_step_comp == 2) ? desc->log2_chroma_w : 0;

变量 s 的全称是 shift (位移)的意思,首先 他 判断 ​​component​​ 是不是 1 跟 2,1/2 代表这是一个 色度 ​​component​​,色度 分量的数量,可以由 亮度分量 的数量 右移 得到,右移多少位呢?这个就在 ​​desc->log2_chroma_w​​ 里面。

0 本身就是 亮度 ​​component​​,所以他不需要判断 0 ,只需判断 1 跟 2。如果是 1 或者 2,那这个 ​​component​​ 就属于色度分量

这里墙裂推荐阅读​​《AVPixFmtDescriptor结构》​​,阅读本文的前提是理解 AVPixFmtDescriptor结构

2,计算 shifted_w 变量的值。

shifted_w = ((width + (1 << s) - 1)) >> s;

​shifted_w​​ 实际上就是色度的样本数,因为 色度的 样本数样通过 width 位移得到, width 实际上就是亮度样本数的大小。提示:​​shifted_w​​ 有可能等于 ​​width​​。

​(1 << s) - 1)​​ 这个操作不太容易看懂,但这是 ​​FFmpeg​​ 里面的一个细节,实际上就是向上取整。他这样写跟直接 调 ​​ceil()​​ 函数是一样的。

shifted_w = ceil( width >> s);

不过位移在指令里面是性能最高的,用 ​​ceil​​ 会带来函数调用损耗。这是 FFmpeg 性能优化的细节之处。

提醒:这里取整实际上就是采样取整,亮度右移 1 位就是色度样本数量,但是右移1 就是除以 2,如果一个数不能被 2 整除。就代表色度样本的分配是不均衡的。

不均衡是什么意思,例如 YUV420p 有 401 个亮度样本,他的规则是每 4 个Y 共享一个 UV。多了一个 亮度样本,最后肯定是一个 Y 独享一个 UV,这就是不均衡。

3,计算 linesize 变量的值。

linesize = max_step * shifted_w;

可以看到,他是用了 ​​max_step​​ 直接相乘,在 ​​AV_PIX_FMT_YUV420P​​ 里面 不太容易看出这句代码的真正含义,因为 ​​420p​​ 所有的 ​​component​​ 的 ​​step​​ 都是 1。

所以我们换成 ​​AV_PIX_FMT_UYVY422​​ 来看一下,如下:

分析技巧:要理解这个函数,必须代入实际的场景来分析,实际断点调试一番。

FFmpeg内存对齐_数组_17

可以看出,​​linesize​​ 算出来 600。

因此,在 ​​AV_PIX_FMT_YUV420P​​ 跟 ​​AV_PIX_FMT_UYVY422​​ 两种个格式下,​​av_image_fill_linesizes​​ 函数执行完之后,​​linesize[4]​​ 数组的值如下:

#AV_PIX_FMT_YUV420P
linesize[4] = {300,150,150,0}
#AV_PIX_FMT_UYVY422
linesize[4] = {600,0,0,0}

再次提醒:​​linesize[4]​​​ 数组 里面存储的是 ​​stride​​ 值,不是样本数。


​av_image_fill_max_pixsteps​​ 跟 ​​image_get_linesize​​ 函数都讲解完毕了,那 ​​av_image_fill_linesizes​​ 函数也算讲解完毕了。

下面回到 ​​av_image_get_buffer_size​​ 函数继续讲解,重点如下:

FFmpeg内存对齐_内存对齐_18

可以看到,在 490 行开始对齐内存 ​​stride​​ 值。


接下来分析,​​av_image_fill_plane_sizes()​​ 函数,注意在这个函数,会把 高度 丢进去。

FFmpeg内存对齐_内存对齐_19

FFmpeg内存对齐_数组_20

上图已经圈出来了 ​​av_image_fill_plane_sizes()​​ 函数的重点 :

1,​​sizes[0]​​ 直接就乘以 height。因为 ​​sizes[0]​​ 是亮度分量,他是不需要唯一的。

2,注意最后一个 for 的 ​​i​​ 变量,他是 从 1 开始的。 ​​(height + (1 << s) - 1) >> s​​ 实际上就是位移取整,跟之前类似。

最后 再把位移之后的 h 进行相乘。所以对于 ​​AV_PIX_FMT_YUV420P​​ ,16位 对齐的整个计算过程如下:

size = FFALIGN((width >> log2_chroma_w),16) * (height >> log2_chroma_h)

所以 上面代码中的 ​​sizes[4]​​ 就是全部的大小,因为宽高都用上了,而且只针对宽度进行对齐。

​av_image_get_buffer_size()​​ 函数最后就是对 ​​sizes[4]​​ 数组 求和,然后返回。

回到本文最开始的疑问,139200 字节是如何算出来的,计算公式如下:FFmpeg内存对齐_内存对齐_21首先,​​FFALIGN(300,16) * 300​​ 是亮度分量的,然后 ​​*2​​ 是因为 UV 是一样大小。

300 按 16 字节对齐是 304,300 右移 1 位 等于 150,150 按 16 字节对齐,就等于 160。FFmpeg内存对齐_字节对齐_22


因此 ​​av_image_get_buffer_size()​​ 函数的真正作用,他的对齐参数,实际上就是对 ​​width​​​ 进行对齐,不会对 ​​height​​ 进行对齐。

本文讲解完毕,再抛另一个问题,内存对齐之后,会多出一些内存,这个会不会影响实际存储的大小。因为 YUV 是需要发给编码器进行编码的,对齐内存之后会不会导致编码器 输出的 ​​AVPacket​​ 的大小也变大了呢?这个请看《FFmpeg内存对齐2》


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  xfEU0RRJpeyr   2023年11月02日   57   0   0 初始化linux数组
  xfEU0RRJpeyr   2023年11月02日   76   0   0 初始化数组赋值
  b1UHV4WKBb2S   2023年11月13日   30   0   0 阴影模糊数组
  nQkVcpdWfLDr   2023年11月13日   42   0   0 数组sort函数python
  M9aMEIE19lAW   2023年11月13日   38   0   0 php数组
1bTlX33AWdQ4