优化32位值构造

问题描述 投票:1回答:5

所以,我有以下代码:

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检查?

c optimization bit-manipulation
5个回答
2
投票

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

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

这使用!x每当swap为真时swap评估为0的事实,并且当!!x为假时x评估为1,即使x本身不是1,a评估为1,即使b本身不是1.乘以结果选择ab酌情。

但请注意,您现在可以使用多个逻辑和算术运算,而不是一个比较和分支。完全不清楚这会在实践中提供性能改进。


由@ChristianGibbons提供:

[假设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); } 保证非负且小于216,]您可以通过删除按位AND组件并将乘法应用于移位而不是参数来简化此方法:

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

1
投票

我们没有太多优化

这里有两个版本

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

生成的代码几乎相同。

lib目录下:

https://godbolt.org/z/F4zOnf

GCC:

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

a

正如你看到clang喜欢工会,gcc转变。


1
投票

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

b

编译器实际上甚至都没有使用乘法指令,因为这里唯一的乘法是2的幂,所以它只使用一个简单的左移来构造将用于移位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 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 的值。

使用Clang -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   

使用Clang -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   

用gcc -O2拆卸原始版本

a

用gcc -O2拆卸新版本

b

编辑:正如约翰·博林格所指出的那样,这个解决方案是在uint8_t shift_mask = (uint8_t) !swap * 16; val = ((uint32_t) (a & 0xFFFF) << (shift_mask)) | ((uint32_t) (b & 0xFFFF) << ( 16 ^ shift_mask )); 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) 是无符号值的假设下编写的,这使得位屏蔽变得多余。如果要将此方法与32位以下的有符号值一起使用,则需要修改:

-O3

我不会在这个版本的反汇编中走得太远,但这里是-O2的clang输出:

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 

为了回应关于性能与他的联合解决方案的P__J__,这里是clang在This上发布的代码版本,该代码可以安全地处理签名类型:

val = swap ? ((uint32_t)a & 0x0000ffff) | ((uint32_t)b << 16) : ((uint32_t)b & 0x0000ffff) | ((uint32_t)a << 16);

它在总指令中更接近联合解决方案,但不使用SHRD,根据-O3的回答,它需要4个时钟才能在intel skylake处理器上执行并耗尽多个操作单元。我会有点好奇他们每个人的实际表现如何。


0
投票
GCC

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


0
投票

Clang编译。 and?:对64位处理器的策略略有不同。 GCC使用分支生成代码,而Clang将运行两个分支,然后使用条件移动。 GCC和Clang都将生成“零延伸短整数”指令,而不是qazxswpoi。

使用qazxswpoi并没有改变生成的代码。

Clang版本看起来效率更高。

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

© www.soinside.com 2019 - 2024. All rights reserved.