Что делает` std:: kill dependency', и почему я хочу его использовать?
я читал о новой модели памяти C++11, и я наткнулся на (§29.3/14-15). Я пытаюсь понять, почему я хотел бы использовать его.
я нашел пример в предложение N2664 но это не помогало.
он начинается с показа кода без std::kill_dependency. Здесь первая строка переносит зависимость во вторую, которая переносит зависимость в операцию индексирования, а затем переносит зависимость в элемент .
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 значение это. Если, гипотетически, компилятор "знает", что массив заполнен нулями, он может оптимизировать этот вызов до 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 звонок и этот другой поток?
предполагая, что мое понимание правильно, есть еще одна вещь, которая меня беспокоит: когда я пишу код, какие причины заставят меня выбрать, чтобы убить зависимость?
4 ответов:
цель 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's 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это позволяет компилятору немного больше свободы в ее оптимизации.
в дополнение к другому ответу я отмечу, что Скотт Мейерс, один из окончательных лидеров в сообществе C++, довольно сильно ударил memory_order_consume. Он в основном сказал, что, по его мнению, ему не было места в стандарте. Он сказал, что есть два случая, когда memory_order_consume имеет никакого эффекта:
- экзотические архитектуры, предназначенные для поддержки 1024 + основных машин с общей памятью.
- дек Альфа
да, еще раз, DEC Альфа находит свой путь в позор, используя оптимизацию, не замеченную ни в одном другом чипе, пока много лет спустя на абсурдно специализированных машинах.
конкретная оптимизация заключается в том, что эти процессоры позволяют разыменовать поле до фактического получения адреса этого поля (т. е. он может искать 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% разработчиков, вероятно, никогда не работа на платформе, где эта функция требуется (или имеет какой-либо эффект вообще).
обычный случай использования
kill_dependencyвытекает из следующего. Предположим, вы хотите сделать атомарные обновления нетривиальной общей структуры данных. Типичный способ сделать это-неатомически создать некоторые новые данные и атомарно качать указатель от структуры данных к новым данным. Как только вы это сделаете, вы не собираетесь изменять новые данные, пока вы не переместите указатель от него на что-то другое (и ждали, пока все читатели освободятся). Эта парадигма широко используется, например, чтение-копирование-обновление в ядре Linux.теперь предположим, что читатель читает указатель, читает новые данные и возвращается позже и снова читает указатель, обнаружив, что указатель не изменился. Аппаратное обеспечение не может сказать, что указатель не был обновлен снова, так что
consumeсемантика он не может использовать кэшированную копию данных, но должен прочитать ее снова из памяти. (Или думать об этом по-другому, аппаратное обеспечение и компилятор не могут спекулятивно перемещать чтение данных до чтение указателя.)вот тут
kill_dependencyприходит на помощь. Обернув указатель вkill_dependency, вы создаете значение, которое больше не будет распространять зависимость, позволяя доступу через указатель использовать кэшированную копию новых данных.
Я предполагаю, что это позволяет эту оптимизацию.
r1 = x.load(memory_order_consume); do_something_with(a[r1->index]);
Comments