摘要
对于DCT函数,不同尺寸函数的实现存在较大的差异。因此在这一系列中,会分别讲述DCT函数不同尺寸的实现方法及原理。本文讲述的是4x4尺寸的内容。4x4尺寸较小,不会达到寄存器数量的极限,数据构造简单,实现相对较为容易,重点讲述一下原理。
正文
原理
DCT函数的实现原理其实是基于FFT的蝶形算法,一定程度上减少加法、乘法运算的次数。具体来看。
在H.265/HEVC中4x4整数DCT公式为:
Y = ( [ 64 64 64 64 83 36 − 36 − 83 64 − 64 − 64 64 36 − 83 83 − 36 ] X [ 64 83 64 36 64 36 − 64 − 83 64 − 36 − 64 83 64 − 83 64 − 36 ] ) ∗ 1 128 ∗ 1 128 Y=(\begin{bmatrix} 64&64&64&64\\ 83&36&-36&-83\\ 64&-64&-64&64\\ 36&-83&83&-36\\ \end{bmatrix}X\begin{bmatrix} 64&83&64&36\\ 64&36&-64&-83\\ 64&-36&-64&83\\ 64&-83&64&-36\\ \end{bmatrix})*\frac{1}{128}*\frac{1}{128} Y=(⎣⎢⎢⎡648364366436−64−8364−36−648364−8364−36⎦⎥⎥⎤X⎣⎢⎢⎡646464648336−36−8364−64−646436−8383−36⎦⎥⎥⎤)∗1281∗1281
其中还是分为列变换和行变换,两者使用的算法是完全相同的。单看右侧的行变换。以第一行作为例子。第一行的元素为 { c o l 0 , c o l 1 , c o l 2 , c o l 3 } \{col0, col1, col2, col3\} {col0,col1,col2,col3} ,行变换后得到四各系数。
v
a
l
0
=
64
∗
c
o
l
0
+
64
∗
c
o
l
1
+
64
∗
c
o
l
2
+
64
∗
c
o
l
3
val0 = 64*col0+64*col1+64*col2+64*col3
val0=64∗col0+64∗col1+64∗col2+64∗col3
v
a
l
1
=
83
∗
c
o
l
0
+
36
∗
c
o
l
1
−
36
∗
c
o
l
2
−
83
∗
c
o
l
3
val1 = 83*col0+36*col1-36*col2-83*col3
val1=83∗col0+36∗col1−36∗col2−83∗col3
v
a
l
2
=
64
∗
c
o
l
0
−
64
∗
c
o
l
1
−
64
∗
c
o
l
2
+
64
∗
c
o
l
3
val2 = 64*col0-64*col1-64*col2+64*col3
val2=64∗col0−64∗col1−64∗col2+64∗col3
v
a
l
3
=
36
∗
c
o
l
0
−
83
∗
c
o
l
1
+
83
∗
c
o
l
2
−
36
∗
c
o
l
3
val3 = 36*col0-83*col1+83*col2-36*col3
val3=36∗col0−83∗col1+83∗col2−36∗col3
其中有16次乘法操作和12次加法操作。但观察变换系数,其实其中是存在一定的规律的。可以将这一规律加以利用。将整个计算过程划分为两个步骤。
步骤一
c
o
l
0
+
c
o
l
3
col0+col3
col0+col3
c
o
l
0
−
c
o
l
3
col0-col3
col0−col3
c
o
l
1
+
c
o
l
2
col1+col2
col1+col2
c
o
l
1
−
c
o
l
2
col1-col2
col1−col2
步骤二
v
a
l
0
=
64
∗
(
c
o
l
0
+
c
o
l
3
)
+
64
∗
(
c
o
l
1
+
c
o
l
2
)
val0 = 64*(col0+col3)+64*(col1+col2)
val0=64∗(col0+col3)+64∗(col1+col2)
v
a
l
1
=
83
∗
(
c
o
l
0
−
c
o
l
3
)
+
36
∗
(
c
o
l
1
−
c
o
l
2
)
val1 = 83*(col0-col3)+36*(col1-col2)
val1=83∗(col0−col3)+36∗(col1−col2)
v
a
l
2
=
64
∗
(
c
o
l
0
+
c
o
l
3
)
−
64
∗
(
c
o
l
1
+
c
o
l
2
)
val2 = 64*(col0+col3)-64*(col1+col2)
val2=64∗(col0+col3)−64∗(col1+col2)
v
a
l
3
=
36
∗
(
c
o
l
0
−
c
o
l
3
)
−
83
∗
(
c
o
l
1
−
c
o
l
2
)
val3 = 36*(col0-col3)-83*(col1-col2)
val3=36∗(col0−col3)−83∗(col1−col2)
两个步骤共需要8次乘法和8次加法。这样看来就非常的明了。对于更大的尺寸也同样适用,只是需要更多的步骤。
实现
实现的关键还是在与数据的构造。当前使用的还是128位向量寄存器,MIPS指令集(msa1.0)。指令+数据,综合呈现处理过程。
函数参数
static void dct4_c(const int16_t* src, int16_t* dst, intptr_t srcStride)
首先使用vbld指令加载数据, 得到4个向量寄存器,低64位保存每行4个数据。然后使用insve指令,插入操作,得到2个向量寄存器。内容分别为。
这里统一一下寄存器的表述方法:
左侧为寄存器高位,右侧为低位。
r
o
w
n
[
c
o
l
m
]
row_n[col_m]
rown[colm]表示第n行的第m列元素,
c
o
l
n
[
r
o
w
m
]
col_n[row_m]
coln[rowm]则表示第n列第m行元素。我认为这种表述方法比较明确,直接使用
(
n
,
m
)
(n,m)
(n,m)表示n行m列,就相当混乱,难以理解。
{
r
o
w
1
[
c
o
l
3
,
c
o
l
2
,
c
o
l
1
,
c
o
l
0
]
,
r
o
w
0
[
c
o
l
3
,
c
o
l
2
,
c
o
l
1
,
c
o
l
0
]
}
\{row1[col3, col2,col1,col0], row0[col3,col2,col1,col0]\}
{row1[col3,col2,col1,col0],row0[col3,col2,col1,col0]}
{
r
o
w
3
[
c
o
l
3
,
c
o
l
2
,
c
o
l
1
,
c
o
l
0
]
,
r
o
w
2
[
c
o
l
3
,
c
o
l
2
,
c
o
l
1
,
c
o
l
0
]
}
\{row3[col3, col2,col1,col0], row2[col3, col2,col1,col0]\}
{row3[col3,col2,col1,col0],row2[col3,col2,col1,col0]}
得到初始数据后开始进行变换。首先进行右侧的行变换。之所以是这样的顺序是因为,load访存是以行的顺序加载数据。当前尺寸较小,可以一次性加载完。但处理后续尺寸时,由于寄存器数量的限制,并行度是受限的。可能无法同时保存所有的原始数据。因此,这种情况下只有完整的行数据,无完整列数据,也就只能进行行变换。而且即使可以保存所有原始数据,先进行行变换,也可以给整个变换过程更大的并行度调动范围,增大可控性。
首先构造步骤一的内容。要得到一行中各个元素的加减组合,依仗当前的数据结构是完全不可能的,需要进行结构调整。
使用shf混洗指令,以32位为混洗单位,简单来说就是交换一下中间的两个32位数据。得到
{
r
o
w
1
[
c
o
l
3
,
c
o
l
2
]
,
r
o
w
0
[
c
o
l
3
,
c
o
l
2
]
,
r
o
w
1
[
c
o
l
1
,
c
o
l
0
]
,
r
o
w
0
[
c
o
l
1
,
c
o
l
0
]
}
\{row1[col3, col2], row0[col3,col2], row1[col1, col0], row0[col1, col0]\}
{row1[col3,col2],row0[col3,col2],row1[col1,col0],row0[col1,col0]}
{
r
o
w
3
[
c
o
l
3
,
c
o
l
2
]
,
r
o
w
2
[
c
o
l
3
,
c
o
l
2
]
,
r
o
w
3
[
c
o
l
1
,
c
o
l
0
]
,
r
o
w
2
[
c
o
l
1
,
c
o
l
0
]
}
\{row3[col3, col2], row2[col3,col2], row3[col1, col0], row2[col1, col0]\}
{row3[col3,col2],row2[col3,col2],row3[col1,col0],row2[col1,col0]}
然后需要将第一个向量寄存器的高64位和第二个寄存器的低64位交换一下,将同一行的元素置于两个寄存器的对应位置。这里用到的指令是insve插入指令和pckod选择奇数位数据(64位),进行交错。得到
{
r
o
w
3
[
c
o
l
1
,
c
o
l
0
]
,
r
o
w
2
[
c
o
l
1
,
c
o
l
0
]
,
r
o
w
1
[
c
o
l
1
,
c
o
l
0
]
,
r
o
w
0
[
c
o
l
1
,
c
o
l
0
]
}
\{row3[col1, col0], row2[col1,col0], row1[col1, col0], row0[col1, col0]\}
{row3[col1,col0],row2[col1,col0],row1[col1,col0],row0[col1,col0]}
{
r
o
w
3
[
c
o
l
3
,
c
o
l
2
]
,
r
o
w
2
[
c
o
l
3
,
c
o
l
2
]
,
r
o
w
1
[
c
o
l
3
,
c
o
l
2
]
,
r
o
w
0
[
c
o
l
3
,
c
o
l
2
]
}
\{row3[col3, col2], row2[col3, col2], row1[col3, col2], row0[col3, col2]\}
{row3[col3,col2],row2[col3,col2],row1[col3,col2],row0[col3,col2]}
要得到 c o l 0 col0 col0和 c o l 3 col3 col3对应, c o l 1 col1 col1和 c o l 2 col2 col2对应还需要对第二个寄存器处理一下。还是使用shf混洗指令,16位数据为单位,得到第二个寄存器。
{ r o w 3 [ c o l 2 , c o l 3 ] , r o w 2 [ c o l 2 , c o l 3 ] , r o w 1 [ c o l 2 , c o l 3 ] , r o w 0 [ c o l 2 , c o l 3 ] } \{row3[col2, col3], row2[col2, col3], row1[col2, col3], row0[col2, col3]\} {row3[col2,col3],row2[col2,col3],row1[col2,col3],row0[col2,col3]}
数据构造完毕,进行加减操作。使用addv和subv指令,进行16位数据加减。得到
{
r
o
w
3
[
c
o
l
1
+
c
o
l
2
,
c
o
l
0
+
c
o
l
3
]
,
r
o
w
2
[
.
.
.
]
,
r
o
w
1
[
.
.
.
]
,
r
o
w
0
[
.
.
.
]
}
\{row3[col1+col2, col0+col3], row2[...], row1[...],row0[...]\}
{row3[col1+col2,col0+col3],row2[...],row1[...],row0[...]}
{
r
o
w
3
[
c
o
l
1
−
c
o
l
2
,
c
o
l
0
−
c
o
l
3
]
,
r
o
w
2
[
.
.
.
]
,
r
o
w
1
[
.
.
.
]
,
r
o
w
0
[
.
.
.
]
}
\{row3[col1-col2, col0-col3], row2[...], row1[...],row0[...]\}
{row3[col1−col2,col0−col3],row2[...],row1[...],row0[...]}
完成步骤一,开始步骤二的内容。观察当前数据结构,需要乘法、加减操作的数据是在同一寄存器的相邻位置。可以直接使用横向运算指令实现。当前设置了四个常量数据(16位),分别完成步骤二中的四个操作。在寄存器中表示为
c
o
n
0
=
{
64
,
64
,
64
,
64
,
64
,
64
,
64
,
64
}
con0=\{64,64,64,64,64,64,64,64\}
con0={64,64,64,64,64,64,64,64}
c
o
n
1
=
{
36
,
83
,
36
,
83
,
36
,
83
,
36
,
83
}
con1=\{36,83,36,83,36,83,36,83\}
con1={36,83,36,83,36,83,36,83}
c
o
n
2
=
{
−
64
,
64
,
−
64
,
64
,
−
64
,
64
,
−
64
,
64
}
con2=\{-64,64,-64,64,-64,64,-64,64\}
con2={−64,64,−64,64,−64,64,−64,64}
c
o
n
3
=
{
−
83
,
36
,
−
83
,
36
,
−
83
,
36
,
−
83
,
36
}
con3=\{-83,36,-83,36,-83,36,-83,36\}
con3={−83,36,−83,36,−83,36,−83,36}
使用dotp点乘指令,对应位置相乘,相邻位置相加。第一个寄存器和 c o n 0 con0 con0和 c o n 2 con2 con2点乘,第二个寄存器和 c o n 1 con1 con1和 c o n 3 con3 con3点乘。表示为
{
r
o
w
3
[
64
∗
(
c
o
l
1
+
c
o
l
2
)
+
64
∗
(
c
o
l
0
+
c
o
l
3
)
]
,
r
o
w
2
[
.
.
.
]
,
r
o
w
1
[
.
.
.
]
,
r
o
w
0
[
.
.
.
]
}
\{row3[64*(col1+col2)+64*(col0+col3)], row2[...],row1[...],row0[...]\}
{row3[64∗(col1+col2)+64∗(col0+col3)],row2[...],row1[...],row0[...]}
{
r
o
w
3
[
36
∗
(
c
o
l
1
−
c
o
l
2
)
+
83
∗
(
c
o
l
0
−
c
o
l
3
)
]
,
.
.
.
}
\{row3[36*(col1-col2)+83*(col0-col3)], ...\}
{row3[36∗(col1−col2)+83∗(col0−col3)],...}
{
r
o
w
3
[
(
−
64
)
∗
(
c
o
l
1
+
c
o
l
2
)
+
64
∗
(
c
o
l
0
+
c
o
l
3
)
]
,
.
.
.
}
\{row3[(-64)*(col1+col2)+64*(col0+col3)], ...\}
{row3[(−64)∗(col1+col2)+64∗(col0+col3)],...}
{
r
o
w
3
[
(
−
83
)
∗
(
c
o
l
1
−
c
o
l
2
)
+
36
∗
(
c
o
l
0
−
c
o
l
3
)
]
,
.
.
.
}
\{row3[(-83)*(col1-col2)+36*(col0-col3)], ...\}
{row3[(−83)∗(col1−col2)+36∗(col0−col3)],...}
最后使用vsrarin指令,集移位、舍入、截半与一身的指令,右移1位,完成行变换。具体为什么只右移1位,而之后的列变换右移8位。两者合起来并没有达到 1 128 ∗ 1 128 \frac{1}{128}*\frac{1}{128} 1281∗1281的效果。这里可能涉及到整个系统的设计,因为变换过程中的部分操作是交给量化过程完成的。原因的话我目前也不是非常清楚。
完成行变换后,当前的数据表达形式已经并不适用了,应该更新为行变换后,数据的形式。更新一下当前的数据结构。
第一个寄存器都是使用第一列的系数,因此得到的都是行变换后第一列的数据。同样第二个寄存器就是第二列,第三个寄存器第三列,第四个寄存器第四列。可以表述为
{
0
,
0
,
0
,
0
,
c
o
l
0
[
r
o
w
3
,
r
o
w
2
,
r
o
w
1
,
r
o
w
0
]
}
\{0,0,0,0,col0[row3,row2, row1, row0]\}
{0,0,0,0,col0[row3,row2,row1,row0]}
{
0
,
0
,
0
,
0
,
c
o
l
1
[
r
o
w
3
,
r
o
w
2
,
r
o
w
1
,
r
o
w
0
]
}
\{0,0,0,0,col1[row3,row2, row1, row0]\}
{0,0,0,0,col1[row3,row2,row1,row0]}
{
0
,
0
,
0
,
0
,
c
o
l
2
[
r
o
w
3
,
r
o
w
2
,
r
o
w
1
,
r
o
w
0
]
}
\{0,0,0,0,col2[row3,row2, row1, row0]\}
{0,0,0,0,col2[row3,row2,row1,row0]}
{
0
,
0
,
0
,
0
,
c
o
l
3
[
r
o
w
3
,
r
o
w
2
,
r
o
w
1
,
r
o
w
0
]
}
\{0,0,0,0,col3[row3,row2, row1, row0]\}
{0,0,0,0,col3[row3,row2,row1,row0]}
后续需要完成的是列变换,因此可以发现,当前的数据结构和进行行变换时的起始数据结构是一样的。因此整个操作过程和上边的完全相同,唯一的不同就是最后的vsrarin指令右移的是8位。现在直接给出结果。
{
c
o
l
3
[
64
∗
(
r
o
w
1
+
r
o
w
2
)
+
64
∗
(
r
o
w
0
+
r
o
w
3
)
]
,
c
o
l
2
[
.
.
.
]
,
c
o
l
1
[
.
.
.
]
,
c
o
l
0
[
.
.
.
]
}
\{col3[64*(row1+row2)+64*(row0+row3)], col2[...],col1[...],col0[...]\}
{col3[64∗(row1+row2)+64∗(row0+row3)],col2[...],col1[...],col0[...]}
{
c
o
l
3
[
36
∗
(
r
o
w
1
−
r
o
w
2
)
+
83
∗
(
r
o
w
0
−
r
o
w
3
)
]
,
.
.
.
}
\{col3[36*(row1-row2)+83*(row0-row3)], ...\}
{col3[36∗(row1−row2)+83∗(row0−row3)],...}
{
c
o
l
3
[
(
−
64
)
∗
(
r
o
w
1
+
r
o
w
2
)
+
64
∗
(
r
o
w
0
+
r
o
w
3
)
]
,
.
.
.
}
\{col3[(-64)*(row1+row2)+64*(row0+row3)], ...\}
{col3[(−64)∗(row1+row2)+64∗(row0+row3)],...}
{
c
o
l
3
[
(
−
83
)
∗
(
r
o
w
1
−
r
o
w
2
)
+
36
∗
(
r
o
w
0
−
r
o
w
3
)
]
,
.
.
.
}
\{col3[(-83)*(row1-row2)+36*(row0-row3)], ...\}
{col3[(−83)∗(row1−row2)+36∗(row0−row3)],...}
第一个寄存器使用第一行的数据,在最终的结果矩阵中就位于第一行的位置。第二个寄存器第二行,第三个第三行,第四个第四行。因此可以直接使用st指令存储,不需要再进行结构变换。至此完成整个过程。