C++17 std::byte 使用 GCC 中的标准算法生成优化程度较低的代码

5 c++ gcc x86-64 memcmp std-byte

我真的很喜欢std::byte作为一种独特的类型,它实现了 C++ 语言定义中指定的字节概念。我不喜欢的是,现代 C++ 编译器将使用标准算法生成优化程度较低的代码。

在这里,我正在使用一个检查标头中前 4 个字节的函数,您可以在Godbolt上关注我的代码片段

bool func_bytes(const std::array<std::byte, 1024>& buf) {
    constexpr std::array<std::byte, 4> header {
        std::byte{0xDE}, std::byte{0xAD}, std::byte{0xBE}, std::byte{0xAF}
    };
    return std::equal(header.begin(), header.end(), buf.begin());
}
Run Code Online (Sandbox Code Playgroud)

这将在 x86-64 gcc trunk 上生成以下程序集

func_bytes(std::array<std::byte, 1024ul> const&):
        cmp     BYTE PTR [rdi], -34
        jne     .L5
        cmp     BYTE PTR [rdi+1], -83
        jne     .L5
        cmp     BYTE PTR [rdi+2], -66
        jne     .L5
        cmp     BYTE PTR [rdi+3], -81
        sete    al
        ret
.L5:
        xor     eax, eax
        ret
Run Code Online (Sandbox Code Playgroud)

如果我将 替换std::byteunsigned char,那么编译器将优化为仅进行dword比较。

bool func_chars(const std::array<unsigned char, 1024>& buf) {
    constexpr std::array<unsigned char, 4> header {0xDE, 0xAD, 0xBE, 0xAF};
    return std::equal(header.begin(), header.end(), buf.begin());
}
Run Code Online (Sandbox Code Playgroud)

这是生成的组件

.LC0:
        .string "\336\255\276\257"
func_chars(std::array<unsigned char, 1024ul> const&):
        mov     eax, DWORD PTR [rdi]
        cmp     DWORD PTR .LC0[rip], eax
        sete    al
        ret
Run Code Online (Sandbox Code Playgroud)

我对优化std::byte版本的解决方案是使用老朋友memcmp()memcpy(),它被翻译成编译器的内置。

bool func_bytes_memcmp(const std::array<std::byte, 1024>& buf) {
    constexpr std::array<std::byte, 4> header {
        std::byte{0xDE}, std::byte{0xAD}, std::byte{0xBE}, std::byte{0xAF}
    };
    return 0==std::memcmp(header.data(), buf.data(), header.size());
}
Run Code Online (Sandbox Code Playgroud)

它产生最小的代码!

func_bytes_memcmp(std::array<std::byte, 1024ul> const&):
        cmp     DWORD PTR [rdi], -1346458146
        sete    al
        ret
Run Code Online (Sandbox Code Playgroud)

这是现代 C++ 方法吗?

use*_*522 10

std::equal这是标准库中实现的质量问题,而不是编译器本身的质量问题(我认为)。

请注意,在编译器资源管理器上,GCC 和 Clang 默认都使用 libstdc++ 作为标准库实现。

它有一个通用的实现,std::equal它使用一个循环在范围内线性迭代,如果元素比较失败则短路。当编译器编译此循环时,它无法用单个比较替换单个字节访问,因为它会破坏短路,可能会在不应该执行的字节上执行加载。

我并不完全确定是否确实存在不发明负载的架构理由,或者是否只是因为性能优势不明确而没有这样做。据我所知,只要我们知道可访问的地址(因为它是 C++ 对象在其生命周期中的一部分),发明负载就没有任何问题。但在进行这些优化之前,高级信息可能不会出现。

如果值类型是“简单”,则还有一个专门化的std::equal转发,但目前仅考虑整数和指针。std::memcmp它应该包括std::byte.

这已经在 GCC 的 bug 跟踪器上报告了但似乎没有收到任何回复。std::byte尽管如此,我认为这应该很容易解决,并且我认为为此目的考虑简单也没有任何问题。

请注意,其他标准库实现可能没有memcmp针对任何类型的优化。std::byte例如,使用 libc++ Clang 会生成带有和 的非最佳代码unsigned char

顺便提一句。header您可以通过创建数组来进一步改进编译器输出static。然后它将产生与该memcmp版本相同的输出。我实际上没有看到它加载字符串而不是使用立即 without 的任何特殊原因static,但static每次调用该函数都应该有一个header具有不同地址的不同实例。至少在内联之前,需要进行复制,因为形成了指向数组的指针,并且编译器不确定是否std::equal不会比较指向header.


作为一种解决方法,我会在比实际标准库实现更多的情况下编写自己的转发实现std::equalmemcmp然后我也会让我的常量static constexpr来帮助编译器一点。但如果这是一次性的情况,我认为您memcmp直接使用的方法没有任何问题。