x264源码阅读笔记:宏的使用

x264 是H.264标准的一个开源编码器,也是目前被广泛使用的。它的代码主要用C写成,也有针对不同架构CPU写的汇编,因此可以达到很高的性能。AVS的参考软件代码实在太烂了,无力吐槽,看看 x264 的安抚下我被辣得不行的眼睛。。

而且“主要用C”,“效率高”,满足这些条件的代码一般都有很多tricks,一看果不其然。下面是几个例子,会不定期更新。

简化赋值操作

看这段帧内预测的代码: common/predict.c

H.264/AVC 中,帧内预测有九种模式,下面的函数就是将当前的8x8色度块设为其上边一行和左边一列像素的均值。

void x264_predict_8x8_dc_c( pixel *src, pixel edge[36] )
{
    PREDICT_8x8_LOAD_LEFT
    PREDICT_8x8_LOAD_TOP
    pixel4 dc = PIXEL_SPLAT_X4( (l0+l1+l2+l3+l4+l5+l6+l7+t0+t1+t2+t3+t4+t5+t6+t7+8) >> 4 );
    PREDICT_8x8_DC( dc );
}

代码只有四行,是因为用到了宏,把大量重复的代码做了抽象:

#define PL(y) \
    UNUSED int l##y = edge[14-y];
#define PT(x) \
    UNUSED int t##x = edge[16+x];
#define PREDICT_8x8_LOAD_TOPLEFT \
    int lt = edge[15];
#define PREDICT_8x8_LOAD_LEFT \
    PL(0) PL(1) PL(2) PL(3) PL(4) PL(5) PL(6) PL(7)
#define PREDICT_8x8_LOAD_TOP \
    PT(0) PT(1) PT(2) PT(3) PT(4) PT(5) PT(6) PT(7)
#define PREDICT_8x8_LOAD_TOPRIGHT \
    PT(8) PT(9) PT(10) PT(11) PT(12) PT(13) PT(14) PT(15)

#define PREDICT_8x8_DC(v) \
    for( int y = 0; y < 8; y++ ) { \
        MPIXEL_X4( src+0 ) = v; \
        MPIXEL_X4( src+4 ) = v; \
        src += FDEC_STRIDE; \
    }

这段宏定义中有几点需要说明:

  • 宏的 标志连接 (token concatenation)。即将 "##" 两边的标志合并为一个标志,这可以为数组中的每个元素起一个方便的名字。

  • 函数式的宏 (Function-like Macros)。这样的宏看起来就像在调用一个函数,其实是一段代码的别名,作用跟 inline 大致类似。注意多行代码每行都需要使用'\'对换行符进行转义。

  • UNUSED 的定义是 #define UNUSED attributeunused,是让编译器停止对未使用变量的警告。这是gcc的特性,如果是MSVC,那么这个宏就是空的。

  • PIXEL_SPLAT_X4 是把均值(0到255)复制四份,放到一个int中,也就是乘以了 0x0001000100010001,通过一次乘法,将一个值复制了四份。

  • MPIXEL_X4 是将四个像素作为一个联合体,这样每条int赋值语句可以同时给四个像素赋值。 代码见 common/common.h。本来需要8*8=64次的赋值操作,现在只需要一次乘法,还有(8/4)*8=16次赋值。

这里好像从代码中未能体现出 标志连接 的作用。再看另外一个函数,这个函数是水平方向的预测,每行的预测值都是对应的左边像素的值:

