优化 32 位值构造

Posted

技术标签:

【中文标题】优化 32 位值构造【英文标题】:optimize 32-bit value construction 【发布时间】:2019-04-22 19:09:44 【问题描述】:

所以,我有以下代码:

uint32_t val;
if (swap) 
   val = ((uint32_t)a & 0x0000ffff) | ((uint32_t)b << 16);
 else 
   val = ((uint32_t)b & 0x0000ffff) | ((uint32_t)a << 16);

有没有办法对其进行优化,并让swap检查以某种方式嵌入到语句中?

【问题讨论】:

什么决定了swap 为什么你认为目前的版本不够理想? ? operator 怎么样?请记住,这根本不会优化运行时间。 swap 可能是编译时间常数或在一组特定工作过程中建立一次的东西。例如,可能正在解释从小端或大端系统接收到的数据。很少有人解释来自包含 little-endian 和 big-endian 数据的单个流的数据。因此,优化此代码的策略是创建两组代码,其中不包含swap 测试,每种情况一组代码。在编译时或开始工作时选择一个,视情况而定。 @EricPostpischil 没有。想象一下带有不同传感器的 RS485 网络。有些是大端有些小。您需要根据传感器读取来更正存储数据 【参考方案1】:

如果目标是避免分支,那么你可以这样写:

val = ((!!swap) * (uint32_t)a + (!swap) * (uint32_t)b) & 0x0000ffff)
        | (((!!swap) * (uint32_t)b + (!swap) * (uint32_t)a) << 16);

这使用了这样一个事实:只要swap 为真,!x 的计算结果为 0,而只要swap 为假,!!x 的计算结果为 1,即使 x 为真,!!x 的计算结果也为 1,即使 @987654327 @ 本身可能不是 1。乘以结果会根据需要选择 ab

但是请注意,您现在拥有多个逻辑和算术运算,而不是一个比较和分支。目前还不清楚这是否会在实践中提供性能改进。


感谢@ChristianGibbons:

[假设ab 保证为非负且小于216,]您可以通过删除按位与组件并将乘法应用于转移而不是参数:

val = ((uint32_t) a << (16 * !swap)) | ((uint32_t)b << (16 * !!swap));

这有更好的机会胜过原始代码(但仍不能确定这样做),但在这种情况下,更公平的比较将是与依赖于相同属性的原始代码版本输入:

uint32_t val;
if (swap) 
   val = (uint32_t)a | ((uint32_t)b << 16);
 else 
   val = (uint32_t)b | ((uint32_t)a << 16);

【讨论】:

它在 Clang 上运行得相当好,而 GCC 完全错过了它......也有 4 个 imuls:F 糟糕,我删除了我的评论,因为我进一步改进了它并将其变成了答案。 不用担心,@ChristianGibbons,感谢您的评论,以证明我抄袭了评论,而不是您的回答。【参考方案2】:

我们没有太多需要优化的地方

这里有两个版本

typedef union

    uint16_t u16[2];
    uint32_t u32;
D32_t;


uint32_t foo(uint32_t a, uint32_t b, int swap)

    D32_t da = .u32 = a, db = .u32 = b, val;

    if(swap)
    
        val.u16[0] = da.u16[1];
        val.u16[1] = db.u16[0];
    
    else
    
        val.u16[0] = db.u16[1];
        val.u16[1] = da.u16[0];
    

    return val.u32;



uint32_t foo2(uint32_t a, uint32_t b, int swap)

    uint32_t val;
    if (swap) 
    
        val = ((uint32_t)a & 0x0000ffff) | ((uint32_t)b << 16);
     
    else 
    
        val = ((uint32_t)b & 0x0000ffff) | ((uint32_t)a << 16);
    

    return val;

生成的代码几乎相同。

叮当声:

foo:                                    # @foo
        mov     eax, edi
        test    edx, edx
        mov     ecx, esi
        cmove   ecx, edi
        cmove   eax, esi
        shrd    eax, ecx, 16
        ret
foo2:                                   # @foo2
        movzx   ecx, si
        movzx   eax, di
        shl     edi, 16
        or      edi, ecx
        shl     esi, 16
        or      eax, esi
        test    edx, edx
        cmove   eax, edi
        ret

gcc:

