Какова минимальная сборка X86, необходимая для спин-блокировки?

Чтобы реализовать спин-блокировку в сборке. Здесь я размещаю решение, которое я придумал. Это правильно? Знаете короче?

замок:

    mov ecx, 0
.loop:
    xchg [eax], ecx
    cmp ecx, 0
    je .loop

выпуск:

    lock dec dword [eax]

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


person melkyades    schedule 08.04.2014    source источник
comment
Примечание. xor ecx, ecx предпочтительнее mov ecx, 0 по размеру и скорости.   -  person Polynomial    schedule 08.04.2014
comment
С другой стороны, вы пропустили префикс блокировки из вашего первого xchg.   -  person Polynomial    schedule 08.04.2014
comment
@Polynomial нет необходимости в префиксе блокировки для xchg, это подразумевается.   -  person Jester    schedule 08.04.2014
comment
@Jester Ах да, я забыл об этом.   -  person Polynomial    schedule 08.04.2014
comment
Я полагаю, вы имели в виду, что [eax] инициализируется значением -1, то есть содержимым ячейки памяти, на которую указывает eax, а не самим eax. Когда вы говорите о минимальной сборке x86, вы имеете в виду количество инструкций, количество байтов или среднее ожидаемое количество циклов блокировки и разблокировки?   -  person amdn    schedule 08.04.2014
comment
stackoverflow.com/a/3855824/17034   -  person Hans Passant    schedule 08.04.2014


Ответы (3)


Самым коротким, вероятно, будет:

acquire:
    lock bts [eax],0
    jc acquire

release:
    mov [eax],0

Для производительности лучше всего использовать подход «проверить, проверить и установить» и использовать pause, например:

acquire:
    lock bts [eax],0    ;Optimistic first attempt
    jnc l2              ;Success if acquired
l1:
    pause
    test [eax],1        
    jne l1              ;Don't attempt again unless there's a chance

    lock bts [eax],0    ;Attempt to acquire
    jc l1               ;Wait again if failed

l2:

release:
    mov [eax],0

Для отладки вы можете добавить дополнительные данные, чтобы упростить обнаружение проблем, например:

acquire:
    lock bts [eax],31         ;Optimistic first attempt
    jnc l2                    ;Success if acquired

    mov ebx,[CPUnumber]
    lea ebx,[ebx+0x80000000]
    cmp [eax],ebx             ;Is the lock acquired by this CPU?
    je .bad                   ; yes, deadlock
    lock inc dword [eax+4]    ;Increase "lock contention counter"
l1:
    pause
    test [eax],0x80000000        
    jne l1                    ;Don't attempt again unless there's a chance

    lock bts [eax],31         ;Attempt to acquire
    jc l1                     ;Wait again if failed

l2: mov [eax],ebx             ;Store CPU number

release:
    mov ebx,[CPUnumber]
    lea ebx,[ebx+0x80000000]
    cmp [eax],ebx             ;Is lock acquired, and is CPU same?
    jne .bad                  ; no, either not acquired or wrong CPU
    mov [eax],0
