为什么使用指向一个成员变量的指针调用函数会抑制其他成员变量的优化?

chr*_*nte 6 c++ optimization language-lawyer

考虑以下程序:

struct X {
    bool b;
    int y;
};

void f(int*);

bool test(X x) {
    if (!x.b) {
        return false;
    }
    f(&x.y);
    return x.b;
}
Run Code Online (Sandbox Code Playgroud)

看来声明

    return x.b;
Run Code Online (Sandbox Code Playgroud)

没有得到优化return true; 所以我的结论是,编译器必须假设可以通过这样做进行f(int*)修改:x.b

void f(int* p) {
    bool* q = reinterpret_cast<bool*>(p - 1);
    *q = false;
}
Run Code Online (Sandbox Code Playgroud)

f这是真正有效的 C++实现吗?或者换句话说,当指向成员的指针传递给不透明函数时,编译器是否必须始终假设整个对象可能会发生变化?

带有 clang-x86 trunk (-O1) 的编译器输出是这样的:

test(X):                              # @test(X)
        push    rax
        mov     qword ptr [rsp], rdi
        test    dil, dil
        je      .LBB0_1
        lea     rdi, [rsp + 4]
        call    f(int*)@PLT
        cmp     byte ptr [rsp], 0
        setne   al
        pop     rcx
        ret
.LBB0_1:
        xor     eax, eax
        pop     rcx
        ret
Run Code Online (Sandbox Code Playgroud)

在这里查看现场演示

我能想到的一种解释是,f如果我只像上面的示例中那样调用它,则给定的定义是有效的 C++,因此编译器必须假设这种可能性。

j6t*_*j6t 8

显示的实现f肯定是未定义的行为。然而,编译器不仅要遵守 C++ 语言,还要遵守允许更改的特定于平台的行为x.b

编译器看不到是如何f实现的。如果用汇编写呢?这样的实现当然可以做它想做的事,包括在x.b不违反任何规范和标准的情况下进行更改。因此,它必须假设最坏的情况并重新读取x.b


use*_*670 5

虽然所示的实现f会导致未定义的行为,但可以以正确的方式实现:

#include <type_traits>
#include <cstddef>

void f(int* p)
{
   static_assert(::std::is_standard_layout_v<X>);
   X * p_object{reinterpret_cast<X *>(reinterpret_cast<unsigned char *>(p) - offsetof(X, y))}; 
   p_object->b = false;
}
Run Code Online (Sandbox Code Playgroud)

因此编译器可能会假设对标准布局类型的对象的任何字段进行别名会导致整个对象的别名。

  • 此外,在实践中,任何现实生活中的编译器都应该假设对对象的任何字段(即使是非标准布局类型)进行别名也会导致整个对象的别名。例如,Boost 严重依赖于诸如 [parent_from_member](https://www.boost.org/doc/libs/1_82_0/boost/intrusive/detail/parent_from_member.hpp) 之类的东西。我认为不受控制的别名实际上是 C++ 程序性能的主要限制因素之一(例如,与 Fortran 相比,据我所知,编译器可以自由地假设没有别名)。 (2认同)