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

Что делает `std:: kill_dependency`, и зачем мне его использовать?

Я читал о новой модели памяти С++ 11, и я пришел к функции std::kill_dependency (& sect; 29.3/14-15). Я изо всех сил пытаюсь понять, почему я когда-нибудь захочу его использовать.

Я нашел пример в предложении N2664, но это мало помогло.

Он начинается с отображения кода без std::kill_dependency. Здесь первая строка переносит зависимость во вторую, которая несет зависимость в операцию индексирования, а затем переносит зависимость в функцию do_something_with.

r1 = x.load(memory_order_consume);
r2 = r1->index;
do_something_with(a[r2]);

В следующем примере используется std::kill_dependency, чтобы разбить зависимость между второй строкой и индексированием.

r1 = x.load(memory_order_consume);
r2 = r1->index;
do_something_with(a[std::kill_dependency(r2)]);

Насколько я могу судить, это означает, что индексирование и вызов do_something_with не являются зависимыми, упорядоченными до второй строки. Согласно N2664:

Это позволяет компилятору переупорядочить вызов do_something_with, например, путем выполнения спекулятивных оптимизаций, которые прогнозируют значение a[r2].

Чтобы сделать вызов do_something_with, необходимо значение a[r2]. Если, предположительно, компилятор "знает", что массив заполнен нулями, он может оптимизировать этот вызов на do_something_with(0); и изменить порядок этого вызова относительно двух других инструкций по своему усмотрению. Он может производить любой из:

// 1
r1 = x.load(memory_order_consume);
r2 = r1->index;
do_something_with(0);
// 2
r1 = x.load(memory_order_consume);
do_something_with(0);
r2 = r1->index;
// 3
do_something_with(0);
r1 = x.load(memory_order_consume);
r2 = r1->index;

Правильно ли я понимаю?

Если do_something_with синхронизируется с другим потоком каким-либо другим способом, что это означает в отношении упорядочения вызова x.load и этого другого потока?

Предполагая, что мое недооценка верна, все еще есть одна вещь, которая меня беспокоит: когда я пишу код, какие причины приведут меня к выбору убить зависимость?

4b9b3361

Ответ 1

Цель memory_order_consume - обеспечить, чтобы компилятор не выполнял некоторые неудачные оптимизации, которые могут нарушить блокирующие алгоритмы. Например, рассмотрите этот код:

int t;
volatile int a, b;

t = *x;
a = t;
b = t;

Соответствующий компилятор может преобразовать его в:

a = *x;
b = *x;

Таким образом, a может не равняться b. Он также может выполнять следующие действия:

t2 = *x;
// use t2 somewhere
// later
t = *x;
a = t2;
b = t;

Используя load(memory_order_consume), мы требуем, чтобы использование загружаемого значения не перемещалось до точки использования. Другими словами,

t = x.load(memory_order_consume);
a = t;
b = t;
assert(a == b); // always true

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

r1 = x.load(memory_order_consume);
r2 = r1->index;
do_something_with(a[std::kill_dependency(r2)]);

Это дает команду компилятору, чтобы это разрешило:

predicted_r2 = x->index; // unordered load
r1 = x; // ordered load
r2 = r1->index;
do_something_with(a[predicted_r2]); // may be faster than waiting for r2 value to be available

Или даже это:

predicted_r2 = x->index; // unordered load
predicted_a  = a[predicted_r2]; // get the CPU loading it early on
r1 = x; // ordered load
r2 = r1->index; // ordered load
do_something_with(predicted_a);

Если компилятор знает, что do_something_with не изменит результат нагрузок для r1 или r2, то он может даже полностью поднять его вверх:

do_something_with(a[x->index]); // completely unordered
r1 = x; // ordered
r2 = r1->index; // ordered

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

Ответ 2

В дополнение к другому ответу, я укажу, что Скотт Мейерс, один из окончательных лидеров в сообществе С++, сильно изменил memory_order_consume. Он в основном сказал, что считает, что в стандарте не было места. Он сказал, что есть два случая, когда memory_order_consume имеет какой-либо эффект:

  • Экзотические архитектуры, предназначенные для поддержки 1024-разрядных систем общей памяти.
  • DEC Alpha

Да, еще раз, DEC Alpha находит свой путь в позоре, используя оптимизацию, которая не виделась ни в одном другом чипе, пока много лет спустя на абсурдно специализированных машинах.

Особая оптимизация заключается в том, что эти процессоры позволяют разыменовывать поле до фактического получения адреса этого поля (т.е. он может искать x- > y, прежде чем он даже ищет x, используя предсказанное значение x). Затем он возвращается и определяет, является ли x значением, которое оно ожидало. Успех это сэкономило время. При отказе он должен вернуться и снова получить x- > y.

Memory_order_consume сообщает компилятору/архитектуре, что эти операции должны выполняться по порядку. Однако в наиболее полезном случае вы захотите сделать (x- > y.z), где z не изменится. memory_order_consume заставит компилятор сохранить x y и z в порядке. kill_dependency (x- > y).z сообщает компилятору/архитектуре, что он может возобновить выполнение таких гнусных переупорядочений.

99.999% разработчиков, вероятно, никогда не будут работать на платформе, где эта функция требуется (или вообще имеет какой-либо эффект).

Ответ 3

Обычный пример использования kill_dependency возникает из следующего. Предположим, вы хотите сделать атомарные обновления для нетривиальной общей структуры данных. Типичный способ сделать это состоит в том, чтобы неатомически создать некоторые новые данные и атомизировать качание указателя из структуры данных в новые данные. Как только вы это сделаете, вы не собираетесь менять новые данные, пока не отбросите указатель от него на что-то другое (и ждали, когда все читатели уйдут). Эта парадигма широко используется, например. read-copy-update в ядре Linux.

Теперь предположим, что читатель читает указатель, читает новые данные и возвращается позже, и снова читает указатель, обнаружив, что указатель не изменился. Аппаратное обеспечение не может сказать, что указатель не был обновлен повторно, поэтому в семантике consume он не может использовать кэшированную копию данных, но должен прочитать ее снова из памяти. (Или подумать об этом по-другому, аппаратное обеспечение и компилятор не могут спекулятивно переместить чтение данных до считывания указателя.)

Здесь kill_dependency приходит на помощь. Упаковывая указатель в kill_dependency, вы создаете значение, которое больше не будет распространять зависимость, позволяя доступ через указатель к использованию кэшированной копии новых данных.

Ответ 4

Я предполагаю, что он позволяет эту оптимизацию.

r1 = x.load(memory_order_consume);
do_something_with(a[r1->index]);