person Brendan    schedule 08.04.2014
comment
Если первая попытка увенчается успехом, не будет ли мусор храниться в [eax]? (в отладочной версии) - person harold; 08.04.2014
comment
@harold: он использует один бит для блокировки, а оставшиеся 31 бит для отслеживания того, кто получил блокировку (чтобы он мог определить, когда вы снимаете блокировку, которую не вы установили, а кто-то другой; и взаимоблокировки ). - person Brendan; 08.04.2014
comment
Да, но я имею в виду, что ebx еще не было установлено разумное значение, если вы выберете этот путь, верно? - person harold; 08.04.2014
comment
Почему lea ebx,[ebx+0x80000000]? Это остаток от использования другого регистра? Кстати, разве CPUnumber не должно быть ThreadNumber? Для корректности важен не физический процессор. В любом случае, мило. Это в основном то же самое, что и мой ответ на чистом ассемблере с использованием xchg в блокировках манипулирования памятью с помощью встроенной сборки. - person Peter Cordes; 09.08.2018
comment
@PeterCordes: это просто ebx = CPUnumber | 0x80000000 (где старший бит используется для флага блокировки/не блокировки, а младшие 31 бит — это номер процессора). Для большинства спин-блокировок это должно быть CPUnumber (например, блокировка/блокировки планировщика в ядре), а для большинства случаев, когда будет использоваться ThreadNumber, это должен быть мьютекс, а не спин-блокировка (представьте себе однопроцессорный процессор, где переключение задач происходит сразу после того, как вы получаете блокировку). lock, и любое количество других потоков тратит впустую все время процессора, пока планировщик, наконец, снова не предоставит процессорное время вашему потоку). - person Brendan; 23.08.2018
comment
Я хотел сказать, почему lea вместо add или or, или даже bts ebx, 31 (может сэкономить размер кода по сравнению с disp32 или imm32)? Обычно вы не используете lea, если только не хотите, чтобы результат был в другом регистре, чем в источнике, потому что он работает на меньшем количестве портов. Однако в этом случае это не требует дополнительного размера кода; код операции + modrm + disp32 против imm32 - person Peter Cordes; 23.08.2018
comment
@PeterCordes: А, теперь понятно. Понятия не имею (с тех пор, как я это написал, прошло слишком много времени); но у меня такое ощущение, что был более ранний черновик, в котором я не хотел изменять флаги (например, тест/сравнение до и условный переход после); и (при редактировании/исправлении примера перед публикацией) код вокруг него был изменен, но исходный LEA остался. - person Brendan; 25.08.2018

Ваш код в порядке, но если вы ищете высокую производительность, я бы предложил это вместо этого:

  xor ecx, ecx
.loop:
  lock xchg [eax], ecx
  test ecx, ecx
  jz .loop

Причины:

  • xor ecx, ecx меньше и не требует литерала, а в современных процессорах это жестко запрограммировано для быстрого нулевого регистра.
  • test ecx, ecx может быть немного меньше и быстрее, чем cmp ecx, 0, потому что вам не нужно загружать литерал, а test — это просто побитовая операция И, а не вычитание.

P.S. Я всегда добавляю туда префикс блокировки, независимо от того, подразумевается ли он, из соображений удобочитаемости — это делает очевидным, что я выполняю заблокированную операцию.

person Polynomial    schedule 08.04.2014
comment
Обратите внимание, что префикс не свободен — он занимает байт. Вряд ли это конец света, но если вы обратите внимание на размер других инструкций... - person gsg; 08.04.2014
comment
@gsg Я только что проверил свой компилятор, и он не добавляет дополнительный байт для префикса блокировки, так как обнаруживает, что он лишний в xchg. - person Polynomial; 08.04.2014
comment
Ха, хорошо. Я проверил gcc и as, оба подходят. Знай свои инструменты, я полагаю. - person gsg; 08.04.2014
comment
Я Windows-обезьяна, для меня нет gcc! ;] - person Polynomial; 08.04.2014
comment
Вы оптимизируете для неправильных вещей. Если вы заботитесь о производительности, не крутите xchg, крутите только для чтения, пока не увидите доступную блокировку, чтобы уменьшить конкуренцию с ядром, пытающимся разблокировать. И pause имеет важное значение. См. ответ Брендана здесь или мой на Блокировки манипулирования памятью с помощью встроенной сборки - person Peter Cordes; 09.08.2018

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

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

Когда инициируется попытка блокировки, рассматриваемое ядро ​​​​выдает сигнал на одном из своих контактов (LOCK), который сообщает всем другим ядрам, их кешам, всей памяти и всем устройствам управления шиной (поскольку они могут обновлять ОЗУ независимо от ядер) для завершения любых незавершенных операций с памятью. Когда они сделали это, они коллективно вызывают другой сигнал — подтверждение блокировки (LOCKA), который возвращается к исходному ядру, и происходит обмен памятью. После этого сигнал LOCK отключается.

Как только вы доберетесь сюда, вы сможете посмотреть значение, полученное с помощью xchg. Если выяснится, что другая задача/поток владеет блокировкой, вам нужно будет выполнить последовательность блокировки заново.

