Записывает ли cmpxchg строку кэша назначения в случае сбоя? Если нет, то лучше xchg для спинлока?

Я предполагаю простую спин-блокировку, которая не относится к ОС, ожидая целей этого вопроса.

Я вижу, что простая спин-блокировка часто реализуется с использованием lock xchg или lock bts вместо lock cmpxchg.

Но разве cmpxchg не избегает записи значения, если ожидания не совпадают? Так разве неудачные попытки не обходятся дешевле с cmpxchg?

Или cmpxchg записывает данные и аннулирует строки кэша других ядер даже в случае сбоя?

Этот вопрос похож на Что конкретно помечает строку кэша x86 как грязную - любая запись или требуется явное изменение?, но это относится к cmpxchg, а не в целом.


person Alex Guteniev    schedule 21.07.2020    source источник
comment
Я думаю, что все атомарные RMW действительно считаются хранилищами, включая lock cmpxchg. По крайней мере, исторически (для видимых извне эффектов) felixcloutier.com/x86/cmpxchg говорит: i> Процессор никогда не производит заблокированное чтение без блокированной записи. Но это не исключает оптимизацию блокировки кэш-памяти для кэшируемой памяти в современных ЦП.   -  person Peter Cordes    schedule 21.07.2020
comment
Он должен, по крайней мере, перевести строку кэша в состояние E, сделав недействительными другие копии, прежде чем пытаться lock cmpxchg, и вот откуда берется стоимость при вращении на нем вместо вращения только для чтения, пока не станет похоже, что блокировка доступна. Переменная блокировки уже обычно будет грязной (не синхронизированной с DRAM)   -  person Peter Cordes    schedule 21.07.2020
comment
@ Питер, понятно. Тогда не имеет значения, оптимизируется ли фактический магазин или нет.   -  person Alex Guteniev    schedule 21.07.2020
comment
Или ... может быть, строка кэша все еще может быть разделена быстрее, если пропустить состояние M и не ждать завершения сохранения?   -  person Alex Guteniev    schedule 21.07.2020
comment
Если вы хотите, чтобы ядро ​​могло читать, но не записывать строку при проверке доступности блокировки, вращайте только для чтения с нагрузкой, отдельной от попытки CAS, xchg или lock bts. Это явно лучше, потому что это оставляет строку в состоянии S, а не E, и является (или должно быть) хорошо известным фактом среди разработчиков блокировки и других циклов вращения (наравне с использованием pause в части повторных попыток вращения). например Примером может служить блокировки манипулирования памятью с помощью встроенной сборки.   -  person Peter Cordes    schedule 21.07.2020
comment
У меня есть частично письменный ответ; Я пошел искать информацию о том, проверял ли кто-нибудь lock cmpxchg сбой, загрязняющий строку кеша. Я обнаружил стоимость атомарной операции, что делает интересный момент, что чистая загрузка + CAS может вызвать 2 промаха кеша: один для получения общего состояния для нагрузка, еще один, чтобы получить Эксклюзив. Я все еще почти уверен, что вращение только для чтения с pause после того, как я увидел, что он заблокирован, - хорошая идея, но я не совсем уверен, что чистая загрузка в качестве первой операции - хорошая идея. Чтобы ускорить рассмотрение дела, лучше всего начать с блокировки CAS.   -  person Peter Cordes    schedule 21.07.2020
comment
@FrancescoMenzani: Да, вращение только для чтения и попытка атомарного RMW только тогда, когда это кажется возможным, определенно лучше. (Спасибо за ссылку, подтверждающую это экспериментальными числами). Интересный вопрос заключается в том, сильно ли повредит то, что первая проверка будет доступна только для чтения, в ситуации с несколько низким уровнем конкуренции.   -  person Peter Cordes    schedule 22.07.2020
comment
@PeterCordes Как первая проверка может быть доступна только для чтения? Не могли бы вы уточнить точную реализацию?   -  person spongebob    schedule 22.07.2020
comment
@FrancescoMenzani: Нравится try_lock в сообщении, на которое вы ссылаетесь. if(load() == already_locked) goto read-only-spin-loop перед попыткой первого xchg или CAS. Блокировка манипуляций с памятью с помощью встроенной сборки, которую я связал ранее, написана таким образом (синтаксис NASM).   -  person Peter Cordes    schedule 22.07.2020
comment
Кто-то предлагает сначала сделать xchg, а затем ослабить нагрузку.   -  person spongebob    schedule 22.07.2020


Ответы (2)


