【问题标题】:Are there any more efficient ways for atomically adding two floats?有没有更有效的方法来原子地添加两个浮点数?
【发布时间】:2018-02-12 12:29:34
【问题描述】:

我有一束由各种线程更新的浮点数。数组的大小远大于线程数。因此同时访问特定的浮点数是相当罕见的。我需要 C++03 的解决方案。

以下代码以原子方式将一个值添加到其中一个浮点数 (live demo)。假设它有效,它可能是最好的解决方案。 我能想到的唯一选择是将数组分成几束并用互斥锁保护每一束。但我不希望后者更有效率。

我的问题如下。是否有任何替代解决方案可以原子地添加浮点数?谁能预测哪个是最有效的?是的,我愿意做一些基准测试。也许可以通过放宽内存限制来改进下面的解决方案,即用其他东西交换__ATOMIC_SEQ_CST。我没有这方面的经验。

void atomic_add_float( float *x, float add )
{
  int *ip_x= reinterpret_cast<int*>( x ); //1
  int expected= __atomic_load_n( ip_x, __ATOMIC_SEQ_CST ); //2
  int desired;
  do  {
    float sum= *reinterpret_cast<float*>( &expected ) + add; //3
    desired=   *reinterpret_cast<int*>( &sum );
  } while( ! __atomic_compare_exchange_n( ip_x, &expected, desired, //4
                                          /* weak = */ true, 
                                          __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST ) );
}

它的工作原理如下。在//1x 的位模式被解释为int,即我假设floatint 具有相同的大小(32 位)。在//2,要增加的值是原子加载的。在//3int 的位模式被解释为float,并添加了和​​数。 (请记住,expected 包含在ip_x == x 中找到的值。)这不会更改ip_x == x 下的值。在//4,如果没有其他线程更改该值,则求和结果仅存储在ip_x == x,即如果expected == *ip_x (docu)。如果不是这种情况,do-loop 将继续,expected 包含在 ip_x == x 广告中找到的更新值。

GCC 的原子访问函数(__atomic_load_n__atomic_compare_exchange_n)可以很容易地被其他编译器的实现所交换。

【问题讨论】:

  • @user463035818 要将其更改为 C 代码,只需通过经典 C 转换交换 reinterpret_cast
  • @ClaasBontus 这可能适用于这个问题,不幸的是 c++ 答案并不总是很容易转换为 c 答案(反之亦然)。请不要标记无关的标签
  • 假设代码可以正常工作,这看起来值得Code Review 批评。请务必先阅读A guide to Code Review for Stack Overflow users,因为那里有些事情的处理方式不同!
  • 在 C++20 中,您可能会得到 std::atomic&lt;float&gt;,但现在不可用。 P0020R6 Floating Point Atomic

标签: c++ multithreading atomic c++03


【解决方案1】:

为了使此功能更高效,您可能希望将__ATOMIC_ACQUIRE 分别用于__atomic_load_n__ATOMIC_RELEASE__ATOMIC_RELAXED 用于__atomic_compare_exchange_n success_memorderfailure_memorder

在 x86-64 上,虽然这不会改变生成的程序集,因为它的内存模型相对强大。与内存模型较弱的 ARM 不同。

【讨论】:

  • 当然,对于 x86 来说没关系,但 __ATOMIC_RELAXED 对于成功的比较交换 IMO 来说仍然不够......
  • @AndriyBerestovskyy 那__ATOMIC_RELAXED 仅用于在__atomic_compare_exchange_n 失败时重新加载,成功__atomic_compare_exchange_n 必须做__ATOMIC_RELEASE。阅读函数的文档,有一些有趣的细节。
  • 当然,只是看错了and __ATOMIC_RELAXED for __atomic_compare_exchange_n success_memorder这句话...
【解决方案2】:

是否有任何替代解决方案可以原子地添加浮点数?谁能预测哪个是最有效的?

当然,至少我想到了几个:

  1. 使用同步原语,即自旋锁。会比 compare-exchange 慢一点。

  2. 事务扩展 (see Wikipedia)。会更快,但这种解决方案可能会限制可移植性。

总体而言,您的解决方案非常合理:速度很快,而且可以在任何平台上运行。

在我看来,所需的内存顺序是:

  • __ATOMIC_ACQUIRE -- 当我们读取 __atomic_load_n() 中的值时
  • __ATOMIC_RELEASE -- 当__atomic_compare_exchange_n() 成功时
  • __ATOMIC_ACQUIRE -- 当__atomic_compare_exchange_n() 失败时

【讨论】:

  • 我不认为你是对的。 __atomic_compare_exchange_n 更新 expected 如果 expected != desired。因此,只需在循环外加载一次expected 就足够了。
  • @ClaasBontus 你是对的,我将__atomic_compare_exchange_n() 与英特尔的 CMPXCHG 指令混淆了......这意味着我们肯定需要一个__ATOMIC_ACQUIRE 来订购它失败的东西......
猜你喜欢
  • 2023-03-07
  • 2017-05-03
  • 2017-05-30
  • 2013-06-04
  • 1970-01-01
  • 1970-01-01
  • 2014-04-13
  • 1970-01-01
相关资源
最近更新 更多