原子操作的成本是什么(比较和交换或原子添加/减少中的任何一个)?它消耗了多少周期?它会暂停SMP或NUMA上的其他处理器,还是会阻止内存访问?它会在无序CPU中刷新重新排序缓冲区吗?
缓存有什么影响?
我对现代流行的CPU感兴趣:x86,x86_64,PowerPC,SPARC,Itanium.
我偶然发现了这篇Reddit 帖子,它是对以下代码片段的一个玩笑,
void f(int& x) {
if (x != 1) {
x = 1;
}
}
void g(int& x) {
x = 1;
}
Run Code Online (Sandbox Code Playgroud)
说这两个函数不等同于“编译器”。我确信任何主要的 C++ 编译器都会将条件赋值优化为无条件存储,从而为f和发出相同的汇编代码g。
谁能向我解释为什么会这样?
我的想法是:无条件存储很可能会更快,因为无论如何我们都必须访问内存来读取比较值,并且分支代码会给分支预测器带来压力。此外,编译器不应将存储视为副作用(AFAIK),即使后续内存访问可能会更快或更慢,具体取决于是否f由于缓存局部性而采用分支。
那么编译器就无法弄清楚这一点吗?虽然证明f和 的等价性g可能并不容易,但我觉得这些编译器能够解决更困难的问题。那么我可能错了,这些功能毕竟不相等,或者这里发生了什么?
暂停指令通常用于测试自旋锁的循环,当一些其他线程拥有自旋锁时,以缓解紧密循环.据说这相当于一些NOP指令.有人能告诉我它是如何适用于自旋锁优化的吗?在我看来,即使是NOP指令也浪费了CPU时间.它们会降低CPU使用率吗?
另一个问题是我可以将暂停指令用于其他类似目的.例如,我有一个忙线程,它不断扫描一些地方(例如队列)以检索新节点; 但是,有时队列是空的,线程只是在浪费cpu时间.睡眠线程并通过其他线程唤醒它可能是一个选项,但线程是关键的,所以我不想让它睡觉.可以暂停指令工作以减轻CPU使用率吗?目前它使用100%cpu的物理核心?
谢谢.
我实现了SpinLock类,如下所示
struct Node {
int number;
std::atomic_bool latch;
void add() {
lock();
number++;
unlock();
}
void lock() {
bool unlatched = false;
while(!latch.compare_exchange_weak(unlatched, true, std::memory_order_acquire));
}
void unlock() {
latch.store(false , std::memory_order_release);
}
};
Run Code Online (Sandbox Code Playgroud)
我实现了上面的类,并创建了两个线程,每个线程调用一个相同的Node类实例的add()方法1000万次.
不幸的是,结果不是2000万.我在这里错过了什么?
我正在用c ++编写多线程应用程序,其中性能至关重要.我需要在线程之间复制小结构时使用大量锁定,为此我选择使用自旋锁.
我已经做了一些研究和速度测试,我发现大多数实现大致同样快:
__asm {}使用类似这样的代码,它得分约70个时间单位,但我不确定是否已创建适当的内存屏障.编辑:这里给出的时间是2个线程锁定和解锁螺旋锁1,000,000次所需的时间.
我知道这并没有太大的区别,但是由于自旋锁是一个使用频繁的对象,人们会认为程序员会同意以最快的方式制作自旋锁.谷歌搜索导致许多不同的方法.我认为如果使用内联汇编并使用指令而不是比较32位寄存器来实现上述方法将是最快的CMPXCHG8B.此外,必须考虑内存障碍,这可以通过LOCK CMPXHG8B(我认为?)来完成,这保证了内核之间共享内存的"专有权".最后[有人建议]对于繁忙的等待应该伴随NOP:REP,这将使超线程处理器切换到另一个线程,但我不确定这是否是真的?
根据我对不同螺旋锁的性能测试,可以看出没有太大区别,但出于纯粹的学术目的,我想知道哪一个是最快的.但是由于我在汇编语言和内存障碍方面的经验非常有限,如果有人可以为我在LOCK CMPXCHG8B中提供的最后一个示例编写汇编代码并在以下模板中使用适当的内存屏障,我会很高兴:
__asm
{
spin_lock:
;locking code.
spin_unlock:
;unlocking code.
}
Run Code Online (Sandbox Code Playgroud) I've written this implementation of a double buffer:
// ping_pong_buffer.hpp
#include <vector>
#include <mutex>
#include <condition_variable>
template <typename T>
class ping_pong_buffer {
public:
using single_buffer_type = std::vector<T>;
using pointer = typename single_buffer_type::pointer;
using const_pointer = typename single_buffer_type::const_pointer;
ping_pong_buffer(std::size_t size)
: _read_buffer{ size }
, _read_valid{ false }
, _write_buffer{ size }
, _write_valid{ false } {}
const_pointer get_buffer_read() {
{
std::unique_lock<std::mutex> lk(_mtx);
_cv.wait(lk, [this] { return _read_valid; });
}
return _read_buffer.data();
}
void end_reading() {
{
std::lock_guard<std::mutex> lk(_mtx);
_read_valid = …Run Code Online (Sandbox Code Playgroud) 这个问题专门针对现代x86-64缓存一致性架构 - 我很欣赏其他CPU的答案可能会有所不同.
如果我写入内存,MESI协议要求首先将缓存行读入缓存,然后在缓存中进行修改(将值写入缓存行,然后将其标记为脏).在较旧的写入微架构中,这将触发高速缓存行被刷新,在写回期间,被刷新的高速缓存行可能会延迟一段时间,并且一些写入组合可能在两种机制下发生(更可能是回写) .我知道这与访问相同缓存行数据的其他核心如何交互 - 缓存监听等.
我的问题是,如果商店恰好匹配缓存中已有的值,如果没有单个位被翻转,那么任何英特尔微架构都会注意到这一点并且不将该行标记为脏,从而可能将该行标记为独占,以及在某些时候跟随的回写内存开销?
当我向更多的循环进行矢量化时,我的矢量化操作组合基元不会明确地检查值的变化,并且在CPU/ALU中这样做似乎很浪费,但我想知道底层缓存电路是否可以在没有显式编码的情况下完成(例如,商店微操作或缓存逻辑本身).由于跨多个内核的共享内存带宽变得更加成为资源瓶颈,这似乎是一种越来越有用的优化(例如,重复调整相同的内存缓冲区 - 如果它们已经存在,我们不会重新读取RAM中的值在缓存中,但强制写回相同的值似乎很浪费).回写缓存本身就是对这类问题的承认.
我可以礼貌地要求阻止"在理论上"或"它确实无关紧要"的答案 - 我知道记忆模型是如何工作的,我正在寻找的是关于如何写出相同价值的硬性事实(而不是避免一个商店)将影响内存总线的争用你可以安全地假设是一台运行多个工作负载的机器几乎总是受内存带宽限制.另一方面,解释为什么芯片不这样做的确切原因(我悲观地假设他们没有这样做)将具有启发性......
更新: 这里的预期线路上的一些答案https://softwareengineering.stackexchange.com/questions/302705/are-there-cpus-that-perform-this-possible-l1-cache-write-optimization但仍然很多推测"它必须很难,因为它没有完成",并说如何在主CPU核心中这样做会很昂贵(但我仍然想知道为什么它不能成为实际缓存逻辑本身的一部分).
哪种自旋锁方法更好(就效率而言)?
#include <atomic>
#define METHOD 1
int main( )
{
std::atomic_flag lock { };
#if METHOD == 1
while ( lock.test_and_set( std::memory_order_acquire ) )
{
while ( lock.test( std::memory_order_relaxed ) );
}
#else
while ( lock.test_and_set( std::memory_order_acquire ) );
#endif
lock.clear( std::memory_order_release );
}
Run Code Online (Sandbox Code Playgroud)
这个例子来自cppreference。test(std::memory_order_relaxed)当我们添加/删除对外部循环内部的调用时会发生什么?
我发现这两种方法(此处)生成的代码存在明显差异。
我是低级别的新手,所以我完全忘记了你可能面临的问题,我甚至不确定我是否理解"原子"一词.现在我试图通过扩展程序集围绕内存操作进行简单的原子锁.为什么?为了好奇.我知道我在这里重新发明轮子,可能会过度简化整个过程.
这个问题? 我在这里提供的代码是否实现了使内存操作既线程安全又可重入的目标?
我只想做...
代码:
volatile int atomic_gate_memory = 0;
static inline void atomic_open(volatile int *gate)
{
asm volatile (
"wait:\n"
"cmp %[lock], %[gate]\n"
"je wait\n"
"mov %[lock], %[gate]\n"
: [gate] "=m" (*gate)
: [lock] "r" (1)
);
}
static inline void atomic_close(volatile int *gate)
{
asm volatile (
"mov %[lock], %[gate]\n"
: [gate] "=m" (*gate)
: [lock] "r" (0)
);
}
Run Code Online (Sandbox Code Playgroud)
然后像:
void *_malloc(size_t size)
{
atomic_open(&atomic_gate_memory);
void *mem = malloc(size);
atomic_close(&atomic_gate_memory); …Run Code Online (Sandbox Code Playgroud) 根据Wiki,CAS 做这样的事情:
function cas(p : pointer to int, old : int, new : int) returns bool {
if *p ? old {
return false
}
*p ? new
return true
}
Run Code Online (Sandbox Code Playgroud)
好吧,在我看来,如果多个处理器将尝试使用相同的参数执行 CAS 指令,则可能会同时进行多次写入尝试,因此无论如何这样做都不安全。
我哪里错了?
我正在为我的最新项目制作一个基于光纤的作业系统,该系统将依赖于使用自旋锁来实现正确的功能。我本来打算使用 PAUSE 指令,因为这似乎是普通现代自旋锁等待部分的黄金标准。然而,在对实现我自己的光纤进行一些研究时,我发现最近机器上的暂停周期持续时间已增加到不利的程度。
我从这里发现了这一点,其中引用了英特尔优化手册,“上一代微架构中 PAUSE 指令的延迟约为 10 个周期,而在 Skylake 微架构上它已扩展到多达 140 个周期,”和“由于 PAUSE 延迟显着增加,对 PAUSE 延迟敏感的工作负载将遭受一些性能损失。”
因此,我想找到 PAUSE 指令的替代方案以在我自己的自旋锁中使用。我读过,在过去,暂停一直是首选,因为它以某种方式节省了能源使用,我猜测这是由于另一个经常引用的事实,即使用暂停以某种方式向处理器发出信号,表明它处于自旋锁之中。我还猜测,这是在功率范围的另一端,为所需的周期数进行一些虚拟计算。
鉴于此,是否有一种最佳情况的解决方案能够接近 PAUSE 的表观能源效率,同时具有作为重复“丢弃”计算的灵活性和低周期计数?
C++ 自旋锁可以使用std::atomic_flag轻松实现,它可以粗略地编码(没有特殊功能),如下所示:
std::atomic_flag f = ATOMIC_FLAG_INIT;
while (f.test_and_set(std::memory_order_acquire)); // Acquire lock
// Here do some lock-protected work .....
f.clear(std::memory_order_release); // Release lock
Run Code Online (Sandbox Code Playgroud)
另外,正如人们在 uops.info(此处的屏幕)上看到的那样,XCHG 可能会占用30相当流行的 Skylake 上的 CPU 周期。这是相当慢的。
通过这样的程序可以测量自旋锁的整体速度。
是否可以在没有 XCHG 的情况下实现自旋锁定?主要关心的是速度,而不仅仅是使用另一条指令。
最快的自旋锁是什么?是否可以将其改为 10 个周期而不是 30 个?还有5个周期?也许是一些平均运行速度很快的概率自旋锁?
它应该以严格的方式实施,这意味着在 100% 的情况下它可以正确保护代码和数据。如果它是概率性的,那么它应该运行可能的时间,但每次运行后仍能 100% 正确地保护。
对我来说,这种自旋锁的主要目的是保护多个线程内非常小的操作,这些操作运行十几个或两个周期,因此 30 个周期的延迟太大了。当然可以说我可以使用原子或其他无锁技术来实现所有操作。但这种技术并不适用于所有情况,并且还需要花费大量工作才能在许多类和方法的庞大代码库中严格实现。因此,还需要一些通用的东西,比如常规的自旋锁。