gpt4 book ai didi

c++ - 如果RMW操作没有任何变化,是否可以针对所有内存顺序对其进行优化?

转载 作者:行者123 更新时间:2023-11-30 01:33:21 24 4
gpt4 key购买 nike

在C / C++内存模型中,编译器可以仅合并然后删除冗余/ NOP原子修改操作,例如:

x++,
x--;

甚至只是
x+=0; // return value is ignored

对于原子标量 x

这是为了保持顺序一致性还是内存顺序较弱?

(注意:对于较弱的内存命令仍然可以执行某些操作;对于轻松的内存命令,这里没有真正的问题。再次编辑:在这种特殊情况下,实际上没有一个严重的问题。请参阅我自己的答案。即使没有轻松,也可以清除。 )

编辑:

问题不在于特定访问的代码生成:如果我想在第一个示例中看到在英特尔上生成的两个 lock add,那么我会使 x变得易变。

问题是这些C / C++指令是否会产生任何影响: 编译器是否可以过滤并删除这些nul操作(不是宽松的顺序操作),作为一种从源到源的转换? (或从抽象树到抽象树的转换,可能在编译器的“前端”)

编辑2:

假设摘要:
  • 并非所有操作都放松
  • 没有什么是易变的
  • 原子对象实际上可以由多个函数和线程访问(没有不共享地址的自动原子)

  • 可选假设:

    如果需要,可以假定未使用原子的地址,所有访问均按名称进行,并且所有访问都具有一个属性:
  • 在任何地方,对该变量的无访问访问都具有轻松的加载/存储元素:所有加载操作应具有获取,所有存储应具有释放(因此所有RMW至少应为acq_rel)。
  • 或者,对于那些宽松的访问,访问代码不会出于更改目的而读取该值:放松的RMW不会进一步保存该值(并且不会测试该值来决定下一步做什么) )。换句话说,除非负载具有获取,否则数据或控件都不依赖于原子对象的值。
  • 或原子的所有访问都顺序一致。

  • 那是我对这些(我相信很常见)用例特别好奇。

    注意:当代码确保观察者具有相同的内存可见性时,即使使用宽松的内存顺序进行访问也不会被视为“完全放松”,因此对于(1)和(2)而言,这被认为是有效的:
    atomic_thread_fence(std::memory_order_release);
    x.store(1,std::memory_order_relaxed);

    因为内存可见性至少与 x.store(1,std::memory_order_release);一样好

    这被认为对(1)和(2)有效:
    int v = x.load(std::memory_order_relaxed);
    atomic_thread_fence(std::memory_order_acquire);

    为了同样的原因。

    这很愚蠢,对于(2)来说微不足道( i只是 int)
    i=x.load(std::memory_order_relaxed),i=0; // useless

    因为没有保存来自轻松操作的信息。

    这对(2)有效:
    (void)x.fetch_add(1, std::memory_order_relaxed);

    这对于(2)无效:
    if (x.load(std::memory_order_relaxed))
    f();
    else
    g();

    结果的决定是基于轻松的负担,
    i += x.fetch_add(1, std::memory_order_release);

    注意:(2)涵盖了原子的最常见用法之一,即线程安全引用计数器。 (更正:尚不清楚所有线程安全计数器在技术上是否符合描述,因为获取只能在递减后的0时完成,然后基于counter> 0做出没有获取的决定;决定不执行任何操作,而是仍然...)

    最佳答案

    不,绝对不是全部。它至少是线程中的内存障碍,以实现更强的内存顺序。

    对于mo_relaxed原子,是的,我认为理论上可以完全优化它,好像它不在源代码中一样。线程完全不属于它可能已经发布的释放序列,这是等效的。

    如果您使用了fetch_add(0, mo_relaxed)的结果,那么我认为将它们折叠在一起,只是进行加载而不是0的RMW可能并不完全等效。围绕放松的RMW的该线程中的障碍仍然会影响所有操作,包括命令放松的操作wrt。非原子操作。使用load + store的tied together as an atomic RMW,订购存储的东西可以在不订购纯负载的情况下订购原子RMW。

    但是我不认为任何C++排序都是这样的:mo_release存储命令较早的加载和存储,而atomic_thread_fence(mo_release)就像asm StoreStore + LoadStore障碍。 (Preshing on fences)。因此,是的,考虑到任何C++施加的排序也同样适用于宽松负载,同样适用于宽松RMW,我认为int tmp = shared.fetch_add(0, mo_relaxed)可以优化为仅负载。

    (实际上,编译器根本不会优化原子,即使对于volatile atomic,它们也基本上像mo_relaxed 一样对待它们。Why don't compilers merge redundant std::atomic writes?http://wg21.link/n4455 + http://wg21.link/p0062。这太难了/不存在让编译器知道什么时候不知道的机制。)

    但是,是的,书面上的ISO C++标准不能保证其他线程实际上可以观察到任何给定的中间状态。

    思想实验:考虑在单核协作多任务系统上的C++实现。它通过在需要避免死锁的地方插入yield调用来实现std::thread,但不是在每条指令之间插入。标准中的任何内容都不需要在num++num--之间产生让步,以便其他线程观察该状态。

    基本规则基本上允许编译器选择合法/可能的顺序,并在编译时确定每次发生的情况。

    在实践中,如果将-- / ++组合在一起而没有对原子对象进行任何修改的情况下,如果解锁/重新锁定实际上从未赋予其他线程机会进行锁定的机会,则会造成公平性问题!这就是为什么编译器没有优化的原因。

    任何一项或两项操作的更强的排序都可以开始,或者是与阅读器同步的发布序列的一部分。负责获取发布存储/ RMW同步负载的读取器-使用此线程,并且必须已经看到此线程的所有先前效果。

    IDK读者将如何知道它正在看到该线程的发布存储区,而不是先前的值,因此,可能很难编写一个真实的示例。至少我们可以创建一个没有可能的UB的应用程序,例如通过读取另一个宽松的原子变量的值,因此,如果我们看不到该值,则可以避免数据争用UB。

    考虑以下顺序:

    // broken code where optimization could fix it
    memcpy(buf, stuff, sizeof(buf));

    done.store(1, mo_relaxed); // relaxed: can reorder with memcpy
    done.fetch_add(-1, mo_relaxed);
    done.fetch_add(+1, mo_release); // release-store publishes the result

    这可以优化为仅将 done.store(1, mo_release);正确发布到另一个线程的 1,而不必在更新的 1值之前过早看到 buf的风险。

    但是,这还可以将放松商店之后的取消对的RMW对优化为栅栏,而这仍然会被打破。 (而不是优化的错。)
    // still broken
    memcpy(buf, stuff, sizeof(buf));

    done.store(1, mo_relaxed); // relaxed: can reorder with memcpy
    atomic_thread_fence(mo_release);

    我还没有想到一个示例,在该示例中,此类合理的优化会破坏安全代码。 当然,即使它们是seq_cst,也要完全删除它们对并不总是安全的。

    seq_cst的递增和递减仍会创建某种内存屏障。 如果未对它们进行优化,则较早的商店将无法与较晚的负载进行交错。为了保留这一点,针对x86进行编译可能仍需要发出 mfence

    当然,显而易见的是 lock add [x], 0,它实际上对我们执行 x++ / x--的共享对象进行了虚拟RMW。但是我认为,仅内存障碍,而不是与对该实际对象或高速缓存行的访问耦合,就足够了。

    当然,它必须充当编译时内存屏障,阻止编译时对非原子访问和原子访问的重新排序。

    对于acq_rel或更弱的 fetch_add(0)或取消序列,x86上可能会免费发生运行时内存障碍,只需要限制编译时的顺序即可。

    另请参阅我对 Can num++ be atomic for 'int num'?的回答的一部分以及对Richard Hodges的回答的评论。 (但是请注意,在 ++--之间何时对其他对象进行了修改时,一些讨论被争论所迷惑。当然,必须保留原子暗示的该线程操作的所有顺序。)

    就像我说的那样,这只是所有假设,真正的编译器不会优化原子,直到尘埃落在N4455 / P0062上为止。

    关于c++ - 如果RMW操作没有任何变化,是否可以针对所有内存顺序对其进行优化?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58667649/

    24 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com