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

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

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

Для начала, некоторые из соответствующих материалов:

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

Boehm, "Нити не могут быть реализованы как библиотека"

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

Sutter, "Prism: основанная на принципе основанная модель памяти для платформ корпоративного ядра Microsoft" , N2197

Boehm, "Concurrency последствия компилятора модели памяти" , 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 в два раза, но даже одной записи будет достаточно для гонки данных, нет? То есть правильной программе С++ 0x необходимо синхронизировать доступ к x, и в этом случае больше не будет расы данных, и ложный магазин тоже не будет проблемой?

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

РЕДАКТИРОВАТЬ: Ответ:

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

4b9b3361

Ответ 1

Я не знаком со всем, на что вы ссылаетесь, но обратите внимание, что в случае 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. Thread 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. Мне кажется разумным, что С++ 0x хотел бы поддерживать такой код:

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

Ясно, что вы не хотите, чтобы это преобразовало:

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

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

Ответ 2

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

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