让 gcc 将一系列 BYTE 比较转换为 WORD/DWORD/QWORD

Posted

技术标签:

【中文标题】让 gcc 将一系列 BYTE 比较转换为 WORD/DWORD/QWORD【英文标题】:Make gcc turn a sequence of BYTE comparisons into WORD/DWORD/QWORD instead 【发布时间】:2017-02-02 13:29:04 【问题描述】:

我目前有以下:

#include <cstdint>
#include <memory>

template<typename T>
bool isZeroed(T const & num) 
    void const * ptr = std::addressof(num);
    uint8_t const * pos = static_cast<uint8_t const *>(ptr);
    uint8_t const * const endpos = pos + sizeof(T);
    for (;pos < endpos; ++pos)
    
      if (*pos != uint8_t(0)) 
        return false;
    
    return true;


int main(int argc, char * argv[])

  return isZeroed(static_cast<uint64_t>(argc));

在 gcc 7 上使用 -O3 生成:

main:
        movsx   rdi, edi
        test    dil, dil
        mov     QWORD PTR [rsp-8], rdi
        jne     .L9
        cmp     BYTE PTR [rsp-7], 0
        jne     .L9
        cmp     BYTE PTR [rsp-6], 0
        jne     .L9
        cmp     BYTE PTR [rsp-5], 0
        jne     .L9
        cmp     BYTE PTR [rsp-4], 0
        jne     .L9
        cmp     BYTE PTR [rsp-3], 0
        jne     .L9
        cmp     BYTE PTR [rsp-2], 0
        jne     .L9
        cmp     BYTE PTR [rsp-1], 0
        sete    al
.L2:
        movzx   eax, al
        ret
.L9:
        xor     eax, eax
        jmp     .L2

见https://godbolt.org/g/HWB3is

在我的脑海中,我认为应该可以将这些 BYTE 比较折叠成一次占用更多字节的比较,例如 WORD/DWORD/QWORD。

有人知道我在代码中做了什么阻止优化器这样做,或者这在 gcc 中是不可能的吗?

【问题讨论】:

我认为编译器忘记了所有字节都是有效的,因此采取措施不访问额外的内存,除非是实际需要的。如果pos[0] 不为零,则源代码不会访问pos[1-7],并且编译器会确保该行为。 PS:int 通常不是 64 位的,但可能在您的环境中。无论如何,32 位的代码也是类似的。 所以我需要故意访问更大块的内存,因为编译器也在保留内存访问模式。 是的,编译器正在做你告诉它做的事情,你想让它做一些不同的事情,告诉它...... 这不是UB吗?通过不兼容类型的指针专门访问对象(例如*pos)。为了不那么标准,结构可以有填充,并且填充不需要为零。 @MargaretBloom 这可能不是未定义的行为,因为允许通过unsigned char 指针访问任何类型的对象,而uint8_t 几乎必须是unsigned char 或不存在。示例中使用的类型 uint64_t 不能有任何填充位,但一般来说,给定的类型,而不仅仅是结构,是否有填充是实现定义的。 【参考方案1】:

考虑到 Jester 的评论,我可以执行以下操作。

#include <cstdint>
#include <memory>

template<typename CmpSizeType>
bool isZeroed(void const * & pos, size_t & bytesLeft)  
  while (bytesLeft >= sizeof(CmpSizeType))
  
    CmpSizeType const * posOfSize = static_cast<CmpSizeType const *>(pos);
    if ( *posOfSize != CmpSizeType(0)) return false;
    pos = posOfSize + 1;
    bytesLeft -= sizeof(CmpSizeType);
  
  return true;


template<typename T>
bool isZeroed(T const & num)   
  size_t bytesLeft = sizeof(T);
  void const * pos = std::addressof(num);
  if(!isZeroed<uint64_t>(pos,bytesLeft)) return false;
  if(!isZeroed<uint32_t>(pos,bytesLeft)) return false;
  if(!isZeroed<uint8_t>(pos,bytesLeft)) return false;
  return true;


