用户定义的原子小于

时间:2017-11-28 21:41:02

标签: multithreading openmp stdatomic

我一直在阅读,似乎std :: atomic不支持比较少/大于变体的比较和交换。

我正在使用OpenMP,需要安全地更新全局最小值。 我认为这就像使用内置API一样简单。 但是,唉,我正试图想出自己的实现。

我主要关注的事实是我不想使用omp临界区来每次执行少于比较,因为在大多数情况下,它可能会产生很大的同步开销以获得非常小的增益。

但是在可能发现新的全局最小值(不太常见)的情况下,同步开销是可以接受的。我想我可以使用以下方法实现它。希望有人提出建议。

  1. 使用std :: atomic_uint作为全局最小值。
  2. 以原子方式将值读入线程本地堆栈。
  3. 将其与当前值进行比较,如果较小,请尝试输入关键部分。
  4. 一旦同步,验证原子值是否仍然小于新值并相应地更新(临界区的主体应该便宜,只需更新几个值)。
  5. 这是一个家庭作业,所以我试图保持我自己的实施。请不要推荐各种库来完成此任务。但是请对此操作可能产生的同步开销进行评论,或者如果不好,请详细说明原因。感谢。

1 个答案:

答案 0 :(得分:3)

您正在寻找的内容如果存在则会被称为fetch_min():获取旧值并将内存中的值更新为min(current, new),与fetch_add完全相同,但使用{{ 1}}。

x86上的硬件不直接支持此操作,但具有LL / SC的计算机可以为其发送稍微更高效的asm,而不是使用min()重试循环来模拟它。 / p>

您可以使用CAS重试循环模拟任何原子操作。在实践中,它通常不必重试,因为在计算负载结果之后的几个周期之后,成功执行负载的CPU通常也会在CAS中成功,因此它非常有效。

有关使用CAS重试循环为CAS ( old, min(old,new) )创建fetch_add的示例,请参阅Atomic double floating point or SSE/AVX vector load/store on x86_64atomic<double>compare_exchange_weak + 1}}。使用double执行此操作并完成所有设置。

回复:评论中的澄清:我认为你说你有一个全球最低限度,但是当你找到一个新的时候,你也希望更新一些相关的数据。你的问题令人困惑,因为&#34;比较和交换少于/大于&#34;并没有帮助你。

我建议使用min来跟踪全局最小值,因此您可以阅读它以决定是否进入关键部分并更新与此最小值相关的相关状态。

只有在持有锁定时(即在关键部分内)修改atomic<unsigned> globmin。然后你可以更新它+相关的数据。它必须是globmin,因此在关键部分之外仅查看atomic<>的读者不会有数据竞争UB。查看相关额外数据的读者必须采取保护它的锁定,并确保从遵守锁定的读者的角度来看globmin +额外数据的更新&#34;原子地&#34;。

globmin
对于globmin来说,

static std::atomic<unsigned> globmin; std::mutex globmin_lock; static struct Extradata globmin_extra; void new_min_candidate(unsigned newmin, const struct Extradata &newdata) { // light-weight early out check to avoid the critical section // No ordering requirement as long as globmin is monotonically decreasing with time if (newmin < globmin.load(std::memory_order_relaxed)) { // enter a critical section. Use OpenMP stuff if you want, this is plain ISO C++ std::lock_guard<std::mutex> lock(globmin_lock); // Check globmin again, after we've excluded other threads from modifying it and globmin_extra if (newmin < globmin.load(std::memory_order_relaxed)) { globmin.store(newmin, std::memory_order_relaxed); globmin_extra = newdata; } // else leave the critical section with no update: // another thread raced with use *outside* the critical section // release the lock / leave critical section (lock goes out of scope here: RAII) } // else do nothing } 就足够了:除了原子性之外,其他任何东西都不需要排序。我们从关键部分/锁获得关联数据的原子性/一致性,而不是加载/存储std::memory_order_relaxed的内存排序语义。

这种方式唯一的原子读 - 修改 - 写操作是锁定本身。 globmin上的所有内容都是加载或存储(便宜得多)。多线程的主要成本仍然是缓存高速缓存线,但是一旦你拥有一个缓存行,每个原子RMW可能比现代x86(http://agner.org/optimize/)上的一个简单存储贵20倍。

使用此设计,如果大多数候选人不低于globmin,则缓存行将在大多数时间内保持在Shared state,因此关键字之外的globmin section可以在L1D缓存中命中。它只是一个普通的加载指令,所以它非常便宜。 (在x86上,即使是seq-cst加载也只是普通加载(并且释放加载只是普通的加载,但是seq_cst存储更昂贵)。在默认排序较弱的其他体系结构中,seq_cst / acquire加载需要一个障碍。)< / p>