C++ 0x модель памяти и спекулятивные загрузки/хранилища

Таким образом, я читал о модели памяти, которая является частью предстоящего C++ 0x стандарт. Однако я немного смущен некоторыми ограничениями для того, что компилятору позволяют сделать, конкретно о спекулятивных загрузках и хранилищах.

Прежде всего, часть соответствующего материала:

Страницы Hans Boehm о потоках и модели памяти в C++ 0x

Boehm, "Потоки не Может быть Реализован как Библиотека"

Boehm и Adve, "Основы модели памяти параллелизма C++"

Sutter, "призма: основанная на принципе последовательная модель памяти для Microsoft Native Code Platforms", N2197

Boehm, "Последствия компилятора модели памяти параллелизма", N2338

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

В свете этого я смущен ограничениями о побочных или спекулятивных загрузках/хранилищах на обычные совместно используемые переменные. Например, в N2338 у нас есть пример

switch (y) {
    case 0: x = 17; w = 1; break;
    case 1: x = 17; w = 3; break;
    case 2: w = 9; break;
    case 3: x = 17; w = 1; break;
    case 4: x = 17; w = 3; break;
    case 5: x = 17; w = 9; break;
    default: x = 17; w = 42; break;
}

в который компилятору не позволяют преобразовать

tmp = x; x = 17;
switch (y) {
    case 0: w = 1; break;
    case 1: w = 3; break;
    case 2: x = tmp; w = 9; break;
    case 3: w = 1; break;
    case 4: w = 3; break;
    case 5: w = 9; break;
    default: w = 42; break;
}

с тех пор, если y == 2 существует побочная запись к x, который мог бы быть проблемой, если бы другой поток одновременно обновлял x. Но, почему это - проблема? Это гонка данных, которая запрещается так или иначе; в этом случае компилятор просто делает это хуже путем записи в x дважды, но даже единственная запись была бы достаточно для гонки данных, нет? Т.е. надлежащий C++ 0x программа должен был бы синхронизировать доступ к x, в этом случае больше не будет гонки данных, и побочное хранилище не было бы проблемой также?

Я так же смущен Примером 3.1.3 в N2197 и некоторых из других примеров также, но возможно объяснение вышеупомянутой проблемы объяснило бы это также.

Править: Ответ:

Причина, почему спекулятивные хранилища являются проблемой, состоит в том, что в примере оператора переключения выше, программист, возможно, выбрал условно получать блокировку, защищающую x только если y! = 2. Следовательно спекулятивное хранилище могло бы представить гонку данных, которая не была там в исходном коде, и преобразование таким образом запрещается. Тот же аргумент относится к Примеру 3.1.3 в N2197 также.

24
задан janneb 5 January 2010 в 07:48
поделиться

2 ответа

Я не знаком со всем, на что вы ссылаетесь, но заметьте, что в случае y==2, в первом битке кода, x вообще не написан (или прочитан, если уж на то пошло). Во втором бите кода он написан дважды. Это больше разница, чем просто написать один раз против двух (по крайней мере, в существующих потоковых моделях, таких как pthreads). Кроме того, хранение значения, которое в противном случае вообще не хранилось бы, имеет большую разницу, чем просто запись один раз против записи дважды. По обеим этим причинам не следует, чтобы компиляторы просто заменяли no-op на tmp = x; x = 17; x = tmp;.

Предположим, поток A хочет предположить, что никакой другой поток не изменяет x. Разумно ожидать, что если y равен 2, а он записывает значение в x, то прочитает его обратно, и он получит обратно записанное значение. Но если поток B одновременно выполняет ваш второй бит кода, то поток A может написать в x, а затем прочитать его, и получить обратно исходное значение, потому что поток B сохранил "до" написания и восстановил "после" его. Или он может получить обратно 17, потому что поток B сохранил 17 "после" записи и сохранил tmp обратно "после" чтения потока A. Поток A может делать любую синхронизацию, которая ему нравится, и это не поможет, потому что поток B не синхронизирован. Причина, по которой он не синхронизирован (в случае y==2), заключается в том, что он не использует x. Таким образом, понятие о том, что конкретный бит кода "использует x", важно для модели потоков, а это значит, что компиляторы не могут позволить изменить код, чтобы использовать x, когда он "не должен".

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

Итак, хотя я не знаком с определением "гонки данных" в Си++0x, я предполагаю, что оно включает в себя некоторые условия, при которых программисту разрешается предполагать, что объект не записывается, и что это преобразование нарушит эти условия. Я предполагаю, что если y==2, то ваш исходный код вместе с параллельным: x = 42; x = 1; z = x в другом потоке, не определяется как гонка данных. Или, по крайней мере, если это гонка данных, то она не позволяет z закончить значением 17 или 42.

Считайте, что в этой программе значение 2 в y может быть использовано, чтобы указать, что "выполняются другие потоки: не модифицируйте x, потому что здесь мы не синхронизированы, так что это приведет к гонке данных". Возможно, причина, по которой синхронизация вообще отсутствует, в том, что во всех остальных случаях y нет других потоков, выполняющихся с доступом к x. Мне кажется разумным, что C++0x захотел бы поддерживать такой код:

if (single_threaded) {
    x = 17;
} else {
    sendMessageThatSafelySetsXTo(17);
}

Очевидно, что в таком случае вы не захотите, чтобы это преобразовывалось:

tmp = x;
x = 17;
if (!single_threaded) {
    x = tmp;
    sendMessageThatSafelySetsXTo(17);
}

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

.
8
ответ дан 29 November 2019 в 00:27
поделиться

Если y==2, а другой поток модифицирует или считывает x, то каким образом в исходном примере присутствует состояние гонки? Этот поток никогда не касается x, поэтому другие потоки могут делать это свободно.

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

5
ответ дан 29 November 2019 в 00:27
поделиться
Другие вопросы по тегам:

Похожие вопросы: