现代 C++ 编译器的内存锯齿有多糟糕?

How bad is memory aliasing with modern C++ compilers?

提问人:GKann 提问时间:7/5/2023 最后编辑:GKann 更新时间:7/15/2023 访问量:142

问:

我正在尝试了解内存混叠的影响以及如何改进我的代码以避免它。我正在重写我的缓存一致性实体组件系统,我想考虑内存别名。

我的主要来源是Christer Ericson在GDC 2003上的演讲,因此我想知道他所描述的问题是否以某种方式被现代C++编译器缓解了。

具体来说,现代 C++ 编译器是否像 Christer 所说的那样遭受内存混叠的影响,尤其是对于成员变量访问(由于隐含的“this”ptr)?

#include <stdlib.h>

class TestList
{
  public:
    TestList()
    {
       // atoi here to avoid compiler optimization around hardcoded 20
       count = atoi("20");
       data = new int64_t[count];
    }

    int64_t count;
    int64_t* data;

    void ClearOptimized();
    void ClearNonOptimized();
};

// Not inlined on purpose
void TestList::ClearOptimized()
{
    // According to Christer, this avoids aliasing even for
    // simple compilers, because we are aiding the compiler
    // to identify that there is no aliasing in the iteration.
    for (int64_t i = 0, size = count; i < size; ++i)
    {
        data[i] = 0;
    }
}
void TestList::ClearNonOptimized()
{
    // According to Christer the compiler doesn't know
    // if 'count' is aliasing with data... A smart compiler
    // should be able to identify it might be aliased for
    // the first element only, but all other iterations can't
    // so it will unroll the first element of the loop into a
    // separated range check.
    for (int64_t i = 0; i < count; ++i)
    {
        data[i] = 0;
    }
}

int main()
{
    TestList listA;
    listA.ClearNonOptimized();
    TestList listB;
    listB.ClearOptimized();
    return listA.data[listA.count-1] + listB.data[listB.count-1];
}

我浏览了一些网站,这些网站表明现代编译器仍然存在大多数问题,尽管现在我们似乎有更好的工具来避免混叠(例如类型双关)。

我尝试通过使用上述代码查看编译器资源管理器来验证这一点。但是我发现很难推理汇编代码......具有最高优化标志的 GCC 和 Clang 似乎每次都在进行额外的 ptr 访问。

铛:

TestList::ClearOptimized():         # @TestList::ClearOptimized()
        push    rax
        mov     rdx, qword ptr [rdi]
        test    rdx, rdx
        jle     .LBB0_2
        mov     rdi, qword ptr [rdi + 8]
        shl     rdx, 3
        xor     esi, esi
        call    memset
.LBB0_2:
        pop     rax
        ret
TestList::ClearNonOptimized():      # @TestList::ClearNonOptimized()
        cmp     qword ptr [rdi], 0
        jle     .LBB1_3
        mov     rax, qword ptr [rdi + 8]
        xor     ecx, ecx
.LBB1_2:                                # =>This Inner Loop Header: Depth=1
        mov     qword ptr [rax + 8*rcx], 0
        add     rcx, 1
        cmp     rcx, qword ptr [rdi]     <=== Is this due to memory aliasing?
        jl      .LBB1_2
.LBB1_3:
        ret

海湾合作委员会:

TestList::ClearOptimized():
        mov     rdx, QWORD PTR [rdi]
        test    rdx, rdx
        jle     .L6
        mov     rdi, QWORD PTR [rdi+8]
        sal     rdx, 3
        xor     esi, esi
        jmp     memset
.L6:
        ret
TestList::ClearNonOptimized():
        cmp     QWORD PTR [rdi], 0
        jle     .L8
        mov     rdx, QWORD PTR [rdi+8]
        xor     eax, eax
.L10:
        mov     QWORD PTR [rdx+rax*8], 0
        add     rax, 1
        cmp     QWORD PTR [rdi], rax     <=== Is this due to memory aliasing?
        jg      .L10
.L8:
        ret

我没看错吗?这是否意味着它将获取缓存中的信息,而不是从寄存器中使用它?

C++ 编译器优化 严格别名 内存别名

评论

0赞 HolyBlackCat 7/5/2023
我不认为 C++20 在这方面改变了任何东西,除了将编译器已经在做的事情形式化。对于那些没有看过视频的人,您能详细说明一下您的问题吗?
1赞 Peter Cordes 7/5/2023
当 GCC 首次引入时,它甚至会打破最简单的情况,例如 .(另请参阅 gcc、严格锯齿和恐怖故事)。从那以后,GCC 不遗余力地减少对坏代码的敌意,通过简单的“明显”严格别名 UB 案例,经常检测坏的类型双关语并按照程序员的意图处理它们。(但显然代码应该使用 ,或在 C++20 中获取大小检查和 constexpr 兼容性。-fno-strict-aliasing*(uint32_t*)my_floatmemcpystd::bit_cast
0赞 Peter Cordes 7/5/2023
我不记得有任何情况,我们不能在避免严格混叠UB的同时获得同样高效的asm。尽管在某些情况下可能需要 GNU C 扩展,例如 (without ),因为没有高效未对齐负载的目标可能不会内联,这不需要对其操作数进行对齐。但在大多数现代目标上,如果 p 或 q 是本地临时地址,则 GCC 将内联到单个加载或存储。(GNU C++ 允许像 C99 那样使用不活跃的联合成员,这有时可能很方便。__attribute__((may_alias))aligned(1)memcpymemcpy(p, q, sizeof(int))
0赞 Peter Cordes 7/5/2023
无论如何,IDK如何回答“它有多糟糕”。如果你的意思是违反严格别名规则有多糟糕,那就是未定义的行为;您的代码可能会完全崩溃。别这样。如果你的意思是通过各种解决方法让编译器做你想做的事需要多少性能,那么它们应该编译成你希望从你正在考虑的任何别名冲突中获得的相同 asm。
0赞 GKann 7/5/2023
谢谢@PeterCordes,我会更好地阐述这个问题,并在今天晚些时候进行编辑。但这个想法是要问的是,现代编译器是否已经更好地识别了无法进行混叠的情况,就像在基于类型的别名分析(TBAA)中一样。我正在寻找的具体情况是,在写给类成员时,Ericson 指出成员变量是通过“this”指针访问的,这导致了隐式别名......我将更改问题以包含代码块。

答: 暂无答案