На большинстве или всех современных процессорах Intel x86 lock cmpxchg в ячейку с типом памяти WB, полностью содержащуюся в одной строке кэша L1D, выполняется следующим образом:

  • L1D выдает запрос чтения с блокировкой, который переводит целевую строку в состояние согласованности кэш-памяти с блокировкой и исключительным доступом и предоставляет запрошенные байты в качестве входных данных для одного из портов выполнения для выполнения сравнения. (Блокировка кэша поддерживается, начиная с P6.) Линия в заблокированном состоянии не может быть аннулирована или исключена по любой причине.
  • Проведите сравнение на равенство.
  • Каким бы ни был результат, отправьте запрос разблокировки-записи к L1D, который изменяет состояние строки кэша на Modified и разблокирует строку, тем самым позволяя другим запросам доступа или согласованности заменить или сделать строку недействительной.

Первый и последний этапы можно наблюдать эмпирически, используя либо определенные события производительности, либо измерения на основе задержки. Один из способов - выделить большой массив атомарных переменных и затем выполнить lock cmpxchg в цикле над этим массивом. Тип запроса чтения с блокировкой - это один из типов запросов RFO. Таким образом, событие L2_TRANS.RFO (или что-то подобное), которое надежно для большинства микроархитектур, можно использовать для измерения количества чтений блокировки в L2. (L2_TRANS.RFO подсчитывает запросы RFO, поэтому лучше отключить аппаратные средства предварительной выборки, чтобы избежать нежелательных попаданий в L2. Это также относится к L2_RQSTS.RFO_*.)

Также есть события для измерения количества обратных записей, такие как L2_TRANS.L1D_WB, L2_TRANS.L2_WB и другие. К сожалению, многие из этих событий и во многих микроархитектурах либо занижены, либо превышены, либо подсчитываются точно, но не обязательно все / только обратные записи строк грязного кэша. Так что с ними труднее рассуждать, и в целом они ненадежны.

Лучшим способом было бы выполнить lock cmpxchg в одном разделе массива на определенном физическом ядре, затем перенести поток на другое физическое ядро ​​(в том же домене совместного использования L3) и выполнить цикл, в котором считываются элементы этого раздела ( нормально читает). Если инструкция lock cmpxchg переводит целевую строку в состояние M, запрос на чтение из другого физического ядра в том же домене совместного использования L3 должен попасть в L3, а также изменен в частных кэшах ядра, на котором было выполнено lock cmpxchg. Эти события можно подсчитать с помощью OFFCORE_RESPONSE.DEMAND_DATA_RD.L3_HIT.HITM_OTHER_CORE (или аналогичного), что надежно для большинства / всех микроархитектур.

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

У Intel есть патент, который предлагает оптимизацию для последнего, причем ядро ​​оптимистично предполагает, что есть не вызывает конкуренции за блокировку и вызывает спекулятивную нормальную нагрузку на целевую линию. Если линия отсутствует в каком-либо другом физическом ядре, линия будет в исключительном состоянии в запрашивающем ядре. Затем, когда заблокированная инструкция выполняется и выдает запрос чтения блокировки, линия, будем надеяться, все еще будет в исключительном состоянии, и в этом случае общая задержка заблокированной инструкции будет уменьшена. Я не знаю, реализует ли какой-либо процессор эту оптимизацию. Если бы это было реализовано, количество L2_TRANS.RFO событий было бы намного меньше, чем количество заблокированных строк.

person Hadi Brais    schedule 11.08.2020
comment
Если патент будет реализован, вероятно ли, что он будет реализован одинаково для всех заблокированных инструкций? - person Alex Guteniev; 11.08.2020
comment
@AlexGuteniev Да, это применимо ко всем. - person Hadi Brais; 11.08.2020

Я сделал несколько тестов. Однако очень синтетический, очень мало работал под замком и измерял пропускную способность очень спорного сценария.

До сих пор не наблюдалось устойчивого эффекта разницы между lock bts xchg или lock cmpxchg.

Однако кое-что повлияло и на другие вещи:

  • Внутренний load цикл определенно полезен, как с pause, так и без него.
  • Один pause в цикле полезен как с циклом загрузки, так и без него.
  • Цикл нагрузки помогает больше, чем просто пауза
  • Наилучшие результаты достигаются при применении улучшенной версии из Справочного руководства по оптимизации архитектур Intel® 64 и IA-32 (см. Ниже)
  • Запуск с нагрузкой вместо RMW / CAS имеет противоречивый эффект: он полезен для тестов без pause, но снижает производительность тестов с pause

Intel® Справочное руководство по оптимизации архитектур 64 и IA-32 рекомендует использовать pause.

Пример 2-4. Пример конкурирующих блокировок с увеличивающимся откатом показывает базовую версию:

/*******************/
/*Baseline Version */
/*******************/
// atomic {if (lock == free) then change lock state to busy}
while (cmpxchg(lock, free, busy) == fail)
{
 while (lock == busy)
 {
 __asm__ ("pause");
 }
}

и улучшенная версия:

/*******************/
/*Improved Version */
/*******************/
int mask = 1;
int const max = 64; //MAX_BACKOFF
while (cmpxchg(lock, free, busy) == fail)
{
 while (lock == busy)
 {
   for (int i=mask; i; --i){
     __asm__ ("pause");
   }
   mask = mask < max ? mask<<1 : max;
 }
}

