这是一个最优化问题. 我想将一个包含6个5位元素的位域复制到U8缓冲区,简单的操作如下:

void Expand(u32 x, u8 b[6]) {
    b[0] = (x >> 0) & 31;
    b[1] = (x >> 5) & 31;
    b[2] = (x >> 10) & 31;
    b[3] = (x >> 15) & 31;
    b[4] = (x >> 20) & 31;
    b[5] = (x >> 25) & 31;
}

这是集合生成的百面旗帜,/O2 /Ot /Gr面、GCC和叮当会给出大致相同的东西.

@Expand@8 PROC
        mov     al, cl
        and     al, 31
        mov     BYTE PTR [edx], al
        mov     eax, ecx
        shr     eax, 5
        and     al, 31
        mov     BYTE PTR [edx+1], al
        mov     eax, ecx
        shr     eax, 10
        and     al, 31
        mov     BYTE PTR [edx+2], al
        mov     eax, ecx
        shr     eax, 15
        and     al, 31
        mov     BYTE PTR [edx+3], al
        mov     eax, ecx
        shr     eax, 20
        shr     ecx, 25
        and     al, 31
        and     cl, 31
        mov     BYTE PTR [edx+4], al
        mov     BYTE PTR [edx+5], cl
        ret     0
@Expand@8 ENDP

But I just don't like it; I know it does exactly what it should be doing, it just seems to me that it could be a lot more efficient.
To me it looks like a 30-bit number that needs to be scaled up to a 48-bit number while inserting zeroes.

                  11111 11111 11111 11111 11111 11111
                                                    ↓
00011111 00011111 00011111 00011111 00011111 00011111

我一直在try 移位、或运算,只在最后用U64(0x1f1f1f1f1f1f)进行AND运算,但我的优化努力仍然不成功.我相信这should是可行的,在不到10个说明,任何指导将不胜感激.

EDIT

我又抓挠了一下脑袋,到目前为止,这是我能想到的最好的:

void Expand(u32 x, u8 b[6]) {
    memset(b, 31, 6);
    b[0] &= x;
    b[1] &= x >>= 5;
    b[2] &= x >>= 5;
    b[3] &= x >>= 5;
    b[4] &= x >>= 5;
    b[5] &= x >>= 5;
}

编译为:

@Expand@8 PROC
        mov     eax, 0x1f1f1f1f
        mov     DWORD PTR [edx], eax
        mov     WORD PTR [edx+4], ax
        and     BYTE PTR [edx], cl
        shr     ecx, 5
        and     BYTE PTR [edx+1], cl
        shr     ecx, 5
        and     BYTE PTR [edx+2], cl
        shr     ecx, 5
        and     BYTE PTR [edx+3], cl
        shr     ecx, 5
        and     BYTE PTR [edx+4], cl
        shr     ecx, 5
        and     BYTE PTR [edx+5], cl
        ret     0
@Expand@8 ENDP

推荐答案

这里有一个跨平台的解决方案,它只需要一个几乎在所有桌面架构上都可以使用的快速乘法器

void Expand(uint32_t x, uint8_t b[6]) {
    uint32_t x024 = x & 0b00'00000'11111'00000'11111'00000'11111;
    uint32_t x135 = x & 0b00'11111'00000'11111'00000'11111'00000;
    uint64_t r024 = x024 * 0x0100'0000'4000'0010ULL & 0x1F001F001F000000;
    uint64_t r135 = x135 * 0x0040'0000'1000'0004ULL & 0x001F001F001F0000;
    uint64_t result = r024 | (r135 >> 11);
#if !BIG_ENDIAN
    result = htonll(result);
#endif
    memcpy(b, &result, 6);
}

有关详细的数学计算,请参见下文.它需要8-9次运算,并以2条平行链运行.您可以通过传递8字节数组而不是6字节,并在以后必要时恢复最后2个元素b[6]/b[7]来改进这一点.

