バイトの 2 つの半分の 4 ビット (16 エントリ) ルックアップ テーブルは、適切なトレードオフのように見えます (@Aki がコメントで指摘しているように)。
AVR 命令はそれぞれ 2 バイトであるため、16 バイトのテーブルは 8 命令と同じスペースを消費します。(おそらく、配列を 256 バイト単位でアラインしてインデックス作成を gcc よりもはるかに安価にできる場合を除いて、速度やサイズの点で価値がないことがわかります。)
各バイトの上位半分と下位半分を使用して、LUT をパックすることは可能です。ただし、それは、テーブル サイズ (8 バイト = 4 命令) を節約するよりも、インデックス作成 (インデックスのビット 4 で分岐を使用して、マスクする前に条件付きで SWAP を使用) に多くのコストがかかります。
AVR GCC の機能を比較してみましょう。gcc4.6 では、Brett のコードをコンパイルするときに、驚くほどひどい最適化の失敗があります (実際にint
は、結果を uint8_t に切り捨てて、その利点を十分に活用していません)。皮肉なことに、SWAP 命令を使用してローテーションに最適化します。x<<4 | x>>4
(AVR には複数カウントのローテート命令がなく、通常のローテートはローテート スルー キャリーです。シフトは命令ごとに 1 つのカウントでのみ使用できます。)
#include <stdint.h>
uint8_t reverse_byte_alu (uint8_t x)
{
uint8_t xeven = x & 0x55, xodd = x & 0xaa;
x = (xeven << 1) | (xodd >> 1); // swap adjacent bit pairs
xeven = x & 0x33, xodd = x & 0xcc;
x = (xeven << 2) | (xodd >> 2); // swap adjacent 2-bit chunks
x = ((x << 4) | (x >> 4)); // 4-bit rotate is recognized as SWAP
return x;
}
gcc4.6 -O3
(Godbolt コンパイラ エクスプローラ)でこの asm にコンパイルします。見逃された最適化は見られません。
reverse_byte_alu:
mov r25,r24
andi r25,lo8(85)
lsl r25
andi r24,lo8(-86)
lsr r24
or r25,r24 # swap of adjacent bits done
mov r24,r25
andi r24,lo8(51)
lsl r24
lsl r24 # << 2
andi r25,lo8(-52)
lsr r25
lsr r25 # >> 2
or r24,r25 # swap pairs of bits done
swap r24 # swap nibbles
ret
16 命令、各 2 バイト、すべて 1 サイクル。(除くret
) https://www.microchip.com/webdoc/avrassemblyr/avrassemblyr.wb_instruction_list.html
したがって、これは、を含まない16のシングルサイクル命令を必要とする@JimmyBの回答よりもわずかに優れています。(ただし、小さなループに巻き上げることができます)。ret
AVR では、配列のインデックス作成は安価ではありません。アドレッシング モードの唯一の選択肢は、ポスト インクリメントまたはプリ デクリメントであり、即時置換はありません。したがって、16 ビットの配列アドレスを 4 ビットのニブル値に追加する必要があります。配列がアドレス空間の下位 256 バイトにある場合、これは安価になる可能性があります。または、配列が 256 バイトにアラインされている場合は、ポインタ レジスタの上位バイトを設定し、ルックアップ値を下位バイトに入れることができます。(gcc はこの最適化を見逃しています)。
配列を 16 バイト境界に整列するように gcc に指示すると、アドレス計算が安価になりますが、命令の総数は 18 であり、そのうちのいくつかはマルチサイクル命令です。
__attribute__((aligned(16))) // makes indexing cheaper
static const uint8_t reverse_nibble[16] = {
0, 0b1000, 0b0100, 0b1100,
0b0010, 0b1010, 0b0110, 0b1110,
0b0001, 0b1001, 0b0101, 0b1101,
0b0011, 0b1011, 0b0111, 0b1111
};
uint8_t reverse_byte_nibble_LUT (uint8_t x) {
uint8_t hi = reverse_nibble[x>>4];
hi = ((hi << 4) | (hi >> 4)); // SWAP instead of SWAP+AND for just a shift
uint8_t lo = reverse_nibble[x & 0x0f];
return hi | lo;
}
17 命令にコンパイルされ、非インクリメント/デクリメント アドレッシング モードで FLASH にアクセスする場合、LD は 2 サイクルの命令です。(フラッシュまたは内部 SRAM にアクセスしていない場合、一部の CPU では 1 サイクルです)。
# gcc4.6 output, not optimal
mov r26,r24
swap r26
andi r26,lo8(15)
ldi r27,lo8(0)
subi r26,lo8(-(reverse_nibble)) # AVR doesn't have add-immediate byte, only sub
sbci r27,hi8(-(reverse_nibble)) # X register = (x>>4) - (-LUT_base)
ld r25,X
swap r25
mov r30,r24
ldi r31,lo8(0)
andi r30,lo8(15)
andi r31,hi8(15) # missed opt, this is a double-redundant 0 & 0
subi r30,lo8(-(reverse_nibble))
sbci r31,hi8(-(reverse_nibble))
ld r24,Z
or r24,r25
ret
ldi r27, 0
/sbci r27
はおそらく最適化の失敗です。16 バイトのアラインされたテーブルでは、上位バイトにキャリーを取得できません。私たちはできると思います:
# generate r30 = x&0x0f
subi r30,lo8(-(reverse_nibble)) # ORI would work, too. no-op with 256-byte aligned table
ldi r31,hi8(reverse_nibble) # reuse this for hi and lo
したがって、これはインデックス作成の改善により速度が向上する可能性がありますが、合計サイズ (コード + テーブル) は明らかに悪くなります。