void x264_predict_8x8_h_c( pixel *src, pixel edge[36] )
{
    PREDICT_8x8_LOAD_LEFT
#define ROW(y) MPIXEL_X4( src+y*FDEC_STRIDE+0 ) =\
               MPIXEL_X4( src+y*FDEC_STRIDE+4 ) = PIXEL_SPLAT_X4( l##y );
    ROW(0); ROW(1); ROW(2); ROW(3); ROW(4); ROW(5); ROW(6); ROW(7);
#undef ROW
}

只需要把行数作为参数传给 “宏函数” ROW,它就自动将对应的 l0, l1, l2…​ 等值赋给了该行的8个像素。如果没有标志连接,可能每个标志都要定义一个宏才可行,否则就要用数组来参数化。而数组的元素写起来不方便(显然l0比l[0]更加方便),而且还多了一次内存访问。

再看下面这个函数,可能会对“l0比l[0]”更加方便有些体会,这个函数是对角线左下方向(DDL)的帧内预测:

static void x264_predict_8x8_ddl_c( pixel *src, pixel edge[36] )
{
    PREDICT_8x8_LOAD_TOP
    PREDICT_8x8_LOAD_TOPRIGHT
    SRC(0,0)= F2(t0,t1,t2);
    SRC(0,1)=SRC(1,0)= F2(t1,t2,t3);
    SRC(0,2)=SRC(1,1)=SRC(2,0)= F2(t2,t3,t4);
    SRC(0,3)=SRC(1,2)=SRC(2,1)=SRC(3,0)= F2(t3,t4,t5);
    SRC(0,4)=SRC(1,3)=SRC(2,2)=SRC(3,1)=SRC(4,0)= F2(t4,t5,t6);
    SRC(0,5)=SRC(1,4)=SRC(2,3)=SRC(3,2)=SRC(4,1)=SRC(5,0)= F2(t5,t6,t7);
    SRC(0,6)=SRC(1,5)=SRC(2,4)=SRC(3,3)=SRC(4,2)=SRC(5,1)=SRC(6,0)= F2(t6,t7,t8);
    SRC(0,7)=SRC(1,6)=SRC(2,5)=SRC(3,4)=SRC(4,3)=SRC(5,2)=SRC(6,1)=SRC(7,0)= F2(t7,t8,t9);
    SRC(1,7)=SRC(2,6)=SRC(3,5)=SRC(4,4)=SRC(5,3)=SRC(6,2)=SRC(7,1)= F2(t8,t9,t10);
    SRC(2,7)=SRC(3,6)=SRC(4,5)=SRC(5,4)=SRC(6,3)=SRC(7,2)= F2(t9,t10,t11);
    SRC(3,7)=SRC(4,6)=SRC(5,5)=SRC(6,4)=SRC(7,3)= F2(t10,t11,t12);
    SRC(4,7)=SRC(5,6)=SRC(6,5)=SRC(7,4)= F2(t11,t12,t13);
    SRC(5,7)=SRC(6,6)=SRC(7,5)= F2(t12,t13,t14);
    SRC(6,7)=SRC(7,6)= F2(t13,t14,t15);
    SRC(7,7)= F2(t14,t15,t15);
}

但我想 x264 给数组元素起名字的做法不只是为了方便,更重要的还是性能吧!

inline函数

common/common.h 中定义了取中位数的一个inline函数。由于它总是被强制inline,作用就相当于宏了。

static ALWAYS_INLINE int x264_median( int a, int b, int c )
{
    int t = (a-b)&((a-b)>>31);
    a -= t;
    b += t;
    b -= (b-c)&((b-c)>>31);
    b += (a-b)&((a-b)>>31);
    return b;
}

然而这个取中位数的函数(从名字上看)为什么这样写,似乎不那么显然。逐条语句分析下吧:

  • int t = (a-b)&((a-b)>>31);

    • (a-b)>>31 相当于 (a < b ? 1 : 0),那么这条语句其实可写作 int t = (a < b) ? ((a-b)&1) : 0; a < b且a与b的差值是奇数时,t=1,其余情况t均为0.

  • a -= t; b += t; 当t为0时,这两条语句没有作用。考虑当t=1,即“a<b且b-a为奇数”时,这两条语句使得 a,b 的差扩大了2,而且奇偶性互换。

  • b -= (b-c)&((b-c)>>31); “b<c且c-b为奇数”时,b-=1,否则b不变。

  • b += (a-b)&((a-b)>>31); “a<b且b-a为奇数”时,b+=1,否则b不变。

貌似还是分析不出来。额,举个例子代入看看:

static ALWAYS_INLINE int x264_median( int a, int b, int c )// (a, b, c) = (3, 9 ,7)
{
    int t = (a-b)&((a-b)>>31);// t = (-6)&1 = 0
    a -= t; // (3, 9, 7)
    b += t; // (3, 9, 7)
    b -= (b-c)&((b-c)>>31); // (3, 9, 7)
    b += (a-b)&((a-b)>>31); // (3, 9, 7)
    return b; // 9
}

不懂。。 额,那啥(玩儿脱了:|),看看 ALWAYS_INLINE 这个宏吧😂!

准备在stackoverflow上提问的时候,在电脑上运行了下,发现上面的分析是错误的,t并不是只取1,0,因为负数右移,使用1来填充,右移31位就是全1,与操作后,保持不变。如果a>=b,那么t是0;如果a<b,那么t的值是a-b,经过"a-=t;b+=t;"之后,a,b互换。前三条语句其实是把max(a,b)赋给a,min(a,b)赋给b。整个函数最终返回的是 min(max(a,b), max(min(a, b), c)) ,至于这个为什么是中值,额,非常不想把六种情况都带进去验证,肯定有更直观的解释!继续想!!

#if defined(__GNUC__) && (__GNUC__ > 3 || __GNUC__ == 3 && __GNUC_MINOR__ > 0)
#define UNUSED __attribute__((unused))
#define ALWAYS_INLINE __attribute__((always_inline)) inline
#define NOINLINE __attribute__((noinline))
#else
#ifdef _MSC_VER
#define ALWAYS_INLINE __forceinline
#define NOINLINE __declspec(noinline)
#else
#define ALWAYS_INLINE inline
#define NOINLINE
#endif
#define UNUSED
#endif

没啥可说的,看看怎么给不同的编译器添加特性的吧。 吐槽下VS的版本号:

MSVC++ 14.0 _MSC_VER == 1900 (Visual Studio 2015)
MSVC++ 12.0 _MSC_VER == 1800 (Visual Studio 2013)
MSVC++ 11.0 _MSC_VER == 1700 (Visual Studio 2012)
MSVC++ 10.0 _MSC_VER == 1600 (Visual Studio 2010)
MSVC++ 9.0  _MSC_VER == 1500 (Visual Studio 2008)
MSVC++ 8.0  _MSC_VER == 1400 (Visual Studio 2005)
MSVC++ 7.1  _MSC_VER == 1310 (Visual Studio 2003)
MSVC++ 7.0  _MSC_VER == 1300
MSVC++ 6.0  _MSC_VER == 1200
MSVC++ 5.0  _MSC_VER == 1100