但您应该真正使用#ifdef,并 for each 受支持的平台提供高效的实现,并为其他平台提供类似上面的备用通用解决方案.在x86上,最快的方式是SIMD或PDEP,这取决于您是针对大型数组执行此操作,还是仅偶尔执行此操作.所有其他平台也都有自己的SIMD,可以用来加速这一过程.或者,您也可以使用与平台无关的SIMD库来自动为任何架构发出高效的SIMD代码.


请注意这instruction count is not a measure for performance.并不是所有指令都是相同的.你的"最佳"实际上比第一个版本更可怕,因为它有一个很长的依赖链,而CPU可以同时启动5个独立的执行,并与后者并行运行

请记住,许多指令都很慢,所以多个更简单的等价指令会更快.可以并行执行的多个指令也会比具有依赖关系的较短序列更快.而短循环也比直接跑更糟糕


算法背后的数学原理

假设输入为00aaaaabbbbbcccccdddddeeeeefffff的32位.在屏蔽之后,乘法将在正确的位置产生位

                                  0000000bbbbb00000ddddd00000fffff (x024)
× 0000000100000000000000000000000001000000000000000000000000010000 (0x0100'0000'4000'0010)
  ────────────────────────────────────────────────────────────────
                              0000000bbbbb00000ddddd00000fffff
    0000000bbbbb00000ddddd00000fffff
+ 000fffff
  0000000100000000000000000000000001000000000000000000000000010000
  ────────────────────────────────────────────────────────────────
& 0001111100000000000111110000000000011111000000000000000000000000 (0x1F001F001F000000)
  ────────────────────────────────────────────────────────────────
= 000fffff00000000000ddddd00000000000bbbbb000000000000000000000000
                                  00aaaaa00000ccccc00000eeeee00000 (x135)
× 0000000001000000000000000000000000010000000000000000000000000100 (0x0040'0000'1000'0004)
  ────────────────────────────────────────────────────────────────
                                00aaaaa00000ccccc00000eeeee00000
+     00aaaaa00000ccccc00000eeeee00000
  eeeee00000
  ────────────────────────────────────────────────────────────────
& 11111000000000001111100000000000111110000000000000000            (0x001F001F001F0000)
  ────────────────────────────────────────────────────────────────
= eeeee00000000000ccccc00000000000aaaaa000000000000000000000000000

合并以上两个结果,我们得到000fffff000eeeee000ddddd000ccccc000bbbbb000aaaaa0000000000000000个结果,当在内存中存储为高字节顺序时,它将以正确的顺序包含预期的字节

Output assembly for comparison

有关算法的更多详细信息,请参阅How to create a byte out of 8 bool values (and vice versa)?

C++相关问答推荐

获取二维数组的最大元素

如何在C中只使用一个带双方括号([i][j])访问语法的malloc来分配动态大小的2d数组?

单指针和空参数列表之间的函数指针兼容性

在函数中使用复合文字来初始化C语言中的变量

C中是否有语法可以直接初始化一个常量文本常量数组的 struct 成员?

如何使fputs功能提示错误输入并要求用户重新输入.程序停止而不是请求新的输入

C语言中的strstr问题

Win32API Wizzard97 PropSheet_SetWizButton不工作

为什么memcpy进入缓冲区和指向缓冲区的指针工作相同?

函数的限制限定指针参数允许优化调用方函数吗?

如何编写一个for循环来计算C中各项的总和?

无法识别C编程语言的语法,如书中所示

哪个首选包含第三个库S头文件?#INCLUDE;文件名或#INCLUDE<;文件名&>?

传递给函数的 struct 中的数组

在我的函数中实现va_arg的问题

在C中定义函数指针?

使用共享变量同步多线程 C 中的函数

在链表中插入一个值

获取 struct 中匿名 struct 的大小

为什么使用 C 引用这个 char 数组会导致 Stack smasing?