代码之家  ›  专栏  ›  技术社区  ›  Stephen Lin

易失性但未设防的读取是否会产生无限过时的值?(在实际硬件上)

  •  28
  • Stephen Lin  · 技术社区  · 12 年前

    在回答时 this question 关于OP的情况出现了另一个我不确定的问题:这主要是一个处理器体系结构问题,但也有一个关于C++11内存模型的附带问题。

    基本上,OP的代码在更高的优化级别上无限循环,因为以下代码(为了简单起见,稍作修改):

    while (true) {
        uint8_t ov = bits_; // bits_ is some "uint8_t" non-local variable
        if (ov & MASK) {
            continue;
        }
        if (ov == __sync_val_compare_and_swap(&bits_, ov, ov | MASK)) {
            break;
        }
    }
    

    哪里 __sync_val_compare_and_swap() 是GCC内置的原子CAS。GCC(合理地)将其优化为无限循环,在这种情况下 bits_ & mask 被检测为 true 在进入循环之前,完全跳过CAS操作,因此我建议进行以下更改(有效):

    while (true) {
        uint8_t ov = bits_; // bits_ is some "uint8_t" non-local variable
        if (ov & MASK) {
            __sync_synchronize();
            continue;
        }
        if (ov == __sync_val_compare_and_swap(&bits_, ov, ov | MASK)) {
            break;
        }
    }
    

    在我回答后,OP注意到 bits_ volatile uint8_t 似乎也很有效。我建议不要走那条路,因为 volatile 通常不应该用于同步,而且在这里使用围栏似乎没有太大的缺点。

    然而,我想了更多,在这种情况下,语义是这样的,如果 ov & MASK 检查是基于过时的值,只要它不是基于无限过时的值(即,只要循环最终被破坏),因为实际的更新尝试 位_ 是同步的。也是如此 不稳定的 这里足以保证这个循环最终会终止,如果 位_ 由另一个线程更新,使得 bits_ & MASK == false ,对于任何现有的处理器?换句话说,在没有显式内存围栏的情况下,编译器没有优化的读取实际上是否有可能被处理器无限期地有效优化?( 编辑: 为了明确起见,我在这里问的是,假设读取是由编译器在循环中发出的,那么现代硬件实际上可以做什么,所以从技术上讲,这不是一个语言问题,尽管用C++语义来表达它很方便。)

    这就是它的硬件角度,但为了稍微更新它,并使其成为关于C++11内存模型的一个可回答的问题,请考虑上面代码的以下变体:

    // bits_ is "std::atomic<unsigned char>"
    unsigned char ov = bits_.load(std::memory_order_relaxed);
    while (true) {
        if (ov & MASK) {
            ov = bits_.load(std::memory_order_relaxed);
            continue;
        }
        // compare_exchange_weak also updates ov if the exchange fails
        if (bits_.compare_exchange_weak(ov, ov | MASK, std::memory_order_acq_rel)) {
            break;
        }
    }
    

    cppreference 声称 std::memory_order_relaxed 意味着“对围绕原子变量的内存访问的重新排序没有限制”,因此与实际硬件将要做什么或不会做什么无关,这确实意味着 bits_.load(std::memory_order_relaxed) 从技术上讲 从不 在之后读取更新的值 位_ 是否在一致性实现中的另一个线程上更新?

    编辑: 我在标准(29.4 p13)中发现了这一点:

    实现应该使原子存储在合理的时间内对原子负载可见。

    因此,显然“无限长时间”等待更新值是不可能的(大多数情况下?),但除了应该是“合理的”之外,没有任何具体的新鲜时间间隔的硬性保证;尽管如此,关于实际硬件行为的问题仍然存在。

    4 回复  |  直到 7 年前
        1
  •  9
  •   Pete Becker    12 年前

    C++11原子处理 问题:

    1. 确保在没有线程切换的情况下读取或写入完整的值;这样可以防止撕裂。

    2. 确保编译器不会跨原子读取或写入对线程内的指令进行重新排序;这确保了线程内的排序。

    3. 确保(对于内存顺序参数的适当选择)在原子写入之前在线程内写入的数据将被读取原子变量并查看所写入的值的线程看到。这就是可见性。

    当您使用 memory_order_relaxed 你无法从轻松的商店或货物中获得可见性的保证。你确实得到了前两个保证。

    实现“应该”(即鼓励)在合理的时间内使内存写入可见,即使使用宽松的排序。这是可以说的最好的;这些东西迟早会出现的。

    因此,是的,从形式上讲,一个从未使轻松的写入对轻松的读取可见的实现符合语言定义。在实践中,这种情况不会发生。

    至于什么 volatile 确实如此,请询问编译器供应商。这取决于实施。

        2
  •  4
  •   Patashu    12 年前

    这在技术上是合法的 std::memory_order_relaxed 加载到永远不会为该加载返回新值。至于是否有任何实施会这样做,我不知道。

    参考文献: http://www.developerfusion.com/article/138018/memory-ordering-for-atomic-operations-in-c0x/ “唯一的要求是,从同一线程对单个原子变量的访问不能重新排序:一旦给定线程看到了原子变量的特定值,该线程随后的读取就无法检索该变量的早期值。”

        3
  •  4
  •   Maja Piechotka    12 年前

    如果处理器没有缓存一致性协议,或者协议非常简单,那么它可以“优化”从缓存中获取过时数据的负载。现在大多数现代多核CPU都实现了缓存一致性协议。然而,A9之前的ARM没有。非CPU架构也可能没有缓存一致性(尽管它们可能不符合C++内存模型)。

    另一个问题是许多体系结构(包括ARM和x86)允许重新排序内存访问。我不知道处理器是否足够聪明,能够注意到对同一地址的重复访问,但我对此表示怀疑(在极少数情况下,这会花费空间和时间,因为编译器应该能够注意到这一点,但好处很小,因为以后的访问很可能是L1命中),但从技术上讲,它可以推测会进行分支,并且可以在第一次访问之前重新排序第二次访问(不太可能,但如果我正确阅读了英特尔和ARM手册,这是允许的)。

    最后,还有一些外部设备不遵守高速缓存一致性。如果CPU通过内存映射IO/DMA进行通信,则页面必须标记为不可缓存(否则,L1/L2/L3/…缓存中的数据将过时)。在这种情况下,处理器通常不会重新排序读写(有关详细信息,请参阅处理器手册-它可能有更细粒度的控制)-编译器可以,因此您需要使用 volatile 然而,由于原子通常是基于缓存的,所以您不需要也可以使用它们。

    恐怕我无法回答这样强大的缓存一致性是否会在未来的处理器中可用。我建议严格遵守规范(“在int中存储指针有什么问题?肯定没有人会使用超过4GiB的用户,所以32b地址就足够大了。”)。其他人回答了正确性,所以我不包括它。

        4
  •  1
  •   Community CDub    7 年前

    这是我的看法,尽管我对这个话题没有太多知识,但还是要谨慎对待。

    这个 volatile 关键字效果很可能取决于编译器,但我认为它实际上实现了我们直观上期望的效果,即避免混叠或任何其他优化,因为这些优化不会让用户在变量生命周期中的任何执行点检查调试器中变量的值。这很接近(可能也一样) answer 关于volatile的含义。

    直接的含义是,任何访问 不稳定的 变量 v 一旦它修改了它,就必须将它提交到内存中。围栏会使它按照其他更新的顺序进行,但无论哪种方式,都会有一个存储 v 在程序集输出中,如果 v 在源级别进行了修改。

    事实上,你问的问题是,如果 v ,加载在寄存器中,未经某些计算修改,是什么迫使CPU执行读取 v 同样适用于任何寄存器,而不是简单地重用它之前已经得到的值。

    我认为答案是CPU 不能 假设一个存储单元从上次读取时起没有改变。即使在单核系统上,内存访问也不是严格留给CPU的。许多其他子系统可以以读写方式访问它(这是其背后的原理 DMA ).

    CPU可能能做的最安全的优化是检查缓存中的值是否发生了变化,并将其用作 v 内存中。缓存应该保持同步。由于DMA附带的缓存失效机制,具有内存。在这种情况下,故障再次出现 cache coherency on multicore ,以及针对多线程情况的“写后写”。最后一个问题无法用简单的方法有效处理 不稳定的 变量,因为它们的修改操作不是原子操作,正如您已经知道的那样。