为什么 Clang 为引用和非空指针参数生成不同的代码?

ben*_*nrg 9 c++ x86-64 clang pass-by-reference language-lawyer

这与为什么 GCC 无法为两个 int32 的结构生成最佳运算符==有关?. 我正在玩 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;
}
Run Code Online (Sandbox Code Playgroud)

https://godbolt.org/z/e49h6d

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

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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

    mov     al, 1
    cmp     dword ptr [rdi], 0
    je      .LBB0_1
    ret
.LBB0_1:
    cmp     dword ptr [rdi + 4], 0
    setne   al
    ret
Run Code Online (Sandbox Code Playgroud)

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???
Run Code Online (Sandbox Code Playgroud)

但如果规范暗示了这一点,我不知道如何。

Nat*_*dge 1

我相信,从标准 C++ 的角度来看,编译器可以为两者发出相同的代码,因为标准中没有像您构造的那样提供“部分对象”的规定。事实上,这可能只是一个错过的优化。

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

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