struct T
  int8_t  a1,b1,c1;
  int16_t a2;
  int32_t a3,b3,c3,c4;
  int64_t a4;
  int16_t a5;
;

int main(int argc, char * argv[])

  return 
    isZeroed(T 0,0,0,0,argc,0,0,0,0,0 ) &&
    isZeroed(static_cast<int8_t>(argc));

改为提供 QWORD 比较:

main:
        pxor    xmm0, xmm0
        movaps  XMMWORD PTR [rsp-56], xmm0
        mov     DWORD PTR [rsp-48], edi
        cmp     QWORD PTR [rsp-48], 0
        movaps  XMMWORD PTR [rsp-40], xmm0
        jne     .L6
        cmp     QWORD PTR [rsp-40], 0
        jne     .L6
        cmp     QWORD PTR [rsp-32], 0
        jne     .L6
        xor     eax, eax
        test    dil, dil
        sete    al
        ret
.L6:
        xor     eax, eax
        ret

【讨论】:

GCC 在这里很愚蠢,明确地将零与零进行比较。 Clang 可以更好地编译它 - 但在某种程度上违背了测试的目的。 您的代码无法处理模板参数T 的对齐方式可能不如uint64_tuint32_t 严格的情况。它也不处理 T 可能有填充的情况。编译器可能会生成代码,其中 T 0,0,0,0,argc,0,0,0,0,0 创建的对象的第四个字节(以及许多其他字节)未初始化。【参考方案2】:

当您告诉编译器查看 BYTE 大小的块时,它会这样做。如果您希望它查看 DWORD 或 QWORD 大小的块,您需要要求它这样做。类似于以下内容:

template<typename T>
bool isZeroed(T const & num) 
    void const * ptr = std::addressof(num);
    uint8_t const * pos = static_cast<uint8_t const *>(ptr);
    uint8_t const * const endpos = pos + sizeof(T);
    for (;pos < endpos; pos += sizeof(uint64_t))
    
      if (*reinterpret_cast<const uint64_t*>(pos) != 0) 
        return false;
    
    for (;pos < endpos; ++pos)
    
      if (*pos != uint8_t(0))
        return false;
    
    return true;

我们首先循环比较 QWORD 大小的块与零。如果找到任何非零值,则其中一个 BYTE 必须是非零值,因此返回 false。然后,我们再次循环处理任何剩余的 BYTE 大小的块。

这会产生您想要的代码。实际上,更好的是——编译器知道你正在向模板函数传递一个 QWORD 大小的值,所以它只使用了 TEST 指令:

main:
    xor     eax, eax
    test    edi, edi
    sete    al
    ret

如果您使用 GCC 扩展并传递一个 128 位整数值,则该函数将编译为以下内容,它会按预期进行两次 QWORD 比较:

main:
    movsx   rdi, edi
    cmp     QWORD PTR [rdi], 0
    jne     .L3
    cmp     QWORD PTR [rdi+8], 0
    sete    al
.L2:
    movzx   eax, al
    ret
.L3:
    xor     eax, eax
    jmp     .L2

请注意,这是非常不安全的,这在reinterpret_cast 中应该很明显。我也质疑这个的效用。为所有整数大小编写模板特化——没有那么多,你会得到更好的代码而不影响安全性。或者,如果您正在测试任意内存块,请使用 memcmp——例如

template<typename T>
bool isZeroed(T const & num) 
    void const * ptr = std::addressof(num);
    uint8_t temp[sizeof(T)] =  ;
    return memcmp(temp, ptr, sizeof(T));

【讨论】:

以上是关于让 gcc 将一系列 BYTE 比较转换为 WORD/DWORD/QWORD的主要内容,如果未能解决你的问题,请参考以下文章

将一系列字典转换为 DataFrame - Pandas

将一系列字符串转换为日期[重复]

如何将一系列数组转换为 pandas/numpy 中的单个矩阵?

如何将一系列记录转换为 SQL 中该范围之后的记录值?

如何将一系列int转换为用于变量的字符串?

java中如何让byte[]与string类型转换后,保持不变