【问题标题】:Why does Clang generate different code for reference and non-null pointer arguments?为什么 Clang 为引用和非空指针参数生成不同的代码?
【发布时间】:2021-05-23 16:14:02
【问题描述】:

这与Why can't GCC generate an optimal operator== for a struct of two int32s? 有关。我在 godbolt.org 上玩这个问题的代码,发现了这种奇怪的行为。

struct Point {
    int x, y;
};

bool nonzero_ptr(Point const* a) {
    return a->x || a->y;
}

bool nonzero_ref(Point const& a) {
    return a.x || a.y;
}

https://godbolt.org/z/e49h6d

对于nonzero_ptr,clang -O3(所有版本)产生这个或类似的代码:

    mov     al, 1
    cmp     dword ptr [rdi], 0
    je      .LBB0_1
    ret
.LBB0_1:
    cmp     dword ptr [rdi + 4], 0
    setne   al
    ret

这严格实现了 C++ 函数的短路行为,仅当 x 字段为零时才加载 y 字段。

对于nonzero_ref,clang 3.6 及更早版本生成与nonzero_ptr 相同的代码,但clang 3.7 到11.0.1 生成

    mov     eax, dword ptr [rdi + 4]
    or      eax, dword ptr [rdi]
    setne   al
    ret

无条件加载y。当参数是指针时,没有任何版本的 clang 愿意这样做。为什么?

我能想到的唯一情况(在 x64 平台上)分支代码的行为会明显不同,这是在 [rdi+4] 处没有映射内存时,但我仍然不确定为什么 clang 会考虑这种情况对于指针而不是引用很重要。我最好的猜测是,有一些语言法律论点认为引用必须是“完整对象”,而指针不一定是:

char* p = alloc_4k_page_surrounded_by_guard_pages();
int* pi = reinterpret_cast<int*>(p + 4096 - sizeof(int));
Point* ppt = reinterpret_cast<Point*>(pi);  // ok???
ppt->x = 42;  // ok???
Point& rpt = *ppt;  // UB???

但如果规范暗示,我不知道如何。

【问题讨论】:

标签: c++ clang language-lawyer x86-64 pass-by-reference


【解决方案1】:

这是一个错过的优化;无分支代码对于两个 C++ 源版本都是安全的。

