Результаты выполнения + = по двойному из нескольких потоков - PullRequest
0 голосов
/ 24 апреля 2020

Рассмотрим следующий код:

void add(double& a, double b) {
    a += b;
}

, который согласно godbolt компилируется на Skylake в:

add(double&, double):
  vaddsd xmm0, xmm0, QWORD PTR [rdi]
  vmovsd QWORD PTR [rdi], xmm0
  ret

Если я позвоню add(a, 1.23) и add(a, 2.34) из разных потоков (для одной и той же переменной a), определенно окажется либо + 1,23, + 2,34, либо + 1,23 + 2,34?

То есть произойдет ли один из этих результатов с данной сборкой, и a не окажется в каком-то другом состоянии?

1 Ответ

3 голосов
/ 24 апреля 2020

Вот уместные вопросы для меня:

Получает ли процессор слово, с которым вы работаете, в одной операции?

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

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

thread 1 fetches first part of a XXXX
thread 1 fetches second part of a YYYY
thread 2 fetches first part of a XXXX
thread 1 increments double represented as XXXXYYYY that becomes ZZZZWWWW by adding b
thread 1 writes back in memory ZZZZ
thread 1 writes back in memory WWWW
thread 2 fetches second part of a that is now WWWW
thread 2 increments double represented as XXXXWWWW that becomes VVVVPPPP by adding b
thread 2 writes back in memory VVVV
thread 2 writes back in memory PPPP

Для сохранения его компактности я использовал один символ для представления 8 битов.

Теперь XXXXWWWW и VVVVPPPP будут представлять общие значения с плавающей запятой, отличные от ожидаемых. Это потому, что вы в итоге смешали две части двух разных двоичных представлений ( IEEE-754 ) двойных переменных.

Сказал, что я знаю, что в некоторых архитектурах на основе ARM доступ к данным не разрешен (это может привести к генерации ловушек), но я подозреваю, что процессоры Intel разрешают это вместо этого.

Поэтому, если ваша переменная a выровнена, ваш результат может быть любым из

a + 1,23, a + 2,34, + 1,23 + 2,34

, если ваш переменная может быть выровнена неправильно (то есть имеет адрес, не кратный 8), ваш результат может быть любым из

a + 1,23, + 2,34, + 1,23 + 2,34 или rubbi sh значение


В качестве еще одного примечания, пожалуйста, имейте в виду, что даже если ваша среда alignof(double) == 8, этого не обязательно достаточно, чтобы сделать вывод, что вы не собирается иметь проблемы смещения. Все зависит от того, откуда берется ваша конкретная переменная. Рассмотрим следующее (или запустите здесь ):

#pragma push()
#pragma pack(1)
struct Packet
{
    unsigned char val1;
    unsigned char val2;
    double val3;
    unsigned char val4;
    unsigned char val5;
};
#pragma pop()


int main()
{
    static_assert(alignof(double) == 8);

    double d;
    add(d,1.23);       // your a parameter is aligned

    Packet p;
    add(p.val3,1.23);  // your a parameter is now NOT aligned

    return 0;
}

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

Пожалуйста, позвольте мне заявление об отказе для тех, кто еще читает этот ответ: использование std::atomic<double> в этих ситуациях является лучший компромисс с точки зрения усилий по реализации и производительности для достижения безопасности потоков. Существуют архитектуры ЦП, которые имеют специальные эффективные инструкции для работы с переменными atomi c без введения тяжелых ограждений. Это может в конечном итоге удовлетворить ваши требования к производительности.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...