foo:
        test    edx, edx
        je      .L2
        shr     edi, 16
        mov     eax, esi
        mov     edx, edi
        sal     eax, 16
        mov     ax, dx
        ret
.L2:
        shr     esi, 16
        mov     eax, edi
        mov     edx, esi
        sal     eax, 16
        mov     ax, dx
        ret
foo2:
        test    edx, edx
        je      .L6
        movzx   eax, di
        sal     esi, 16
        or      eax, esi
        ret
.L6:
        movzx   eax, si
        sal     edi, 16
        or      eax, edi
        ret

https://godbolt.org/z/F4zOnf

如您所见,clang 喜欢联合,gcc 转变。

【讨论】:

@AnttiHaapala 我很惊讶 gcc 在使用联合时会生成如此糟糕的代码。 直到现在我才知道 SHRD 的存在。【参考方案3】:

与 John Bollinger 避免任何分支的回答类似,我想出了以下方法来尝试减少执行的操作量,尤其是乘法。

uint8_t shift_mask = (uint8_t) !swap * 16;
val = ((uint32_t) a << (shift_mask)) | ((uint32_t)b << ( 16 ^ shift_mask  ));

实际上,两个编译器都没有使用乘法指令,因为这里唯一的乘法是 2 的幂,所以它只使用简单的左移来构造用于移位 ab 的值。

用 Clang -O2 拆解原件

0000000000000000 <cat>:
   0:   85 d2                   test   %edx,%edx
   2:   89 f0                   mov    %esi,%eax
   4:   66 0f 45 c7             cmovne %di,%ax
   8:   66 0f 45 fe             cmovne %si,%di
   c:   0f b7 c0                movzwl %ax,%eax
   f:   c1 e7 10                shl    $0x10,%edi
  12:   09 f8                   or     %edi,%eax
  14:   c3                      retq   
  15:   66 66 2e 0f 1f 84 00    data16 nopw %cs:0x0(%rax,%rax,1)
  1c:   00 00 00 00 

用 Clang -O2 反汇编新版本

0000000000000000 <cat>:
   0:   80 f2 01                xor    $0x1,%dl
   3:   0f b6 ca                movzbl %dl,%ecx
   6:   c1 e1 04                shl    $0x4,%ecx
   9:   d3 e7                   shl    %cl,%edi
   b:   83 f1 10                xor    $0x10,%ecx
   e:   d3 e6                   shl    %cl,%esi
  10:   09 fe                   or     %edi,%esi
  12:   89 f0                   mov    %esi,%eax
  14:   c3                      retq   
  15:   66 66 2e 0f 1f 84 00    data16 nopw %cs:0x0(%rax,%rax,1)
  1c:   00 00 00 00 

用 gcc -O2 反汇编原版

0000000000000000 <cat>:
   0:   84 d2                   test   %dl,%dl
   2:   75 0c                   jne    10 <cat+0x10>
   4:   89 f8                   mov    %edi,%eax
   6:   0f b7 f6                movzwl %si,%esi
   9:   c1 e0 10                shl    $0x10,%eax
   c:   09 f0                   or     %esi,%eax
   e:   c3                      retq   
   f:   90                      nop
  10:   89 f0                   mov    %esi,%eax
  12:   0f b7 ff                movzwl %di,%edi
  15:   c1 e0 10                shl    $0x10,%eax
  18:   09 f8                   or     %edi,%eax
  1a:   c3                      retq   

用 gcc -O2 反汇编新版本

0000000000000000 <cat>:
   0:   83 f2 01                xor    $0x1,%edx
   3:   0f b7 c6                movzwl %si,%eax
   6:   0f b7 ff                movzwl %di,%edi
   9:   c1 e2 04                shl    $0x4,%edx
   c:   89 d1                   mov    %edx,%ecx
   e:   83 f1 10                xor    $0x10,%ecx
  11:   d3 e0                   shl    %cl,%eax
  13:   89 d1                   mov    %edx,%ecx
  15:   d3 e7                   shl    %cl,%edi
  17:   09 f8                   or     %edi,%eax
  19:   c3                      retq   

编辑: 正如 John Bollinger 指出的那样,这个解决方案是在假设 ab 是无符号值的情况下编写的,从而使位掩码变得多余。如果此方法用于 32 位以下的有符号值,则需要修改:

