c++ - 由于获取-释放内存顺序而错过了优化机会或所需的行为?

标签 c++ multithreading llvm atomic memory-barriers

我目前正在尝试提高自定义“伪”堆栈的性能,它是这样使用的(本文末尾提供了完整代码):

void test() {
  theStack.stackFrames[1] = StackFrame{ "someFunction", 30 };      // A
  theStack.stackTop.store(1, std::memory_order_seq_cst);           // B
  someFunction();                                                  // C
  theStack.stackTop.store(0, std::memory_order_seq_cst);           // D

  theStack.stackFrames[1] = StackFrame{ "someOtherFunction", 35 }; // E
  theStack.stackTop.store(1, std::memory_order_seq_cst);           // F
  someOtherFunction();                                             // G
  theStack.stackTop.store(0, std::memory_order_seq_cst);           // H
}

采样器线程定期挂起目标线程并读取 stackTopstackFrames 数组。

我最大的性能问题是 stackTop 的顺序一致存储,因此我试图找出是否可以将它们更改为发布存储。

中心需求是:当sampler线程挂起目标线程,读取stackTop == 1时,那么stackFrames[1]中的信息需要完整呈现并且一致。这意味着:

  1. 当 B 被观察到时,A 也必须被观察到。 (“在放置堆栈帧之前不要增加 stackTop。”)
  2. 当 E 被观察到时,D 也必须被观察到。 ("当放置下一帧的信息时,前一个堆栈帧必须已经退出。")

我的理解是,对 stackTop 使用释放-获取内存排序可以保证第一个要求,但不能保证第二个要求。更具体地说:

  • 在程序顺序中 stackTop 发布存储之前的任何写入都不能重新排序以发生在它之后。

但是,对于按程序顺序将发布存储到 stackTop之后 发生的写入,没有任何声明。因此,我的理解是可以在观察到 D 之前观察到 E。这是正确的吗?

但如果是这样的话,那么编译器就不能像这样重新排序我的程序了:

void test() {
  theStack.stackFrames[1] = StackFrame{ "someFunction", 30 };      // A
  theStack.stackTop.store(1, std::memory_order_release);           // B
  someFunction();                                                  // C

  // switched D and E:
  theStack.stackFrames[1] = StackFrame{ "someOtherFunction", 35 }; // E
  theStack.stackTop.store(0, std::memory_order_release);           // D

  theStack.stackTop.store(1, std::memory_order_release);           // F
  someOtherFunction();                                             // G
  theStack.stackTop.store(0, std::memory_order_release);           // H
}

...然后组合 D 和 F,优化掉零存储?

因为如果我在 macOS 上使用系统 clang 编译上述程序,那不是我所看到的:

$ clang++ -c main.cpp -std=c++11 -O3 && objdump -d main.o

main.o: file format Mach-O 64-bit x86-64

Disassembly of section __TEXT,__text:
__Z4testv:
       0:   55  pushq   %rbp
       1:   48 89 e5    movq    %rsp, %rbp
       4:   48 8d 05 5d 00 00 00    leaq    93(%rip), %rax
       b:   48 89 05 10 00 00 00    movq    %rax, 16(%rip)
      12:   c7 05 14 00 00 00 1e 00 00 00   movl    $30, 20(%rip)
      1c:   c7 05 1c 00 00 00 01 00 00 00   movl    $1, 28(%rip)
      26:   e8 00 00 00 00  callq   0 <__Z4testv+0x2B>
      2b:   c7 05 1c 00 00 00 00 00 00 00   movl    $0, 28(%rip)
      35:   48 8d 05 39 00 00 00    leaq    57(%rip), %rax
      3c:   48 89 05 10 00 00 00    movq    %rax, 16(%rip)
      43:   c7 05 14 00 00 00 23 00 00 00   movl    $35, 20(%rip)
      4d:   c7 05 1c 00 00 00 01 00 00 00   movl    $1, 28(%rip)
      57:   e8 00 00 00 00  callq   0 <__Z4testv+0x5C>
      5c:   c7 05 1c 00 00 00 00 00 00 00   movl    $0, 28(%rip)
      66:   5d  popq    %rbp
      67:   c3  retq

具体来说,2b 处的 movl $0, 28(%rip) 指令仍然存在。

巧合的是,这个输出正是我所需要的。但我不知道我是否可以依赖它,因为据我所知,我选择的内存顺序不能保证它。

所以我的主要问题是:获取-释放内存顺序是否为我提供了另一个我不知道的(幸运的)保证?还是编译器只是偶然地做了我需要的事情/因为它没有尽可能地优化这个特殊情况?

完整代码如下:

// clang++ -c main.cpp -std=c++11 -O3 && objdump -d main.o

#include <atomic>
#include <cstdint>

struct StackFrame
{
  const char* functionName;
  uint32_t lineNumber;
};

struct Stack
{
  Stack()
    : stackFrames{ StackFrame{ nullptr, 0 }, StackFrame{ nullptr, 0 } }
    , stackTop{0}
  {
  }

  StackFrame stackFrames[2];
  std::atomic<uint32_t> stackTop;
};

Stack theStack;

void someFunction();
void someOtherFunction();