Why is gcc allowed to speculatively load from a struct? GCC 实际上通过指针推测性地加载两个结构成员,即使 C 源代码只引用一个或另一个。因此,至少 GCC 开发人员在他们对 C 和 C++ 标准的解释中认为这种优化是 100% 安全的(我认为这是故意的,而不是错误)。 Clang 生成一个 0 或 1 索引来选择要加载哪个 int,因此 clang 仍然像您的情况一样不愿意发明负载。 (C 与 C++:有或没有 -xc 的相同 asm,源版本移植为:https://godbolt.org/z/6oPKKd


您的 asm 中的明显区别是指针版本避免访问 a-&gt;y 如果 a-&gt;x != 0,并且这只对正确性很重要1 如果 a-&gt;y 在未映射的页面中;你是对的,这是相关的极端案例。

但 ISO C++ 不允许部分对象。您示例中的页面边界设置是我很确定未定义的行为。在读取a-&gt;x 的执行路径中,编译器可以假设读取a-&gt;y 也是安全的。

这当然不是int *p;p[0] || p[1] 的情况,因为它是完全有效的隐式长度0 终止数组恰好是1 个元素长,在页面的最后 4 个字节。

正如@Nate 在 cmets 中建议的那样,也许 clang 在优化时根本没有利用 ISO C++ 事实;当它考虑这种“if-conversion”类型的优化(分支到无分支)时,它可能会在内部转换为更像数组的东西。或者也许 LLVM 只是不让自己通过指针发明负载。

它总是可以为引用 args 执行此操作,因为引用保证非 NULL。调用者执行 nonzero_ref(*ppt) 将是“更多”UB,就像在您的部分对象示例中一样,因为在 C++ 术语中,我们取消引用指向整个对象的指针。


一个实验:取消引用指针以获得完整的 tmp 对象

bool nonzero_ptr_full_deref(Point const* pa) {
    Point a = *pa;
    return a.x || a.y;
}

https://godbolt.org/z/ejrn9h - 无分支编译,与nonzero_ref 相同。不知道这告诉我们什么/多少。这是我所期望的,因为它可以在 C++ 源代码中有效地无条件访问a-&gt;y


脚注 1:与所有主流 ISA 一样,x86-64 不进行硬件竞争检测,因此加载另一个线程正在编写的内容的可能性可能只对性能很重要,而且只有在因为我们已经在读取一个成员,所以完整的结构被分割在一个缓存行边界上。如果对象不跨越缓存行,则任何错误共享性能影响都已经产生。

像这样制作 asm 不会“引入 data-race UB”,因为 x86 asm 对这种可能性具有明确定义的行为,这与 ISO C++ 不同。 asm 适用于从 [rdi+4] 加载的任何可能值,因此它正确实现了 C++ 源代码的语义。发明读取是线程安全的,与写入不同,并且是允许的,因为它不是volatile,因此访问不是可见的副作用。唯一的问题是指针是否必须指向一个完全有效的Point 对象。

部分数据竞争(在非atomic 对象上)是未定义行为,是为了允许在硬件上使用竞争检测进行 C++ 实现。另一个是允许编译器假设重新加载他们曾经访问过的东西是安全的,并期望相同的值,除非在两点之间有获取或 seq_cst 加载。如果第二次加载与第一次不同,甚至编写会崩溃的代码。在这种情况下,这无关紧要,因为我们不是在谈论将 1 访问变为 2(而不是 0 变为 1,其值可能无关紧要),但这就是为什么滚动你自己的原子(例如在 Linux 内核中)需要使用 @987654343 @ 转换为 ACCESS_ONCE (https://lwn.net/Articles/793253/#Invented%20Loads)。

【讨论】:

  • 两个版本都没有优化吗?也就是说,您不想在&t:xorl %eax, %eax; cmpq $0, (%rdi); setnz %al 吗?似乎节省了一个完整的负载,并没有看到它会如何破坏任何东西nonzero_ref isnt。
  • @Noah:是的,这可能会更好,除非如果两个结构成员分别编写或跨越缓存行边界拆分,则可能会在存储转发停顿上。 (那么如果 OoO exec 可以隐藏这种延迟,它会花费更多的延迟,但不一定是吞吐量。)这在 cmets 和原始问题的答案Why can't GCC generate an optimal operator== for a struct of two int32s? 中进行了详细讨论。
  • @Noah:另外,你可以通过使用cmp %rax, (%rdi) 来节省代码大小,因为你只是对它进行了异或归零。无论哪种方式仍然可以微融合 Intel 上的 load+cmp,但如果那是 RIP 相对寻址模式(用于静态数据),immediate + rip-rel 不能微融合,因此寄存器实际上会更好。如果您在 cmp 结果上使用分支而不是 setnz,则 cmp %reg, (%reg) / jne 可以微 + 宏熔断,而 cmpq $0, (%reg) / jne 只能微熔而不是宏熔断。 (但是如果不做 setnz,你就不需要异或归零)。我真的应该写一篇关于 cmp mem 的规范
【解决方案2】:

我相信,从标准 C++ 的角度来看,编译器可以为两者发出相同的代码,因为标准中没有像您构建的那样对“部分对象”的规定。事实上,它不能简单地错过优化。

可以比较像a-&gt;x || b-&gt;y 这样的代码,其中编译器确实必须发出一个分支,因为只要a-&gt;x 不为零,调用者就可以合法地为b 传递一个空指针或无效指针。另一方面,如果a,b 是引用,那么根据标准,a.x || b.y 不需要分支,因为它们必须始终是对有效对象的引用。所以nonzero_ptr 中的“错过的优化”可能只是编译器没有注意到它可以利用a-&gt;xa-&gt;y 中的指针是同一个指针这一事实。

或者,clang 作为一个扩展,可能会尝试生成在您使用非标准功能创建只能访问某些成员的对象时仍然可以工作的代码。这适用于指针但不适用于引用的事实可能是该扩展的错误或限制,但我不认为这是任何形式的一致性违规。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-09-08
    • 2015-03-19
    • 2020-10-16
    • 1970-01-01
    • 2011-04-21
    • 2011-02-21
    • 2017-05-10
    • 2021-07-07
    相关资源
    最近更新 更多