我目前正在尝试提高自定义“伪”堆栈的性能,这样使用(本帖末尾提供完整代码):
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
}
采样器线程定期挂起目标线程并读取stackTop
和stackFrames
数组。
我最大的性能问题是顺序一致的商店stackTop
,所以我试图找出是否可以将它们更改为发布商店。
中心要求是:当采样器线程挂起目标线程并读取stackTop == 1
时,stackFrames[1]
中的信息需要完全存在且一致。这意味着:
stackTop
。”)我的理解是,对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
指令确定为最昂贵的部分。
答案 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