Подтвердить что ты не робот

Должен ли std:: atomic быть изменчивым?

Я запускаю поток, который выполняется до тех пор, пока не будет установлен флаг.

std::atomic<bool> stop(false);

void f() {
  while(!stop.load(std::memory_order_{relaxed,acquire})) {
    do_the_job();
  }
}

Интересно, может ли компилятор развернуть цикл таким образом (я не хочу, чтобы это произошло).

void f() {
  while(!stop.load(std::memory_order_{relaxed,acquire})) {
    do_the_job();
    do_the_job();
    do_the_job();
    do_the_job();
    ... // unroll as many as the compiler wants
  }
}

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

Должен ли я помещать volatile?


Прошу прощения за то, что вы неоднозначны. Я предполагаю, что я понимаю, что такое переупорядочение и что означает memory_order_*, и я уверен, что полностью понимаю, что такое volatile.

Я думаю, что цикл while() может быть преобразован как бесконечный оператор if, подобный этому.

void f() {
  if(stop.load(std::memory_order_{relaxed,acquire})) return;
  do_the_job();
  if(stop.load(std::memory_order_{relaxed,acquire})) return;
  do_the_job();
  if(stop.load(std::memory_order_{relaxed,acquire})) return;
  do_the_job();
  ...
}

Так как заданные порядки памяти не препятствуют перемещению последовательностей перед операцией после атомной нагрузки, я думаю, что она может быть перегруппирована, если она не будет изменяться.

void f() {
  if(stop.load(std::memory_order_{relaxed,acquire})) return;
  if(stop.load(std::memory_order_{relaxed,acquire})) return;
  if(stop.load(std::memory_order_{relaxed,acquire})) return;
  ...
  do_the_job();
  do_the_job();
  do_the_job();
  ...
}

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

void f() {
  if(stop.load(std::memory_order_{relaxed,acquire})) return;

  while(true) {
    do_the_job();
  }
}

Там никогда не будет такой безумной реализации, но я думаю, это все еще возможная ситуация. Я думаю, что единственный способ предотвратить это - поставить volatile на атомную переменную и спросить об этом.

Есть много догадок, которые я сделал, скажите, пожалуйста, если что-то не так.

4b9b3361

Ответ 1

Может ли компилятор кэшировать значение атомной переменной и развернуть цикл?

Компилятор не может кэшировать значение атомной переменной.

Однако, поскольку вы используете std::memory_order_relaxed, это означает, что компилятор может свободно переупорядочивать нагрузки и хранить от/до этой атомной переменной в отношении других загрузок и хранилищ.

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

Преобразование кода, которое вы хотели бы избежать, не было бы корректным преобразованием, поскольку это нарушило бы модель памяти С++: в первом случае у вас есть один заряд атомной переменной, за которым следует вызов do_the_job, во втором, у вас есть несколько вызовов. Наблюдаемое поведение преобразованного кода может быть различным.


И заметка std:: memory_order:

Связь с изменчивым

В потоке выполнения доступ (чтение и запись) ко всем энергозависимым объектам гарантированно не будет переупорядочен относительно друг друга, но этот порядок не гарантируется наблюдаемым другим потоком, поскольку волатильный доступ не устанавливает inter -прозрачная синхронизация.

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

Этот бит энергонезависимого доступа к памяти может быть свободно переупорядочен вокруг нестабильного доступа, это правда и для расслабленной атомистики, так как ослабленная загрузка и магазины могут быть переупорядочены в отношении других нагрузок и хранилищ.

Другими словами, украшение вашего атома с помощью volatile не повлияет на поведение вашего кода.


Независимо от того, что атомные переменные С++ 11 не должны быть помечены ключевыми словами volatile.


Вот пример того, как g++ - 5.2 отличает атомные переменные. Следующие функции:

__attribute__((noinline)) int f(std::atomic<int>& a) {
    return a.load(std::memory_order_relaxed);
}

__attribute__((noinline)) int g(std::atomic<int>& a) {
    static_cast<void>(a.load(std::memory_order_relaxed));
    static_cast<void>(a.load(std::memory_order_relaxed));
    static_cast<void>(a.load(std::memory_order_relaxed));
    return a.load(std::memory_order_relaxed);
}

__attribute__((noinline)) int h(std::atomic<int>& a) {
    while(a.load(std::memory_order_relaxed))
        ;
    return 0;
}

Скомпилированный с помощью g++ -o- -Wall -Wextra -S -march=native -O3 -pthread -std=gnu++11 test.cc | c++filt > test.S создает следующую сборку:

f(std::atomic<int>&):
    movl    (%rdi), %eax
    ret

g(std::atomic<int>&):
    movl    (%rdi), %eax
    movl    (%rdi), %eax
    movl    (%rdi), %eax
    movl    (%rdi), %eax
    ret

h(std::atomic<int>&):
.L4:
    movl    (%rdi), %eax
    testl   %eax, %eax
    jne .L4
    ret

Ответ 2

Если do_the_job() не изменяет stop, не имеет значения, может ли компилятор развернуть цикл или нет.

std::memory_order_relaxed просто гарантирует, что каждая операция является атомарной, но не препятствует переупорядочению доступа. Это означает, что если другой поток устанавливает stop в true, цикл может продолжать выполняться несколько раз, потому что обращения могут быть переупорядочены. Таким образом, это та же ситуация, что и при развернутом цикле: do_the_job() может выполняться несколько раз после того, как другой поток установил stop в true.

Нет, не используйте volatile, используйте std::memory_order_acquire и std::memory_order_release.