2017-07-30 27 views
1

我目前正試圖改善自定義「僞」棧,這樣使用(完整代碼在這篇文章的末尾提供)的性能:定期由於採集釋放內存排序而錯過了優化機會或所需行爲?

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也必須遵守。 (「到位時,把下一幀的信息,先前的堆棧幀必須已經退出。」)

我的理解是,用釋放獲取內存排序爲stackTop保證的第一要求,而不是第二。更具體地說:

  • 在程序順序中的stackTop版本存儲之前沒有寫入可以重新排序以在其之後發生。

不過,聲明沒有作出有關出現釋放,商店stackTop在程序順序寫入。因此,我的理解是E可以在觀察到D之前觀察到。它是否正確?

但是,如果是這樣的話,以後就不會編譯器能夠重新安排我的程序是這樣的:

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++ -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 

具體而言,在2bmovl $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說明作爲最昂貴的部分。

+0

我打算通過使'functionName'和'獲得第二個保障lineNumber'原子,以及使用它們的發佈商店。這似乎沒有影響生成的代碼,所以我認爲它會沒事的。我只是想知道這是否真的有必要。 –

+0

在討論二叉樹時,通常使用術語*葉*嗎?它在這裏意味着什麼? – VTT

+0

我會將它重命名爲「stackTop」以使其更清晰。 –

回答

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 
} 

這將提供您正在尋找的第二個擔保即E可以不D.之前觀察到的,否則我認爲編譯器將不得不重新排序的權利如你所建議的那樣。

由於採樣線程「收購」 stackTop,它的閱讀,應提供額外的同步前掛起目標線程,它應該永遠看到有效數據stackTop爲1

如果採樣不暫停目標線程,或者如果掛起不會等待線程實際掛起(請檢查此問題),我認爲在讀取堆棧頂部之後,必須使用互斥鎖或等效方法來阻止採樣器讀取陳舊數據(例如,如果它被掛起調度程序在錯誤的時刻)。

如果你可以依靠暫停提供同步和只需要由編譯器來約束重新排序,你應該看看std::atomic_signal_fence

+0

使零店也成爲一個收購行動是一個好主意,謝謝!至於線程掛起:Windows和Mac API等待線程被掛起,並且在Linux實現中,我們鎖定了信號處理程序中的互斥鎖,所以我認爲我們都很好。 爲了完整起見,我應該提到,我不相信將'memory_order_acq_rel'傳遞給'store'是有意義的;我想我可能需要調用'exchange'來代替。 (我正在修改的代碼是使用一個甚至不允許我將'memory_order_acq_rel'傳遞給'store'的API,所以調用'exchange'真的是我唯一的選擇。) –

+0

很高興我的錯誤答案爲您提供了一個有用的想法:-),編輯使用交換。但這是另一個跡象表明,這種使用內存排序是不尋常的。 – PaulR

+0

啊,事實證明x86上的'exchange(0,std :: memory_order_acq_rel)'編譯爲與'store(0,std :: memory_order_seq_cst)'相同的(慢)'xchgl'指令。所以我在這裏還沒有完成。我可以在商店之後進行獲取加載並忽略加載的值,我猜?不知道這是否有幫助。 –