我是一名程序员,正在进行一个项目。有时候需要反转字节的顺序。我目前正在使用AVR Studio Mega32 微控制器。
例如:
0000 0001 becomes 1000 0000
0001 0110 becomes 0110 1000
1101 1001 becomes 1001 1011
开始之前,我有以下内容:
ldi r20,0b00010110
什么是最简单的方法来反转字节,使r20变成01101000?
static inline __attribute__ ((always_inline))
uint8_t avr_reverse_byte (uint8_t x)
{
x = ((x & 0x55) << 1) | ((x & 0xaa) >> 1);
x = ((x & 0x33) << 2) | ((x & 0xcc) >> 2);
/* x = ((x & 0x0f) << 4) | ((x & 0xf0) >> 4); */
__asm__ ("swap %0" : "=r" (x) : "0" (x)); /* swap nibbles. */
return x;
}
所以,与'C'代码相比,并没有太大的改进,除了用swap
指令实现了最终的高低半字节交换。
x = ((x << 4) | (x >> 4))
旋转习惯用语是否带有&0x0f
/ &0xf0
,并将其编译为swap
指令。您不需要内联asm。具有讽刺意味的是,函数的其余部分编译得非常糟糕。由于某种原因,如果您不显式转换第1个和第2个表达式中的|
操作数,则gcc无法将int
(来自整数提升)优化为实际的单寄存器操作,并使用2个寄存器https://godbolt.org/g/sYx1uP。(我还没有检查当前的gcc,因为Godbolt只有AVR gcc4.6。) - Peter Cordes目前我不能提供AVR代码。但是一般的位翻转技术如下:
abcd efgh p
badc fehg p = ((p and 0AAh) shr 1) or ((p shl 1) and 0AAh)
dcba hgfe p = ((p and 033h) shr 2) or ((p shl 2) and 033h)
hgfe dcba p = ((p and 00Fh) shr 4) or ((p shl 4) and 0F0h)
另一种简单的方法是使用进位标志:
重复8次:
lsl r20 ; shift one bit into the carry flag
ror r0 ; rotate carry flag into result
(输入在r20
中,输出在r0
中,r20
的内容将被破坏; 寄存器可以自由更改。)
这需要16条指令@ 2字节,每个周期= 32字节的程序内存和16个周期来完全“展开”一个字节。如果放在循环中,代码大小可以缩小,但执行时间会增加。
让我们来比较一下AVR GCC的行为。gcc4.6在编译Brett的代码时出现了令人惊讶的糟糕的错过优化(实际上将其提升为int
,而没有充分利用将结果截断为uint8_t
)。具有讽刺意味的是,它确实将x<<4 | x>>4
优化为使用SWAP指令进行旋转。(AVR没有多重计数旋转指令,普通的旋转是通过Carry位来实现的。移位只能使用每个指令一个计数。)
#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编译器浏览器)将其编译为此汇编代码。我没有看到任何未优化的地方。
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字节,全部都是单周期的(除了ret
)。https://www.microchip.com/webdoc/avrassembler/avrassembler.wb_instruction_list.html
所以这比@JimmyB的答案略微好一些,他需要16个单周期指令,不包括ret
。(但它可以被组合成一个小循环)。
__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个周期。(在某些CPU上,当不访问flash或内部SRAM时,只需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
因此,通过更好的索引可能会提高速度,但总大小(代码+表格)肯定会变差。
int
比寄存器小的架构),因此它是编译器和C类型提升规则的一个有趣的边角案例。gcc仔细应用as-if规则并优化掉对int
的提升真的很重要。我希望Godbolt有更多更新的编译器,适用于多个非x86架构,特别是AVR,因为4.6版本太旧了。 - Peter Cordes稍加调整,可以从我的评论中的代码片段中获得额外的性能。
当我们确保LUT对齐到16字节边界时,我们可以通过异或生成地址。此外,我们还可以通过索引将表格进行XOR运算,允许在原地修改参数x
。我已经注释掉了GCC生成的不必要指令。
__attribute__((aligned(16))) // makes indexing cheaper
static const uint8_t reverse_nibble_xor[16] = {
0 ^ 0, 1 ^ 0b1000, 2 ^ 0b0100, 3 ^ 0b1100,
4 ^ 0b0010, 5 ^ 0b1010, 6 ^ 0b0110, 7 ^ 0b1110,
8 ^ 0b0001, 9 ^ 0b1001, 10 ^ 0b0101, 11 ^ 0b1101,
12 ^ 0b0011, 13 ^ 0b1011, 14 ^ 0b0111, 15 ^ 0b1111
};
uint8_t reverse_ams(uint8_t x)
{
uint8_t *p = (uint8_t *)((uint16_t)reverse_nibble_xor ^ (x & 15));
x ^= p[0];
x = ((x << 4) | (x >> 4));
uint8_t *q = (uint8_t *)((uint16_t)reverse_nibble_xor ^ (x & 15));
x ^= q[0];
return x;
}
reverse_ams:
ldi r18,lo8(reverse_nibble)
// ldi r19,hi8(reverse_nibble)
ldi r31,hi8(reverse_nibble) // use r31 directly instead of r19
mov r30,r24
// ldi r31,lo8(0)
andi r30,lo8(15)
// andi r31,hi8(15)
eor r30,r18
// eor r31,r19
ld r25,Z
eor r25,r24
swap r25
mov r30,r25
// ldi r31,lo8(0)
andi r30,lo8(15)
// andi r31,hi8(15)
eor r30,r18
// eor r31,r19
ld r24,Z
eor r24,r25
ret
(table16[orig & 15]<<4) | table16[(orig>>4)]
。这可以很容易地转换成汇编语言,表格只需进行一点点的头脑努力即可生成。此外,内存要求和速度在某种程度上保持了良好的平衡。 - Aki Suihkonen