Как я могу сделать код, который одновременно читает и изменяет массив, четко определенным, не вводя блокировку?

Я пишу программу, которая вычисляет таблицу эндшпиля для шахматного варианта. Алгоритм заполнения табличной базы работает так:

  1. Начните с огромного массива unsigned char, каждый элемент которого представляет одну позицию (мы всегда предполагаем, что это ход белых). Элемент массива является четным, если позиция проиграна, нечетным, если она выиграна, 0xff, если она недействительна, 0xfe, если позиция ничья.
  2. Пройдитесь по массиву, отмечая каждую недопустимую позицию 0xff, каждую позицию, где белые матируются 0x00, и все остальные позиции 0x0fe.
  3. Перебрать массив, учитывая только позиции, отмеченные 0xfe. Проверьте, есть ли ход, ведущий к позиции, элемент массива которой четный, если он есть, запишите единицу плюс номер этой позиции в элемент, соответствующий текущей позиции. Если все ходы ведут к позициям, обозначенным нечетными числами (т. е. это проигрышная позиция), отметьте эту позицию как единицу плюс наибольшее из этих чисел, чтобы указать, сколько времени занимает самая сильная защита.
  4. Повторяйте третий шаг, пока массив больше не изменится.

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

  • Разделите массив на n частей, пусть над каждой частью работает один поток. Пусть текущая итерация будет i.
  • Если поток должен прочитать элемент из массива, и он равен i, обработайте его так, как если бы он был установлен в 0xfe, потому что это означает, что элемент был только что записан параллельно другим потоком.

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

Что я могу сделать, чтобы распараллелить этот алгоритм, не нарушая никаких ограничений модели памяти C и не вызывая значительного замедления (например, путем добавления блокировок)?

Упрощенное описание проблемы

Вот упрощенный алгоритм, демонстрирующий ту же концепцию, но лишенный всего лишнего:

  1. Начните с массива unsigned char a[n]. Каждый элемент массива равен либо 0, либо 1.
  2. Для каждого элемента массива, для которого установлено значение 0: Если некоторые другие элементы массива равны 0 или 2, установите для этого члена массива значение 2. Проверяемые элементы массива зависят от индекса элемента массива, который мы хотим обновить. Между индексом и элементами массива, которые нам нужно проверить, нет простой связи, она по существу случайна.

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


person fuz    schedule 25.07.2016    source источник
comment
Это интересный вопрос. Как массив был заполнен с самого начала? Если вы передаете ответственность за кусок массива потоку, есть ли что-то еще, касающееся массива в то же время?   -  person EvilTeach    schedule 25.07.2016
comment
@EvilTeach Массив изначально заполняется на втором этапе неупрощенного алгоритма. Каждый фрагмент массива записывается только потоком, ответственным за этот фрагмент, но будет случайным образом считываться всеми другими потоками.   -  person fuz    schedule 25.07.2016
comment
Я бы дублировал вашу таблицу n раз и позже объединил бы результаты в одну таблицу.   -  person jxh    schedule 25.07.2016
comment
@jxh Это невозможно сделать из-за огромного размера таблицы (несколько гигабайт).   -  person fuz    schedule 25.07.2016
comment
Вероятно, вы могли бы разложить таблицу так, чтобы только те части, которые требуют изменения, были абстрагированы в меньшую таблицу, а на остальные ссылались из общей таблицы, доступной только для чтения. Но см. мой комментарий к ответу Криса.   -  person jxh    schedule 25.07.2016
comment
Теперь, когда я думаю об этом, вам нужно только две копии. Потоки заполняют новую копию, а читают со старой.   -  person jxh    schedule 25.07.2016
comment
@jxh Сохранение массива даже дважды - не вариант. Он просто слишком велик. Мне потребовалось много времени, чтобы хотя бы раз убедиться, что он помещается в оперативную память моего компьютера.   -  person fuz    schedule 25.07.2016


Ответы (1)


Именно для этого используется квалификатор типа _Atomic в C11. Вы бы объявили свой массив как

_Atomic unsigned char a[n];

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

До C11 не было стандартного способа сделать это, но обычно, в зависимости от реализации, определенные типы данных будут атомарными для чтения и записи. Чтобы узнать, что это такое, вам придется просмотреть документацию по реализации, которую вы используете.


Обратите внимание, что порядок доступа к памяти по умолчанию для C11 _Atomic равен memory_order_seq_cst (последовательная согласованность), и если вам это не нужно, вы можете использовать действия atomic_load_explicit и atomic_store_explicit с более слабым порядком памяти (т.е. memory_order_relaxed в вашем примере)

person Chris Dodd    schedule 25.07.2016
comment
Это возможно, но тогда компилятор создает барьер памяти, что мне не нужно (мне не нужны обновленные данные). - person fuz; 25.07.2016
comment
Возможно atomic_load_explicit() с memory_order_relaxed? - person jxh; 25.07.2016
comment
@jxh У груза нет барьера, он есть только у магазина. Хотя, если я ослаблю порядок памяти, это может сработать. - person fuz; 25.07.2016
comment
@jxh memory_order_relaxed как при загрузке, так и при сохранении делает свое дело. Не могли бы вы опубликовать это как ответ? - person fuz; 26.07.2016
comment
@FUZxxl: Крис уже обновил ответ. У меня нет проблем с тем, что вы принимаете ответ Криса. - person jxh; 26.07.2016