"saturation( (signed short)a + (unsigned char b) )"をさらに最適化

やべ、最適化がおもしろい(笑)。
前回のコードは直感的に美しくないと思っていたんだけど(ステータスレジスタをいじってるあたりとか)、やっぱりさらに短かくすることができた。


static int add_pixels_clamped_ARM_new(short *block, unsigned char *dest, int line_size)
{
asm volatile (
"mov r10, #8 \n\t"

"add_pixels_clamped_ARM_new_loop: \n\t"

/* load dest */
"ldr r4, [%[pdest] ] \n\t"
/* block[0] and block[1]*/
"ldrsh r5, [%[pblock] ] \n\t"
"ldrsh r7, [%[pblock], #2] \n\t"
"and r6, r4, #0xFF \n\t"
"and r8, r4, #0xFF00 \n\t"
"add r6, r5, r6 \n\t"
"add r8, r7, r8, lsr #8 \n\t"
"mvn r5, r5 \n\t"
"mvn r7, r7 \n\t"
"tst r6, #0x100 \n\t"
"movne r6, r5, lsr #24 \n\t"
"tst r8, #0x100 \n\t"
"movne r8, r7, lsr #24 \n\t"
"mov r9, r6 \n\t"
"ldrsh r5, [%[pblock], #4] \n\t" /* moved form [A] */
"orr r9, r9, r8, lsl #8 \n\t"
/* block[2] and block[3] */
/* [A] */
"ldrsh r7, [%[pblock], #6] \n\t"
"and r6, r4, #0xFF0000 \n\t"
"and r8, r4, #0xFF000000 \n\t"
"add r6, r5, r6, lsr #16 \n\t"
"add r8, r7, r8, lsr #24 \n\t"
"mvn r5, r5 \n\t"
"mvn r7, r7 \n\t"
"tst r6, #0x100 \n\t"
"movne r6, r5, lsr #24 \n\t"
"tst r8, #0x100 \n\t"
"movne r8, r7, lsr #24 \n\t"
"orr r9, r9, r6, lsl #16 \n\t"
"ldr r4, [%[pdest], #4] \n\t" /* moved form [B] */
"orr r9, r9, r8, lsl #24 \n\t"
/* store dest */
"ldrsh r5, [%[pblock], #8] \n\t" /* moved form [C] */
"str r9, [%[pdest] ] \n\t"

/* load dest */
/* [B] */
/* block[4] and block[5] */
/* [C] */
"ldrsh r7, [%[pblock], #10] \n\t"
"and r6, r4, #0xFF \n\t"
"and r8, r4, #0xFF00 \n\t"
"add r6, r5, r6 \n\t"
"add r8, r7, r8, lsr #8 \n\t"
"mvn r5, r5 \n\t"
"mvn r7, r7 \n\t"
"tst r6, #0x100 \n\t"
"movne r6, r5, lsr #24 \n\t"
"tst r8, #0x100 \n\t"
"movne r8, r7, lsr #24 \n\t"
"mov r9, r6 \n\t"
"ldrsh r5, [%[pblock], #12] \n\t" /* moved from [D] */
"orr r9, r9, r8, lsl #8 \n\t"
/* block[6] and block[7] */
/* [D] */
"ldrsh r7, [%[pblock], #14] \n\t"
"and r6, r4, #0xFF0000 \n\t"
"and r8, r4, #0xFF000000 \n\t"
"add r6, r5, r6, lsr #16 \n\t"
"add r8, r7, r8, lsr #24 \n\t"
"mvn r5, r5 \n\t"
"mvn r7, r7 \n\t"
"tst r6, #0x100 \n\t"
"movne r6, r5, lsr #24 \n\t"
"tst r8, #0x100 \n\t"
"movne r8, r7, lsr #24 \n\t"
"orr r9, r9, r6, lsl #16 \n\t"
"add %[pblock], %[pblock], #16 \n\t" /* moved from [E] */
"orr r9, r9, r8, lsl #24 \n\t"
"subs r10, r10, #1 \n\t" /* moved from [F] */
/* store dest */
"str r9, [%[pdest], #4] \n\t"

/* [E] */
/* [F] */
"add %[pdest], %[pdest], %[line_size] \n\t"
"bne add_pixels_clamped_ARM_new_loop \n\t"
:
: [pblock] "r"(block),
[pdest] "r"(dest),
[line_size] "r"(line_size)
: "r4", "r5", "r6", "r7", "r8", "r9", "r10", "cc", "memory" );
}

結果、オリジナルから3%の速度向上。fps換算で1.2fps向上。これなら時間を掛けた価値があるというもの。

自分でアルゴリズムを忘れそうなのでメモしとこう。

1要素を演算するコードの最適化していないC版。


short block[64];
unsigned char dest[64];
int a;

a = *block + *dest;
if (a < 0)
a = 0;
if (a > 255)
a = 255;
*dest = a;

最適化版。ストア/ロード以外は4命令。これ以上速くするのは無理、と思う。


1: ldr r4, [%[pdest] ]
2: ldrsh r5, [%[pblock] ]
3: and r6, r4, #0xFF
4: add r6, r5, r6
5: mvn r5, r5
6: tst r6, #0x100
7: movne r6, r5, lsr #24
8: mov r9, r6

pblockがsigned short[64]へのポインタで、IDCTを行なった結果が入っている。この要素の値域を-256〜255(16bit中9bitしか使われない)であると限定するのがポイント。規格的にどうなのかわからないが、一通り再生してみた感じでは、この値域を外れる値は出力されない模様。

1行目。pdestから4要素をまとめてr4にロード。

2行目。pblockから1要素を符号拡張してr5にロード。*pblockが-2(0xFFFE)だとしたら、r5には0xFFFFFFFEが入る。

3行目。r4から1要素だけ取りだす。2要素目なら0xFF00でマスクして、4行目でr4を参照する際にlsr #8する。

4行目。普通に加算する。この時点では飽和のことは無視。

5行目。もうr5は使わないので、7行目のために全ビットを反転させて、飽和用の値を作り出す。

6行目。4行目で加算した結果に注目。負数を加算してアンダーフローした場合は31〜9ビット目は問答無用で1になる。また、取り得る値の正数同士を加算し、255を越えた場合(0x100〜0x1FE)は必ず9ビット目が1になる。飽和させる必要が無い場合は0になる。よって9ビット目をテストすることで、飽和処理が必要なのか判断できる。

7行目。6行目のテストの結果、飽和が必要なときだけ実行される。飽和した結果は0x00か0xFFの二通りしかない。あとはどちらの値を代入するかだが、減算飽和するのは*pblockが負数であった場合のみである。逆に*pblockが正数であればかならず加算飽和になる。*pblockの符号は最上位ビットで判断できるが、値域が-256〜255なので上位23ビットはすべて1、すべて0しかあり得ない。よって*pblockのビットを反転させたあと、上位8ビットを24ビット右シフトした結果が飽和結果として利用できる。

8行目。結果を格納。


あとは2要素の演算をそれぞれ別のレジスタセットで行ない交互に混ぜることにより、結果を格納したレジスタを直後に参照することによるサイクルペナルティを避ける。そしてループ展開。