Wenn einige Codes mit Klirren 3.9.1 und Optimierungen kompilieren (-O2) Ich lief in einig unerwartetes Verhalten zur Laufzeit, die ich nicht mit anderen Compilern (Klirren 3.8 und gcc 6.3) gesehen.Mapping C++ zu Montage
Ich dachte, ich könnte ein unbeabsichtigtes undefiniertes Verhalten haben (das Kompilieren mit Ubsan entfernt das unerwartete Verhalten), also habe ich versucht, das Programm zu vereinfachen und festgestellt, dass eine bestimmte Funktion die Unterschiede im Verhalten verursacht.
Nun, ich Abbilden der Baugruppe c zurück ++, um zu sehen, wo es schief geht, um zu versuchen und herauszufinden, warum dies geschieht und es gibt ein paar Teile ich Schwierigkeiten Mapping zurück habe.
C++:
#include <atomic>
#include <cstdint>
#include <cstdlib>
#include <thread>
#include <cstdio>
enum class FooState { A, B };
struct Foo {
std::atomic<std::int64_t> counter{0};
std::atomic<std::int64_t> counter_a{0};
std::atomic<std::int64_t> counter_b{0};
};
//__attribute__((noinline))
FooState to_state(const std::int64_t c) {
return c >= 0 ? FooState::A : FooState::B;
}
static const int NUM_MODIFIES = 100;
int value_a = 0, value_b = 0;
Foo foo;
std::atomic<std::int64_t> total_sum{0};
void test_function() {
bool done = false;
while (!done) {
const std::int64_t count =
foo.counter.fetch_add(1, std::memory_order_seq_cst);
const FooState state = to_state(count);
int &val = FooState::A == state ? value_a : value_b;
if (val == NUM_MODIFIES) {
total_sum += val;
done = true;
}
std::atomic<std::int64_t> &c =
FooState::A == state ? foo.counter_a : foo.counter_b;
c.fetch_add(1, std::memory_order_seq_cst);
}
}
Montage:
test_function(): # @test_function()
test rax, rax
setns al
lock
inc qword ptr [rip + foo]
mov ecx, value_a
mov edx, value_b
cmovg rdx, rcx
cmp dword ptr [rdx], 100
je .LBB1_3
mov ecx, foo+8
mov edx, value_a
.LBB1_2: # =>This Inner Loop Header: Depth=1
test al, 1
mov eax, foo+16
cmovne rax, rcx
lock
inc qword ptr [rax]
test rax, rax
setns al
lock
inc qword ptr [rip + foo]
mov esi, value_b
cmovg rsi, rdx
cmp dword ptr [rsi], 100
jne .LBB1_2
.LBB1_3:
lock
add qword ptr [rip + total_sum], 100
test al, al
mov eax, foo+8
mov ecx, foo+16
cmovne rcx, rax
lock
inc qword ptr [rcx]
ret
ich gefunden habe, dass to_state
als noinline
Markierung oder das Ändern done
global zu sein scheint das unerwartete Verhalten zu "reparieren".
Das unerwartete Verhalten, das ich gesehen habe ist, dass, wenn der Zähler ist> = 0, dann sollte counter_a sonst erhöht wird counter_b erhöht werden soll. Von dem, was ich manchmal sagen kann, passiert das nicht, sondern genau zu bestimmen, wann/warum es schwierig war.
Ein Teil der Baugruppe, die ich verwenden könnte, sind die test rax, rax; setns al
und die test al, 1
Teile. Es sieht so aus, als würde der Anfangstest al
nicht deterministisch setzen und dann wird dieser Wert verwendet, um zu bestimmen, welcher Zähler erhöht werden soll, aber vielleicht verstehe ich etwas falsch.
Unten ist ein eher kleines Beispiel dieses Problem zu demonstrieren. Es bleibt normalerweise für immer hängen, wenn es mit clang 3.9 und -O2 kompiliert wird und läuft sonst bis zum Abschluss.
#include <atomic>
#include <cstdint>
#include <cstdlib>
#include <thread>
#include <cstdio>
enum class FooState { A, B };
struct Foo {
std::atomic<std::int64_t> counter{0};
std::atomic<std::int64_t> counter_a{0};
std::atomic<std::int64_t> counter_b{0};
};
//__attribute__((noinline))
FooState to_state(const std::int64_t c) {
return c >= 0 ? FooState::A : FooState::B;
}
//__attribute__((noinline))
FooState to_state2(const std::int64_t c) {
return c >= 0 ? FooState::A : FooState::B;
}
static const int NUM_MODIFIES = 100;
int value_a = 0, value_b = 0;
Foo foo;
std::atomic<std::int64_t> total_sum{0};
void test_function() {
bool done = false;
while (!done) {
const std::int64_t count =
foo.counter.fetch_add(1, std::memory_order_seq_cst);
const FooState state = to_state(count);
int &val = FooState::A == state ? value_a : value_b;
if (val == NUM_MODIFIES) {
total_sum += val;
done = true;
}
std::atomic<std::int64_t> &c =
FooState::A == state ? foo.counter_a : foo.counter_b;
c.fetch_add(1, std::memory_order_seq_cst);
}
}
int main() {
std::thread thread = std::thread(test_function);
for (std::size_t i = 0; i <= NUM_MODIFIES; ++i) {
const std::int64_t count =
foo.counter.load(std::memory_order_seq_cst);
const FooState state = to_state2(count);
unsigned log_count = 0;
auto &inactive_val = FooState::A == state ? value_b : value_a;
inactive_val = i;
if (FooState::A == state) {
foo.counter_b.store(0, std::memory_order_seq_cst);
const auto accesses_to_wait_for =
foo.counter.exchange((std::numeric_limits<std::int64_t>::min)(),
std::memory_order_seq_cst);
while (accesses_to_wait_for !=
foo.counter_a.load(std::memory_order_seq_cst)) {
std::this_thread::yield();
if(++log_count <= 10) {
std::printf("#1 wait_for=%ld, val=%ld\n", accesses_to_wait_for,
foo.counter_a.load(std::memory_order_seq_cst));
}
}
} else {
foo.counter_a.store(0, std::memory_order_seq_cst);
auto temp = foo.counter.exchange(0, std::memory_order_seq_cst);
std::int64_t accesses_to_wait_for = 0;
while (temp != INT64_MIN) {
++accesses_to_wait_for;
--temp;
}
while (accesses_to_wait_for !=
foo.counter_b.load(std::memory_order_seq_cst)) {
std::this_thread::yield();
if (++log_count <= 10) {
std::printf("#2 wait_for=%ld, val=%ld\n", accesses_to_wait_for,
foo.counter_b.load(std::memory_order_seq_cst));
}
}
}
std::printf("modify #%lu complete\n", i);
}
std::printf("modifies complete\n");
thread.join();
const std::size_t expected_result = NUM_MODIFIES;
std::printf("%s\n", total_sum == expected_result ? "ok" : "fail");
}
Warum betrachten Sie Assemblersprache Debugcode? Erstelle mvce und benutze einen Debugger? –
Sie sagen immer "unerwartetes Verhalten" *, aber ich bin immer noch unsicher, welches Verhalten Sie nicht erwarten? Kannst du das bitte klären? – UnholySheep
@UnholySheep Tut mir leid. Ich habe den Beitrag mit weiteren Informationen aktualisiert. – CTT