Упущенная возможность оптимизации или требуемое поведение из-за упорядочения памяти для выпуска-выпуска?

В настоящее время я пытаюсь улучшить производительность пользовательского «псевдо» стека, который используется следующим образом (полный код приведен в конце этого поста):

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] должен быть полностью присутствующим и последовательным. Это означает:

  1. Когда наблюдается B, также необходимо соблюдать A. («Не увеличивайте stackTop перед установкой кадра стека. «)
  2. Когда E наблюдается, D также должен соблюдаться. («При размещении информации о следующем кадре предыдущий кадр стека должен быть завершен».)

Насколько я понимаю, использование упорядочивания памяти для упорядочения stackTop гарантирует первое требование, но не второе. Более конкретно:

  • Не пишет, что до stackTop release-store в программном порядке может быть переупорядочен, чтобы происходить после него.

Тем не менее, не делается никаких заявлений о записи, которые происходят после релиз-магазин для 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

В частности, 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

Решение

Вы можете написать это так:

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.

Если ваш сэмплер не приостановил целевой поток или если приостановка не ожидает фактического приостановления потока (проверьте это), я думаю, что потребуется мьютекс или эквивалент, чтобы предотвратить сэмплер от чтения устаревших данных после чтения вершины стека как один (пример, если он был приостановлен планировщиком в неподходящий момент).

Если вы можете полагаться на приостановку для обеспечения синхронизации и вам просто нужно ограничить переупорядочение компилятором, вы должны взглянуть на станд :: atomic_signal_fence

1

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]