Lock free single producer/single consumer circular buffer - CPU 猜测能否打破内存屏障逻辑?
Lock free single producer/single consumer circular buffer - Can CPU speculation break the memory barrier logic?
当我考虑推测执行及其对简单代码的影响时,我一直在寻找无锁的单个 producer/single 消费者循环缓冲区。
使用此实现,只有一个线程可以调用 push()
函数,另一个线程可以调用 pop()
函数。
这里是 Producer
代码:
bool push(const Element& item)
{
const auto current_tail = _tail.load(std::memory_order_relaxed); //(1)
const auto next_tail = increment(current_tail);
if(next_tail != _head.load(std::memory_order_acquire)) //(2)
{
_array[current_tail] = item; //(3)
_tail.store(next_tail, std::memory_order_release); //(4)
return true;
}
return false; // full queue
}
这里是 Consumer
代码:
bool pop(Element& item)
{
const auto current_head = _head.load(std::memory_order_relaxed); //(1)
if(current_head == _tail.load(std::memory_order_acquire)) //(2)
return false; // empty queue
item = _array[current_head]; //(3)
_head.store(increment(current_head), std::memory_order_release); //(4)
return true;
}
问题
如果 push()
由于推测执行而被编译为以下函数怎么办:
bool push(const Element& item)
{
const auto current_tail = _tail.load(std::memory_order_relaxed); // 1
const auto next_tail = increment(current_tail);
//The load is performed before the test, it is valid
const auto head = _head.load(std::memory_order_acquire);
//Here is the speculation, the CPU speculate that the test will succeed
//store due to speculative execution AND it respects the memory order due to read-acquire
_array[current_tail] = item;
_tail.store(next_tail, std::memory_order_release);
//Note that in this case the test checks if you it has to restore the memory back
if(next_tail == head)//the code was next_tail != _head.load(std::memory_order_acquire)
{
//We restore the memory back but the pop may have been called before and see an invalid memory
_array[current_tail - 1] = item;
_tail.store(next_tail - 1, std::memory_order_release);
return true;
}
return false; // full queue
}
对我来说,为了完全有效,推送函数应该确保在条件成功之后发出障碍:
bool push(const Element& item)
{
const auto current_tail = _tail.load(std::memory_order_relaxed); // 1
const auto next_tail = increment(current_tail);
if(next_tail != _head.load(std::memory_order_relaxed)) // 2
{
//Here we are sure that nothing can be reordered before the condition
std::atomic_thread_fence(std::memory_order_acquire); //2.1
_array[current_tail] = item; // 3
_tail.store(next_tail, std::memory_order_release); // 4
return true;
}
return false; // full queue
}
回复:您建议的重新排序:不,编译器不能发明写入原子变量。
运行时推测也无法发明实际上对其他线程可见的写入。它可以将任何它想要的东西放入自己的私有存储缓冲区,但必须先检查早期分支的正确性,然后才能对其他线程可见存储。
通常这是通过 in-order 退役来实现的:一条指令只能退役(成为 non-speculative),前提是所有先前的指令都是 retired/non-speculative。在存储指令退出之前,存储不能从存储缓冲区提交到 L1d 缓存。
re: 标题:不,推测执行仍然要尊重内存模型。如果 CPU 想要推测性地加载一个不完整的 acquire-load,它可以,但前提是它 检查 以确保这些加载结果在它们'重新 "officially" 允许发生。
x86 CPUs do 实际上是这样做的,因为强大的 x86 内存模型意味着 all 负载是 acquire-loads,所以任何 out-of-order 加载都必须是推测性的,如果无效则回滚。 (这就是为什么你可以获得 memory-order mis-speculation 管道核武器。)
所以 asm 按照 ISA 规则规定的方式工作,C++ 编译器知道这一点。编译器使用它在目标 ISA 之上实现 C++ 内存模型。
如果你用 C++ 做一个 acquire-load,它真的可以作为一个 acquire-load。
您可以根据编写的 C++ 重新排序规则在心理上为可能的 compile-time + run-time 重新排序建立逻辑模型。参见 http://preshing.com/20120913/acquire-and-release-semantics/。
当我考虑推测执行及其对简单代码的影响时,我一直在寻找无锁的单个 producer/single 消费者循环缓冲区。
使用此实现,只有一个线程可以调用 push()
函数,另一个线程可以调用 pop()
函数。
这里是 Producer
代码:
bool push(const Element& item)
{
const auto current_tail = _tail.load(std::memory_order_relaxed); //(1)
const auto next_tail = increment(current_tail);
if(next_tail != _head.load(std::memory_order_acquire)) //(2)
{
_array[current_tail] = item; //(3)
_tail.store(next_tail, std::memory_order_release); //(4)
return true;
}
return false; // full queue
}
这里是 Consumer
代码:
bool pop(Element& item)
{
const auto current_head = _head.load(std::memory_order_relaxed); //(1)
if(current_head == _tail.load(std::memory_order_acquire)) //(2)
return false; // empty queue
item = _array[current_head]; //(3)
_head.store(increment(current_head), std::memory_order_release); //(4)
return true;
}
问题
如果 push()
由于推测执行而被编译为以下函数怎么办:
bool push(const Element& item)
{
const auto current_tail = _tail.load(std::memory_order_relaxed); // 1
const auto next_tail = increment(current_tail);
//The load is performed before the test, it is valid
const auto head = _head.load(std::memory_order_acquire);
//Here is the speculation, the CPU speculate that the test will succeed
//store due to speculative execution AND it respects the memory order due to read-acquire
_array[current_tail] = item;
_tail.store(next_tail, std::memory_order_release);
//Note that in this case the test checks if you it has to restore the memory back
if(next_tail == head)//the code was next_tail != _head.load(std::memory_order_acquire)
{
//We restore the memory back but the pop may have been called before and see an invalid memory
_array[current_tail - 1] = item;
_tail.store(next_tail - 1, std::memory_order_release);
return true;
}
return false; // full queue
}
对我来说,为了完全有效,推送函数应该确保在条件成功之后发出障碍:
bool push(const Element& item)
{
const auto current_tail = _tail.load(std::memory_order_relaxed); // 1
const auto next_tail = increment(current_tail);
if(next_tail != _head.load(std::memory_order_relaxed)) // 2
{
//Here we are sure that nothing can be reordered before the condition
std::atomic_thread_fence(std::memory_order_acquire); //2.1
_array[current_tail] = item; // 3
_tail.store(next_tail, std::memory_order_release); // 4
return true;
}
return false; // full queue
}
回复:您建议的重新排序:不,编译器不能发明写入原子变量。
运行时推测也无法发明实际上对其他线程可见的写入。它可以将任何它想要的东西放入自己的私有存储缓冲区,但必须先检查早期分支的正确性,然后才能对其他线程可见存储。
通常这是通过 in-order 退役来实现的:一条指令只能退役(成为 non-speculative),前提是所有先前的指令都是 retired/non-speculative。在存储指令退出之前,存储不能从存储缓冲区提交到 L1d 缓存。
re: 标题:不,推测执行仍然要尊重内存模型。如果 CPU 想要推测性地加载一个不完整的 acquire-load,它可以,但前提是它 检查 以确保这些加载结果在它们'重新 "officially" 允许发生。
x86 CPUs do 实际上是这样做的,因为强大的 x86 内存模型意味着 all 负载是 acquire-loads,所以任何 out-of-order 加载都必须是推测性的,如果无效则回滚。 (这就是为什么你可以获得 memory-order mis-speculation 管道核武器。)
所以 asm 按照 ISA 规则规定的方式工作,C++ 编译器知道这一点。编译器使用它在目标 ISA 之上实现 C++ 内存模型。
如果你用 C++ 做一个 acquire-load,它真的可以作为一个 acquire-load。
您可以根据编写的 C++ 重新排序规则在心理上为可能的 compile-time + run-time 重新排序建立逻辑模型。参见 http://preshing.com/20120913/acquire-and-release-semantics/。