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

StackOverflow https://stackoverflow.com/questions/2001913

Вопрос

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

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

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

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

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

Саттер, "Призма:Основанная на принципах последовательная модель памяти для платформ Microsoft Native Code", N2197

Бем, "Последствия компилятора модели памяти параллелизма", 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 и некоторыми другими примерами, но, возможно, объяснение вышеупомянутой проблемы также объяснило бы это.

Редактировать:Ответ:

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

Это было полезно?

Решение

Я не знаком со всеми материалами, на которые вы ссылаетесь, но обратите внимание, что в случае 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 (или любую другую ячейку памяти).Существует ряд удобных идиом, которые поэтому были бы невозможны, например, совместное использование неизменяемых данных между потоками без синхронизации.

Итак, хотя я не знаком с определением C ++ 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);
}

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

Другие советы

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top