如何反转一个字节

6

我是一名程序员,正在进行一个项目。有时候需要反转字节的顺序。我目前正在使用AVR Studio Mega32 微控制器。

例如:

0000 0001 becomes 1000 0000
0001 0110 becomes 0110 1000
1101 1001 becomes 1001 1011

开始之前,我有以下内容:
ldi r20,0b00010110

什么是最简单的方法来反转字节,使r20变成01101000?

3
请查看https://dev59.com/CFTTa4cB1Zd3GeqPpRo7,有一些解决方法。您还可以查看http://graphics.stanford.edu/~seander/bithacks.html#BitReverseObvious以获取更多反转字节的算法(代码是用C编写的,但应该很容易移植到汇编语言)。 - dwalter
5
对谁来说容易呢?个人而言,我会使用(table16[orig & 15]<<4) | table16[(orig>>4)]。这可以很容易地转换成汇编语言,表格只需进行一点点的头脑努力即可生成。此外,内存要求和速度在某种程度上保持了良好的平衡。 - Aki Suihkonen
@AkiSuihkonen:在AVR上,表索引不便宜,4位移位需要SWAP +掩码。因此,它可能比纯ALU版本稍微快一点。我把这个放在了答案中。 - Peter Cordes
5个回答

3
这是一段代码 - 它是为GNU工具链编写的(avr-gcc,binutils,avr-libc等) - 但它应该很容易适应:
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指令实现了最终的高低半字节交换。


AVR gcc可以识别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

2

目前我不能提供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)

2

另一种简单的方法是使用进位标志:

重复8次:

lsl r20 ; shift one bit into the carry flag
ror r0  ; rotate carry flag into result 

(输入在r20中,输出在r0中,r20的内容将被破坏; 寄存器可以自由更改。)

这需要16条指令@ 2字节,每个周期= 32字节的程序内存和16个周期来完全“展开”一个字节。如果放在循环中,代码大小可以缩小,但执行时间会增加。


1
一张包含16个条目的4位查找表可作为字节的两半部分的良好折衷方案(正如@Aki在评论中指出的那样)。
AVR指令每个2个字节,因此16字节的表格与8个指令的空间成本相同。(实际上,除非您可以通过256字节对齐数组来使索引比gcc更便宜,否则这似乎不值得用于速度或大小。)
可以使用每个字节的高位和低位来打包LUT。但是,相对于节省的表格大小(8字节=4条指令),这将在索引上花费更多(使用索引的第4位的分支条件ally SWAP before masking) 。

让我们来比较一下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。(但它可以被组合成一个小循环)。


AVR中的数组索引不是很便宜。唯一的寻址模式是后增或前减,没有立即位移。因此,必须将16位数组地址加上4位半字值。如果您的数组在低256字节的地址空间中,这可能更便宜。或者,如果您的数组是256字节对齐的,您可以只设置指针寄存器的高字节,并将查找值放在低字节中。(gcc错过了这个优化)。
告诉gcc将数组对齐到16字节边界会使地址计算更便宜,但总指令数为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个周期。(在某些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

因此,通过更好的索引可能会提高速度,但总大小(代码+表格)肯定会变差。


我不确定何时定下了我在答案中提供的代码片段,但是很久以前我在AVR LED驱动器中使用过它!中间结果有权被提升为“int”,而gcc-4.6现在已经过时了。我为AVR-GNU工具链保持非常及时的文档,所以一旦我有动力,我会看看avr-gcc-8.1.0能做什么。 - Brett Hale
@BrettHale:实际上我在现实生活中并不进行任何AVR开发,因此我没有在本地安装AVR编译器。这是一种有趣的架构(Godbolt上唯一一个int比寄存器小的架构),因此它是编译器和C类型提升规则的一个有趣的边角案例。gcc仔细应用as-if规则并优化掉对int的提升真的很重要。我希望Godbolt有更多更新的编译器,适用于多个非x86架构,特别是AVR,因为4.6版本太旧了。 - Peter Cordes

1

稍加调整,可以从我的评论中的代码片段中获得额外的性能。

当我们确保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

我们可以通过异或来生成地址 - 为什么不用“加”呢? - JimmyB
为什么不呢?理论上,编译器应该知道按位操作不会产生进位,这意味着低字节与高字节是隔离的。但是编译器无法理解使用异或或将0..15加到0xABC0时的情况。 - Aki Suihkonen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接