uint8_t shift_mask = (uint8_t) !swap * 16;
val = ((uint32_t) (a & 0xFFFF) << (shift_mask)) | ((uint32_t) (b & 0xFFFF) << ( 16 ^ shift_mask  ));

我不会过多介绍这个版本的反汇编,但这是 -O2 处的 clang 输出:

0000000000000000 <cat>:
   0:   80 f2 01                xor    $0x1,%dl
   3:   0f b6 ca                movzbl %dl,%ecx
   6:   c1 e1 04                shl    $0x4,%ecx
   9:   0f b7 d7                movzwl %di,%edx
   c:   d3 e2                   shl    %cl,%edx
   e:   0f b7 c6                movzwl %si,%eax
  11:   83 f1 10                xor    $0x10,%ecx
  14:   d3 e0                   shl    %cl,%eax
  16:   09 d0                   or     %edx,%eax
  18:   c3                      retq   
  19:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)

为了回应 P__J__ 关于性能与联合解决方案的对比,下面是 clang 在-O3 吐出的对于处理带符号类型安全的代码版本:

0000000000000000 <cat>:
   0:   85 d2                   test   %edx,%edx
   2:   89 f0                   mov    %esi,%eax
   4:   66 0f 45 c7             cmovne %di,%ax
   8:   66 0f 45 fe             cmovne %si,%di
   c:   0f b7 c0                movzwl %ax,%eax
   f:   c1 e7 10                shl    $0x10,%edi
  12:   09 f8                   or     %edi,%eax
  14:   c3                      retq   
  15:   66 66 2e 0f 1f 84 00    data16 nopw %cs:0x0(%rax,%rax,1)
  1c:   00 00 00 00 

它在总指令上更接近联合解决方案,但不使用 SHRD,根据This 的回答,在英特尔 Skylake 处理器上执行需要 4 个时钟,并使用多个操作单元。我有点好奇他们每个人的实际表现如何。

【讨论】:

正如我在自己的回答中提到的,这种方法假设ab 输入都保证为非负且小于2^16。鉴于正在执行的任务的明显性质,这似乎是合理的,在这种情况下,原始帖子中的按位与是不必要的。但如果这保证,那么这将无法正常工作。 @JohnBollinger 关于签名假设的要点。我将我的测试代码写入了一个参数列表为(uint16_t a, uint16_t b, _Bool swap) 的函数;我肯定做了一个假设,这是打算与无符号 16 位值一起使用的。我将进行编辑以澄清假设。 @P__J__ 这是否考虑到每个操作可能需要的时钟数?我快速搜索了一下 SHRD 操作,第一个结果是关于 SO 的一个问题,关于 SHRD 有多慢。另外,我注意到您使用的是-O3。当我碰到O3时,我发现我的拆卸完全不同。【参考方案4】:
val = swap ? ((uint32_t)a & 0x0000ffff) | ((uint32_t)b << 16) : ((uint32_t)b & 0x0000ffff) | ((uint32_t)a << 16);

这将实现您要求的“嵌入”。但是,我不推荐这样做,因为它会使可读性变差并且没有运行时优化。

【讨论】:

? 不会改变任何关于生成代码效率的东西 正如我在回答中所说的那样。我相信我的帖子回答了 OP 的问题,正如他/她所说的那样。它当然不会改变运行时间或任何效率。【参考方案5】:

使用-O3 编译。 GCC 和 Clang 对 64 位处理器的策略略有不同。 GCC 生成带有分支的代码,而 Clang 将运行两个分支,然后使用条件移动。 GCC 和 Clang 都将生成 “零扩展 short to int” 指令,而不是 and

使用?: 也没有改变生成的代码。

Clang 版本似乎更高效。

总而言之,两者都会生成相同的代码如果你不需要交换。

【讨论】:

以上是关于优化 32 位值构造的主要内容,如果未能解决你的问题,请参考以下文章

是否有用于 16 和/或 32 位值的 memset() 函数?

Qt winId() 强制 32 位值

精通MATLAB最优化计算的实 例 目 录

带窗口函数的简单SQL查询优化

斜率优化DP

使用 SPI 从 STM32 上的磁性编码器传输和读取 16 位值