无锁的单个生产者/单个使用者循环缓冲区-CPU推测能否打破内存屏障逻辑?

时间:2019-01-20 17:23:31

标签: c++ multithreading c++11 atomic lock-free

当我考虑投机执行及其对简单代码的影响时,我一直在研究无锁的单个生产者/单个消费者循环缓冲区。

通过此实现,只有一个唯一的线程可以调用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
}

1 个答案:

答案 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/