Windows SRWLOCK также может быть хорошим примером для подражания. Он использует цикл загрузки и pause. он начинается с взаимосвязанной операции lock bts для монопольного получения, lock cmpxchg для совместного использования. Даже TryAcquireSRWLockExclusive делает только lock bts:

RtlTryAcquireSRWLockExclusive:
00007FFA86D71370  lock bts    qword ptr [rcx],0  
00007FFA86D71376  setae       al  
00007FFA86D71379  ret  

Однако он не реализует экспоненциально растущее pause количество ожидающих версий. Он выполняет несколько небольших нагрузок с одним pause, затем переходит в режим ожидания ОС.

person Alex Guteniev    schedule 06.08.2020
comment
Я предполагаю, что вы просто тестировали несколько потоков, ничего не делая, кроме попыток спама взять блокировку; IDK, если тест только для чтения перед первым атомарным RMW может качественно отличаться в (надеюсь) более типичной ситуации со средним или низким уровнем конкуренции. (Например, на самом деле лучше, чем просто менее плохо, в правильно написанной реализации с циклом вращения + pause только для чтения после сбоя.) Это всегда могло быть плохо, я не учел тот факт, что доступ только для чтения будет возможно, если линия будет в общем состоянии, тогда RMW потребуется RFO. - person Peter Cordes; 06.08.2020
comment
Попытка сначала использовать RMW - оптимистичный вариант, поэтому он вероятно даже лучше в случаях с низким уровнем конкуренции. - person Peter Cordes; 06.08.2020
comment
@PeterCordes, я увеличил общую переменную под блокировкой, чтобы использовать режим блокировки, и пару целочисленных делений снаружи, чтобы смоделировать что-то, что сделано не под блокировкой. Хотя, наверное, всего пара отделов - это не так уж и много. - person Alex Guteniev; 06.08.2020
comment
Если это 64-битные деления на процессоре Intel, это, возможно, начинает иметь значение, например, 24 цикла / 56 мопов для idiv r64 на SKL, хотя OoO exec может перекрывать микрокод div / idiv с выполнением микрокода инструкции locked. (В отличие от lfence, заблокированные инструкции - это только барьеры памяти, а не барьеры выполнения). - person Peter Cordes; 06.08.2020
comment
@PeterCordes, сделал их 64-битным делением, теперь разница менее значительна, но все же запуск с load немного хуже, и load и pause лучше, и рекомендация Intel работает лучше всего. Я все еще думаю, что штраф за первую загрузку достаточно мал, чтобы сделать это в try_lock, где отрицательный результат также является результатом. - person Alex Guteniev; 06.08.2020
comment
Я не хочу блокировать OoO в тесте, поскольку OoO также будет происходить в реальном коде, поэтому предположим, что инструкция lock перекрывается с окружающим кодом - person Alex Guteniev; 06.08.2020
comment
Гипотеза: вариант использования, в котором может выиграть первая загрузка, - это когда критическая секция занимает больше времени, поэтому более вероятно, что все несколько потоков увидят блокировку, взятую во время одного ее выполнения. Это означает, что они вызывают меньше конфликтов за поток разблокировки. (И, возможно, меньше общего трафика, но ваш тест не измеряет этого; может быть интересно что-то с ограниченной пропускной способностью на другом гиперпотоке каждого ядра). Или, может быть, нет, может быть, более длинный критический раздел дает время для того, чтобы все успокоилось, чтобы все другие потоки попали в свой цикл только для чтения, а не в середине RMW. - person Peter Cordes; 06.08.2020
comment
Правильно, вы не хотите блокировать OoO exec здесь, я просто указываю / думаю вслух о том, что, хотя пропускная способность idiv r64 составляет ~ 24 цикла, это не означает, что n * 24 циклов между разблокировкой и попыткой повторно захватить блокировку из-за OoO exec. Но реальные варианты использования могут заканчиваться сохранением в памяти прямо перед попыткой взятия блокировки, а не просто инструкциями ALU, которых должна ждать инструкция locked. (Из-за заказа магазина он должен истощить SB, в том числе ожидание, пока магазины в полете получат свои данные, удалиться и зафиксировать.) - person Peter Cordes; 06.08.2020
comment
Да, я думаю, что обычно вам нужно оптимизировать, чтобы случай без разрешения работал быстро, даже для функции TryLock. Надеюсь, что в большинстве программ это удается с первой попытки. Я задавался вопросом, может ли ЦП суметь обновить обычное чтение до RFO до того, как это будет слишком поздно (в этом случае не будет штрафов за производительность), но ваше тестирование, похоже, доказывает, что это не так. Спасибо за это; возможно, я закончу свой частичный ответ и опубликую его в какой-то момент. - person Peter Cordes; 06.08.2020