void test() {
  theStack.stackFrames[1] = StackFrame{ "someFunction", 30 };
  theStack.stackTop.store(1, std::memory_order_release);
  someFunction();
  theStack.stackTop.store(0, std::memory_order_release);

  theStack.stackFrames[1] = StackFrame{ "someOtherFunction", 35 };
  theStack.stackTop.store(1, std::memory_order_release);
  someOtherFunction();
  theStack.stackTop.store(0, std::memory_order_release);
}

/**
 * // Sampler thread:
 *
 * #include <chrono>
 * #include <iostream>
 * #include <thread>
 *
 * void suspendTargetThread();
 * void unsuspendTargetThread();
 * 
 * void samplerThread() {
 *   for (;;) {
 *     // Suspend the target thread. This uses a platform-specific
 *     // mechanism:
 *     //  - SuspendThread on Windows
 *     //  - thread_suspend on macOS
 *     //  - send a signal + grab a lock in the signal handler on Linux
 *     suspendTargetThread();
 * 
 *     // Now that the thread is paused, read the leaf stack frame.
 *     uint32_t stackTop =
 *       theStack.stackTop.load(std::memory_order_acquire);
 *     StackFrame& f = theStack.stackFrames[stackTop];
 *     std::cout << f.functionName << " at line "
 *               << f.lineNumber << std::endl;
 * 
 *     unsuspendTargetThread();
 * 
 *     std::this_thread::sleep_for(std::chrono::milliseconds(1));
 *   }
 * }
 */

并且,为了满足好奇心,如果我使用顺序一致的存储,这是程序集:

$ clang++ -c main.cpp -std=c++11 -O3 && objdump -d main.o

main.o: file format Mach-O 64-bit x86-64

Disassembly of section __TEXT,__text:
__Z4testv:
       0:   55  pushq   %rbp
       1:   48 89 e5    movq    %rsp, %rbp
       4:   41 56   pushq   %r14
       6:   53  pushq   %rbx
       7:   48 8d 05 60 00 00 00    leaq    96(%rip), %rax
       e:   48 89 05 10 00 00 00    movq    %rax, 16(%rip)
      15:   c7 05 14 00 00 00 1e 00 00 00   movl    $30, 20(%rip)
      1f:   41 be 01 00 00 00   movl    $1, %r14d
      25:   b8 01 00 00 00  movl    $1, %eax
      2a:   87 05 20 00 00 00   xchgl   %eax, 32(%rip)
      30:   e8 00 00 00 00  callq   0 <__Z4testv+0x35>
      35:   31 db   xorl    %ebx, %ebx
      37:   31 c0   xorl    %eax, %eax
      39:   87 05 20 00 00 00   xchgl   %eax, 32(%rip)
      3f:   48 8d 05 35 00 00 00    leaq    53(%rip), %rax
      46:   48 89 05 10 00 00 00    movq    %rax, 16(%rip)
      4d:   c7 05 14 00 00 00 23 00 00 00   movl    $35, 20(%rip)
      57:   44 87 35 20 00 00 00    xchgl   %r14d, 32(%rip)
      5e:   e8 00 00 00 00  callq   0 <__Z4testv+0x63>
      63:   87 1d 20 00 00 00   xchgl   %ebx, 32(%rip)
      69:   5b  popq    %rbx
      6a:   41 5e   popq    %r14
      6c:   5d  popq    %rbp
      6d:   c3  retq

仪器将 xchgl 指令确定为最昂贵的部分。

最佳答案

你可以这样写:

void test() {
  theStack.stackFrames[1] = StackFrame{ "someFunction", 30 };      // A
  theStack.stackTop.store(1, std::memory_order_release);           // B
  someFunction();                                                  // C
  theStack.stackTop.exchange(0, std::memory_order_acq_rel);        // D

  theStack.stackFrames[1] = StackFrame{ "someOtherFunction", 35 }; // E
  theStack.stackTop.store(1, std::memory_order_release);           // F
  someOtherFunction();                                             // G
  theStack.stackTop.exchange(0, std::memory_order_acq_rel);        // H
}

这应该提供您正在寻找的第二个保证,即在 D 之前可能不会观察到 E。否则我认为编译器将有权按照您的建议重新排序指令。

由于采样器线程“获取”stackTop 并在读取之前挂起目标线程,这应该提供额外的同步,因此当 stackTop 为 1 时它应该始终看到有效数据。

如果你的采样器没有挂起目标线程,或者如果挂起不等待线程实际挂起(检查这个),我认为需要一个互斥锁或等效物来防止采样器在读取后读取陈旧数据堆栈顶部作为一个(例如,如果它在错误的时刻被调度程序暂停)。

如果您可以依靠挂起来提供同步并且只需要限制编译器的重新排序,您应该看看 std::atomic_signal_fence

关于c++ - 由于获取-释放内存顺序而错过了优化机会或所需的行为?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45403712/

相关文章:

c++ - 根据 id 从列表中删除结构

c++ - 这被认为是显式 C++ 模板类实例化吗?

c++ - 未定义的 C/C++ 符号作为运算符

java - 进程内通信方式

Java 连接从 jar 失败,但在 Eclipse 中正常

ubuntu - Clang 格式给出错误

c++ - 错误 : Must have class type

python - 为什么多线程Python程序在ec2微实例上速度很慢?

visual-studio - 让 msbuild 使用 MT_StaticRelease

llvm - 函数指针作为要调用的参数