Предположим, что самым медленным устройством управления шиной на вашем компьютере является карта PCI с частотой 33 МГц. Если он что-то делает, для его завершения потребуется любое количество тактов шины PCI. Каждый цикл означает сто циклов ожидания на процессоре с частотой 3,3 ГГц. Подумайте об этом с точки зрения сохранения одного или двух циклов в последовательности блокировки. В ЦП, наборе микросхем и материнской плате есть несколько шин, и некоторые из них, все или ни одна из них могут быть активны в любой момент времени, например, когда инициируется LOCK. Активная шина, которая дольше всех отвечает командой LOCKA, будет определять, насколько быстро будет завершена блокировка.

Попробуйте сами: измерьте, сколько времени потребуется, чтобы сделать десять миллионов спин-блокировок (схватить и отпустить).

Я написал еще немного о спин-блокировках здесь, здесь и здесь.

Уловка производительности с блокировками шины (спин-блокировки, критические разделы в Windows) заключается в том, чтобы использовать их как можно реже, что означает организацию ваших данных, чтобы сделать это возможным. Блокировка шины, скорее всего, завершится быстрее на компьютере, не являющемся сервером. Это связано с тем, что устройства управления шиной на сервере работают более или менее постоянно. Поэтому, если ваше приложение основано на сервере, экономия на блокировках шины может иметь решающее значение для поддержания производительности.

ИЗМЕНИТЬ

Питеру Кордесу,

Вы утверждаете, что

... это не связано с мастерингом шины, по крайней мере, не на процессорах, поскольку, по крайней мере, Nehalem

Из последнего руководства по системному программированию Intel:

8.1.4 Влияние операции LOCK на внутренние кэши процессора

Для процессоров Intel486 и Pentium сигнал LOCK# всегда устанавливается на шине во время операции LOCK, даже если блокируемая область памяти кэшируется в процессоре.

Для семейств процессоров P6 и более поздних, если область памяти, заблокированная во время операции LOCK, кэшируется в процессоре, выполняющем операцию LOCK, как память с обратной записью, и полностью содержится в строке кэша, процессор может не утверждать сигнал LOCK# на шине. Вместо этого он изменит расположение памяти внутри и позволит механизму когерентности кеша гарантировать, что операция выполняется атомарно. Эта операция называется «блокировкой кэша». Механизм когерентности кэша автоматически предотвращает одновременное изменение данных в этой области двумя или более процессорами, кэшировавшими одну и ту же область памяти.

Во втором абзаце написано

... процессор не может установить сигнал LOCK# на шине.

Не знаю, как для вас, но для меня, по крайней мере, «может быть» звучит подозрительно, в отличие от «не будет» или «не будет».

Цифры, которые я привел, могут быть или не быть правильными — даже я время от времени ошибаюсь — но я призываю вас перестать цитировать тот или иной «авторитет» и вместо этого замарать руки, выполняя работу, чтобы либо опровергнуть мои цифры, либо найти ошибка(и) или несоответствия. Я включил соответствующий исходный код в другую ветку (где я также пожаловался на ваши возвышенные, теоретические, сидящие в кресле комментарии), так что вам не понадобится вечность, чтобы начать.

Начните, например, с доказательства того, что я

завышение стоимости блокировки в случае отсутствия разногласий...

Жду вашего анализа.

person Olof Forshell    schedule 09.04.2014
comment
xchg [mem], reg имеет обратную задержку ~ 25 циклов на процессорах семейства Sandybridge (в обычной памяти, т. е. с возможностью кэширования с обратной записью), при этом строка кэша остается в модифицированном состоянии в кэше L1d ядра. agner.org/optimize. Требуется только блокировка кеша, а не блокировка шины, для выровненного двойного слова, чтобы это работало благодаря MESI. (И благодаря современному x86, имеющему согласованный с кэшем DMA.) Вы завышаете стоимость блокировки в случае отсутствия конфликтов, когда один и тот же поток неоднократно блокирует/разблокирует одну и ту же блокировку. - person Peter Cordes; 09.08.2018
comment
Хороший дизайн, позволяющий избежать блокировки, безусловно, полезен, но он не связан с управлением шиной, по крайней мере, не на процессорах, по крайней мере, с Nehalem и, возможно, несколько раньше. - person Peter Cordes; 09.08.2018