由于获取释放内存排序而错过了优化机会或所需行为?

时间:2017-07-30 19:15:12

标签: 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,所以我试图找出是否可以将它们更改为发布商店。

中心要求是:当采样器线程挂起目标线程并读取stackTop == 1时,stackFrames[1]中的信息需要完全存在且一致。这意味着:

  1. 当观察到B时,还必须观察到A. (“在放置堆栈框架之前不要增加stackTop。”)
  2. 当观察到E时,还必须观察到D. (“当放置下一帧的信息时,必须退出前一个堆栈帧。”)
  3. 我的理解是,对stackTop使用release-acquire内存排序可以保证第一个要求,但不能保证第二个要求。更具体地说:

    • 在程序顺序中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
    

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

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

    所以我的主要问题是:获取 - 释放内存顺序是否给了我另一个(幸运的)保证,我不知道?或者编译器只是偶然地执行我需要的东西/因为它没有优化这个特殊情况,因为它可以吗?

    以下完整代码:

    // 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指令确定为最昂贵的部分。

1 个答案:

答案 0 :(得分:1)

你可以这样写:

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。否则我认为编译器将有权按照您的建议对指令重新排序。

由于采样器线程&#34;获取&#34; stackTop并且在读取之前挂起目标线程应该提供额外的同步,当stackTop为1时,它应该总是看到有效数据。

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

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