Стоимость эксплуатации атома

Какова стоимость атомной операции (любой из сравнения и замены или атомного добавления/уменьшения)? Сколько циклов он потребляет? Будет ли он приостанавливать другие процессоры на SMP или NUMA или блокирует доступ к памяти? Будет ли он сбросить буфер переупорядочения в процессоре не по порядку?

Какие эффекты будут в кеше?

Мне интересны современные популярные процессоры: x86, x86_64, PowerPC, SPARC, Itanium.

Ответ 1

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

Стоимость префикса x86 LOCK или CAS до PentiumPro (как описано в документе) - это доступ к памяти (например, кэш-пропуск), + остановка операций с памятью другими процессорами + любые конфликты с другими процессорами, пытающимися для блокировки автобуса. Однако, поскольку PentiumPro для памяти Writeback (т.е. кэшируемой) (во всей памяти, с которой работает приложение, если вы не говорите напрямую с аппаратным обеспечением), вместо блокировки всех операций с памятью блокируется только соответствующая келина (на основе ссылки, указанной выше).

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

Прежде чем переходить к слишком подробным сведениям, я скажу, что операция LOCKed требует одного промаха в кэше + возможного разногласия с другим процессором в одной и той же линии кэширования, тогда как CAS + предыдущая загрузка (что почти всегда требуется, за исключением муфтексов, где вы всегда CAS 0 и 1) может стоить два промаха в кэше.

Он объясняет, что загрузка + CAS в одном месте может стоить двух промахов в кэше, таких как Load-Linked/Store-Conditional (см. там для последней). Его объяснение основывается на знании протокола согласования кэш-памяти MESI. Он использует 4 состояния для кешины: M (офицированный), E (xclusive), S (hared), я (nvalid) (и, следовательно, он называется MESI), поясняется ниже, где это необходимо. Сценарий, объясненный, следующий:

  • LOAD вызывает промаху в кеше - соответствующая кэшлайн загружается из памяти в общем состоянии (т.е. другим процессорам по-прежнему разрешено сохранять эту кешью в памяти, в этом состоянии изменений не допускается). Если местоположение находится в памяти, пропущен этот кеш. Возможная стоимость: 1 кеш-промах. (пропущен, если кешлайн находится в Shared, Exclusive или Modified, то есть данные находятся в этом кэше L1 ЦП).
  • программа вычисляет новые значения для хранения,
  • и он запускает атомную инструкцию CAS.
    • Он должен избегать параллельной модификации, поэтому он должен удалить копии кешины из кеша других процессоров, чтобы переместить кешью в состояние Exclusive. Возможная стоимость: 1 ошибка кэша. Это не нужно, если оно уже принадлежит исключительно, то есть в Исключительном или Модифицированном состоянии. В обоих состояниях никакие другие ЦП не поддерживают кешлайн, но в Исключительном состоянии он еще не был изменен.
    • После этого сообщения переменная изменяется в нашем локальном кеш процессора, и в этот момент она глобально видима для всех других ЦП (поскольку их кеши являются согласованными с нашими). В конечном итоге он будет записан в основную память в соответствии с обычными алгоритмами.
    • Другие процессоры, пытающиеся прочитать или изменить эту переменную, сначала должны будут получить эту кешью в режиме общего или эксклюзивного использования, и для этого свяжутся с этим процессором и получат обновленную версию кэширования. Вместо этого операция LOCKed может стоить только пропущенному кешу (потому что келин будет запрошен непосредственно в эксклюзивном состоянии).

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

Ответ 2

Я сделал некоторые профилирования со следующей настройкой: тестовая машина (AMD Athlon64 x2 3800+) была загружена, переключена на длительный режим (прерывания отключены), а интересная команда была выполнена в цикле, 100 итераций развернуты и 1000 циклов циклы. Тело цикла было выровнено с 16 байтами. Время измерялось с помощью команды rdtsc до и после цикла. Кроме того, был выполнен фиктивный цикл без какой-либо инструкции (который измерял 2 цикла на итерацию цикла и 14 циклов для остальных), и результат был вычтен из результата времени профилирования команды.

Были измерены следующие инструкции:

  • "lock cmpxchg [rsp - 8], rdx" (оба с совпадением сравнения и несоответствием),
  • "lock xadd [rsp - 8], rdx",
  • "lock bts qword ptr [rsp - 8], 1"

Во всех случаях измеренное время составляло около 310 циклов, ошибка составляла около +/- 8 циклов

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

Чтобы оценить стоимость заблокированной инструкции при пропуске кеша, я добавил инструкцию wbinvld перед заблокированной инструкцией и поместил wbinvld plus an add [rsp - 8], rax в цикл сравнения. В обоих случаях стоимость составляла около 80 000 циклов на пару инструкций! В случае блокировки bts разница во времени составляла около 180 циклов на инструкцию.

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

Заключение: заблокированная операция тяжелая, но промаха в кеше может быть намного тяжелее. Также: заблокированная операция не вызывает промахов в кеше. Это может привести только к трафику синхронизации кеша, когда кешлайн не принадлежит исключительно.

Чтобы загрузить машину, я использовал версию FreeLdr x64 из проекта ReactOS. Здесь исходный код asm:

#define LOOP_COUNT 1000
#define UNROLLED_COUNT 100

PUBLIC ProfileDummy
ProfileDummy:

    cli

    // Get current TSC value into r8
    rdtsc
    mov r8, rdx
    shl r8, 32
    or r8, rax

    mov rcx, LOOP_COUNT
    jmp looper1

.align 16
looper1:

REPEAT UNROLLED_COUNT
    // nothing, or add something to compare against
ENDR

    dec rcx
    jnz looper1

    // Put new TSC minus old TSC into rax
    rdtsc
    shl rdx, 32
    or rax, rdx
    sub rax, r8

    ret

PUBLIC ProfileFunction
ProfileFunction:

    cli

    rdtsc
    mov r8, rdx
    shl r8, 32
    or r8, rax
    mov rcx, LOOP_COUNT

    jmp looper2

.align 16
looper2:

REPEAT UNROLLED_COUNT
    // Put here the code you want to profile
    // make sure it doesn't mess up non-volatiles or r8
    lock bts qword ptr [rsp - 8], 1
ENDR

    dec rcx
    jnz looper2

    rdtsc
    shl rdx, 32
    or rax, rdx
    sub rax, r8

    ret

Ответ 3

На шинной основе SMP атомный префикс LOCK подтверждает (включает) сигнал шины LOCK#. Он запретит использование других процессоров/устройств на шине.

Книга Ppro и P2 http://books.google.com/books?id=3gDmyIYvFH4C&pg=PA245&dq=lock+instruction+pentium&lr=&ei=_E61S5ehLI78zQSzrqwI&cd=1#v=onepage&q=lock%20instruction%20pentium&f=false страницы 244-246

Заблокированные инструкции выполняют сериализацию, синхронизацию. /about Out-of-order/locked RMW/read-modify-write = atomic self/инструкция гарантирует, что процессор выполнит все инструкции перед заблокированной инструкцией перед ее выполнением. /пока не сработала запись/она заставляет все опубликованные записи внутри процессора быть сброшенными во внешнюю память перед выполнением следующей инструкции.

/о SMP/семафоре находится в кеше в состоянии S... выдача транзакции чтения и недействительности для 0 байтов даты (это kill/общих копий строки кэша в соседних CPU/)