当我考虑投机执行及其对简单代码的影响时,我一直在研究无锁的单个生产者/单个消费者循环缓冲区。
通过此实现,只有一个唯一的线程可以调用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
}
答案 0 :(得分:2)
re:您建议的重新排序:不,编译器无法发明对原子变量的写操作。
运行时推测也无法发明出实际上对其他线程可见的写入。它可以将所需的任何内容放入自己的私有存储缓冲区中,但是必须先检查早期分支的正确性,然后其他线程才能看到存储。
通常这是按顺序退休的:只有在所有先前的指令都退休/不投机之后,一条指令才可以退休(成为不投机)。直到存储指令退出后,存储才可以从存储缓冲区提交到L1d缓存。
re:标题:不,推测执行仍然必须遵守内存模型。如果CPU要以推测方式加载超过不完整的获取负载,则可以,但只有检查以确保在“正式”允许发生加载结果时这些加载结果仍然有效。 / p> 实际上,
x86 CPU do 会这样做,因为强大的x86内存模型意味着 all 都是获取负载,因此任何无序加载都必须投机,如果无效则回滚。 (这就是为什么您会得到内存顺序错误推测管道核武器的原因。)
所以asm以ISA规则说的方式工作,而C ++编译器知道这一点。编译器使用它来在目标ISA之上实现C ++内存模型。
如果您使用C ++进行获取加载,则它实际上可以作为获取加载。
您可以根据编写的C ++重新排序规则在逻辑上为可能的编译时+运行时重新排序建模逻辑。参见http://preshing.com/20120913/acquire-and-release-semantics/。