Самая быстрая спин-блокировка при встроенной сборке

Я пишу многопоточное приложение на C ++, где производительность имеет решающее значение. Мне нужно использовать много блокировок при копировании небольших структур между потоками, для этого я решил использовать спин-блокировки.

Я провел небольшое исследование и тестирование скорости и обнаружил, что большинство реализаций примерно одинаково быстры:

  • Microsofts CRITICAL_SECTION с SpinCount, установленным на 1000, набирает около 140 единиц времени.
  • Реализация этого алгоритма с помощью Microsofts InterlockedCompareExchange оценивается примерно в 95 единиц времени.
  • Ive также попытался использовать некоторую встроенную сборку с __asm {}, используя что-то вроде этот код, и он набирает около 70 единиц времени, но я не уверен, что был создан надлежащий барьер памяти.

Изменить: время, указанное здесь, - это время, необходимое для блокировки и разблокировки спин-блокировки 2 потоков 1000000 раз.

Я знаю, что это не большая разница, но, поскольку спин-блокировка - это часто используемый объект, можно подумать, что программисты договорились бы о самом быстром способе создания спин-блокировки. Однако поиск в Google приводит к множеству разных подходов. Я думаю, что этот вышеупомянутый метод был бы самым быстрым, если бы он был реализован с использованием встроенной сборки и используя инструкцию CMPXCHG8B вместо сравнения 32-битных регистров. Кроме того, необходимо учитывать барьеры памяти, это может быть сделано с помощью LOCK CMPXHG8B (я думаю?), который гарантирует «исключительные права» на разделяемую память между ядрами. Наконец [некоторые предполагают], что для ожидания занятости следует сопровождать NOP: REP, что позволило бы процессорам Hyper-threading переключаться на другой поток, но я не уверен, так ли это?

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

__asm
{
     spin_lock:
         ;locking code.
     spin_unlock:
         ;unlocking code.
}

person sigvardsen    schedule 14.08.2012    source источник
comment
+1 за предоставление хороших источников и информации перед запросом. я думаю, ты дал больше, чем тебе нужно. Спасибо   -  person huseyin tugrul buyukisik    schedule 14.08.2012
comment
Сколько именно это много? Было бы чертовски много беспокоиться о том, как быстро вы можете вращаться. Вы уверены, что нет лучшего способа ограничить доступ здесь? Помните также, что скорость, с которой вы вращаетесь, не влияет на то, когда вы фактически устанавливаете блокировку. Неважно, как быстро вы вращаетесь, другой парень должен сначала разблокировать его. Рассмотрите возможность зацикливания yield () для передачи выполнения другому запущенному потоку или процессу, если окажется, что вы собираетесь вращаться в течение длительного времени.   -  person Wug    schedule 14.08.2012
comment
rep nop он же pause также заставляет P4 не делать отсталых вещей, когда вы покидаете цикл вращения. В руководстве Intel явно рекомендуется использовать его в циклах ожидания и вращения. Разрешено ли вам использовать XACQUIRE и XRELEASE (недоступно до Haswell)?   -  person harold    schedule 14.08.2012
comment
@Wug Время, указанное в тестах производительности, - это время, которое требуется 2 потокам для одновременной блокировки, копирования 4 целых (для добавления реализма) и разблокировки спин-блокировки, возможно, 10000000 раз (у меня нет исходного кода на этом компьютере). Указанные единицы времени не дают никакой информации о том, сколько циклов было выполнено.   -  person sigvardsen    schedule 14.08.2012
comment
Если вам нужна производительность, используйте структуры данных без блокировок / без конкуренции на быстром пути и блокируйте только на медленном пути.   -  person PlasmaHH    schedule 14.08.2012
comment
@PlasmaHH Я знаю, что вы можете копировать данные, но только до 32-битных на 32-битных машинах и 64-битных на 64-битных машинах с блокированными операциями Microsoft. Но я не знаю, что вы можете выполнять атомарные чтения и записи в структурах большего размера, чем приведенные выше примеры? Не могли бы вы указать мне на какую-нибудь литературу / литературу или примеры? Можно ли это сделать в сборке?   -  person sigvardsen    schedule 14.08.2012
comment
@sigvardsen: Если вы объясните подробнее, что делаете, возможно, мы сможем предложить альтернативные решения.   -  person GManNickG    schedule 15.08.2012
comment
@GManNickG Мне нужно скопировать структуру, состоящую из 8 int = 256 бит, из одного потока в другой - конечно, атомарно.   -  person sigvardsen    schedule 15.08.2012
comment
@sigvardsen, что, если вы атомарно скопируете указатель на эти данные?   -  person harold    schedule 15.08.2012
comment
@sigvardsen: Вы упускаете суть. Почему вы это делаете? Какую проблему вы решаете?   -  person GManNickG    schedule 15.08.2012
comment
Надо сказать, что трудно поверить, что узким местом алгоритма является производительность спин-блокировок. Может, вы ими злоупотребляете? Может не надо так сильно блокировать? Может быть, улучшение конструкции алгоритма сделает эту оптимизацию совершенно ненужной? Может можно пожертвовать памятью ради производительности? Конечно, вполне возможно, что вы могли, наконец, закончить с дерьмовыми спин-блокировками, но как правило: это всегда ваша вина. codinghorror. ru / blog / 2008/03 /   -  person Sedat Kapanoglu    schedule 15.08.2012
comment
@ssg Как я упоминал в своем вопросе, я понимаю, что все эти реализации спин-блокировки почти одинаково быстры. Но я поднял этот вопрос просто из чисто академического интереса. Хорошая практика - реализовать лучший алгоритм, хотя в этом нет необходимости.   -  person sigvardsen    schedule 15.08.2012
comment
Барьеры памяти не имеют ничего общего с исключительными правами. Речь идет об упорядочивании памяти, обязательно прочтите документацию Intel о порядке памяти на x86, что в большинстве случаев устраняет препятствия.   -  person Gunther Piez    schedule 15.08.2012
comment
@sigvardsen Хорошо, я пропустил чисто академическую часть, моя ошибка :)   -  person Sedat Kapanoglu    schedule 15.08.2012
comment
@sigvardsen: на x86_64 у вас есть 16-байтовые атомные свопы, их обычно достаточно почти для всех структур данных без блокировок. Многие из этих структур даже работают (частично) без префиксов блокировки, просто используя тот факт, что для правильно выровненных вещей чтение / запись являются атомарными, иногда в зависимости от барьеров памяти. В сети действительно много материала для чтения.   -  person PlasmaHH    schedule 15.08.2012


Ответы (5)


Просто посмотрите здесь: спин-блокировка x86 с использованием cmpxchg

И благодаря Кори Нельсону

__asm{
spin_lock:
xorl %ecx, %ecx
incl %ecx
spin_lock_retry:
xorl %eax, %eax
lock; cmpxchgl %ecx, (lock_addr)
jnz spin_lock_retry
ret

spin_unlock:
movl $0 (lock_addr)
ret
}

Другой источник сообщает: http://www.geoffchappell.com/studies/windows/km/cpu/cx8.htm

       lock    cmpxchg8b qword ptr [esi]
is replaceable with the following sequence

try:
        lock    bts dword ptr [edi],0
        jnb     acquired
wait:
        test    dword ptr [edi],1
        je      try
        pause                   ; if available
        jmp     wait

acquired:
        cmp     eax,[esi]
        jne     fail
        cmp     edx,[esi+4]
        je      exchange

fail:
        mov     eax,[esi]
        mov     edx,[esi+4]
        jmp     done

exchange:
        mov     [esi],ebx
        mov     [esi+4],ecx

done:
        mov     byte ptr [edi],0

А вот обсуждение реализации lock-free и lock: http://newsgroups.derkeiler.com/Archive/Comp/comp.programming.threads/2011-10/msg00009.html

person huseyin tugrul buyukisik    schedule 14.08.2012
comment
Где бы вы разместили инструкцию PAUSE (NOP: REP)? внутри цикла или раньше? - person sigvardsen; 16.08.2012
comment
вместо паузы в части ожидания. Но для более старых процессоров, насколько я знаю - person huseyin tugrul buyukisik; 16.08.2012
comment
Эта реализация вращается на lock cmpxchg, а не на атомарной загрузке перед попыткой обмена. (См. Ответ Некрополя). pause позволяет избежать неправильного определения порядка памяти, когда вы запускаете загрузку вместо инструкции locked. См. мой ответ на другой вопрос для простой / минимальной реализации спин-блокировки asm что, как мне кажется, позволяет избежать очевидных проблем. - person Peter Cordes; 27.05.2016
comment
Это также работает для 64-битной версии? Почему он говорит только о спин-блокировке x86? - person Goaler444; 24.02.2017
comment
@ Goaler444 x86 означает общее имя архитектуры и также должен работать для x86_64 с 64-битным lock_addr с использованием cmpxchgq вместо cmpxchgl - person huseyin tugrul buyukisik; 24.02.2017

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

  1. Вращение на непостоянном чтении, а не на атомарной инструкции, позволяет избежать ненужных блокировок шины, особенно при блокировках с высокой степенью конкуренции.
  2. Используйте откат для очень спорных замков
  3. Встроенная блокировка, предпочтительно с встроенными функциями для компиляторов, где встроенный asm вреден (в основном MSVC).
person Necrolis    schedule 16.08.2012

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

Я тоже не буду здесь жаловаться, но могу однозначно заявить, что вопрос о быстрой спин-блокировке длиной в три или несколько инструкций - по крайней мере, на архитектуре x86 - бесполезная погоня.

Вот почему:

Вызов спин-блокировки с помощью типичной кодовой последовательности

lock_variable DW 0    ; 0 <=> free

mov ebx,offset lock_variable
mov eax,1
xchg eax,[ebx]

; if eax contains 0 (no one owned it) you own the lock,
; if eax contains 1 (someone already does) you don't

Освобождение спин-блокировки тривиально

mov ebx,offset lock_variable
mov dword ptr [ebx],0

Инструкция xchg поднимает стопорный штифт на процессоре, что фактически означает, что мне нужна шина в течение следующих нескольких тактов. Этот сигнал проходит через кеши до самого медленного устройства управления шиной, которым обычно является шина PCI. Когда каждое устройство управления шиной завершает работу, сигнал locka (подтверждение блокировки) отправляется обратно. Затем происходит собственно обмен. Проблема в том, что последовательность блокировки / блокировки занимает ОЧЕНЬ много времени. Шина PCI может работать на частоте 33 МГц с несколькими циклами задержки. На процессоре с тактовой частотой 3,3 ГГц это означает, что каждый цикл шины PCI занимает сто циклов процессора.

Как правило, я предполагаю, что блокировка займет от 300 до 3000 циклов процессора, и, в конце концов, я не знаю, буду ли я владельцем блокировки. Таким образом, несколько циклов, которые вы можете сохранить с помощью «быстрой» спин-блокировки, будут миражом, потому что никакая блокировка не похожа на следующую, она будет зависеть от ситуации с вашей шиной в течение этого короткого времени.

________________РЕДАКТИРОВАТЬ________________

Я только что прочитал, что спин-блокировка - это «часто используемый объект». Что ж, вы, очевидно, не понимаете, что спин-блокировка потребляет огромное количество циклов процессора каждый раз, когда она вызывается. Или, говоря другими словами, каждый раз, когда вы вызываете его, вы теряете значительную часть своих возможностей обработки.

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

Дело не только в написании быстрого кода, но и в организации ваших данных. Когда вы пишете «копирование небольших структур между потоками», вы должны понимать, что блокировка может занять в сотни раз больше времени, чем фактическое копирование.

________________РЕДАКТИРОВАТЬ________________

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

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

________________РЕДАКТИРОВАТЬ________________

Обновление за май 2016 г.

Питер Кордес продвигал идею о том, что «настройка блокировки в неконкурентном случае может иметь смысл» и что время блокировки в несколько сотен тактовых циклов не возникает на современных ЦП, за исключением ситуаций, когда переменная блокировки не выровнена. Мне стало интересно, написана ли моя предыдущая тестовая программа в 32-битном Watcom C - WOW64 может помешать, поскольку он работал в 64-битной ОС: Windows 7.

Итак, я написал 64-битную программу и скомпилировал ее с помощью TDM gcc 5.3. Программа использует вариант инструкции неявной блокировки шины «XCHG r, m» для блокировки и простое присвоение «MOV m, r» для разблокировки. В некоторых вариантах блокировки переменная блокировки была предварительно протестирована, чтобы определить, возможно ли даже попытаться заблокировать (используя простое сравнение «CMP r, m», вероятно, не выходя за пределы L3). Вот:

// compiler flags used:

// -O1 -m64 -mthreads -mtune=k8 -march=k8 -fwhole-program -freorder-blocks -fschedule-insns -falign-functions=32 -g3 -Wall -c -fmessage-length=0

#define CLASSIC_BUS_LOCK
#define WHILE_PRETEST
//#define SINGLE_THREAD

typedef unsigned char      u1;
typedef unsigned short     u2;
typedef unsigned long      u4;
typedef unsigned int       ud;
typedef unsigned long long u8;
typedef   signed char      i1;
typedef          short     i2;
typedef          long      i4;
typedef          int       id;
typedef          long long i8;
typedef          float     f4;
typedef          double    f8;

#define usizeof(a) ((ud)sizeof(a))

#define LOOPS 25000000

#include <stdio.h>
#include <windows.h>

#ifndef bool
typedef signed char bool;
#endif

u8 CPU_rdtsc (void)
{
  ud tickl, tickh;
  __asm__ __volatile__("rdtsc":"=a"(tickl),"=d"(tickh));
  return ((u8)tickh << 32)|tickl;
}

volatile u8 bus_lock (volatile u8 * block, u8 value)
{
  __asm__ __volatile__( "xchgq %1,%0" : "=r" (value) : "m" (*block), "0" (value) : "memory");

  return value;
}

void bus_unlock (volatile u8 * block, u8 value)
{
  __asm__ __volatile__( "movq %0,%1" : "=r" (value) : "m" (*block), "0" (value) : "memory");
}

void rfence (void)
{
  __asm__ __volatile__( "lfence" : : : "memory");
}

void rwfence (void)
{
  __asm__ __volatile__( "mfence" : : : "memory");
}

void wfence (void)
{
  __asm__ __volatile__( "sfence" : : : "memory");
}

volatile bool LOCK_spinlockPreTestIfFree (const volatile u8 *lockVariablePointer)
{
  return (bool)(*lockVariablePointer == 0ull);
}

volatile bool LOCK_spinlockFailed (volatile u8 *lockVariablePointer)
{
  return (bool)(bus_lock (lockVariablePointer, 1ull) != 0ull);
}

void LOCK_spinlockLeave (volatile u8 *lockVariablePointer)
{
  *lockVariablePointer = 0ull;
}

static volatile u8 lockVariable = 0ull,
                   lockCounter =  0ull;

static volatile i8 threadHold = 1;

static u8 tstr[4][32];    /* 32*8=256 bytes for each thread's parameters should result in them residing in different cache lines */

struct LOCKING_THREAD_STRUCTURE
{
  u8 numberOfFailures, numberOfPreTests;
  f8 clocksPerLock, failuresPerLock, preTestsPerLock;
  u8 threadId;
  HANDLE threadHandle;
  ud idx;
} *lts[4] = {(void *)tstr[0], (void *)tstr[1], (void *)tstr[2], (void *)tstr[3]};

DWORD WINAPI locking_thread (struct LOCKING_THREAD_STRUCTURE *ltsp)
{
  ud n = LOOPS;
  u8 clockCycles;

  SetThreadAffinityMask (ltsp->threadHandle, 1ull<<ltsp->idx);

  while (threadHold) {}

  clockCycles = CPU_rdtsc ();
  while (n)
  {
    Sleep (0);

#ifdef CLASSIC_BUS_LOCK
    while (LOCK_spinlockFailed (&lockVariable)) {++ltsp->numberOfFailures;}
#else
#ifdef WHILE_PRETEST
    while (1)
    {
      do
      {
        ++ltsp->numberOfPreTests;
      } while (!LOCK_spinlockPreTestIfFree (&lockVariable));

      if (!LOCK_spinlockFailed (&lockVariable)) break;
      ++ltsp->numberOfFailures;
    }
#else
    while (1)
    {
      ++ltsp->numberOfPreTests;
      if (LOCK_spinlockPreTestIfFree (&lockVariable))
      {
        if (!LOCK_spinlockFailed (&lockVariable)) break;
        ++ltsp->numberOfFailures;
      }
    }
#endif
#endif
    ++lockCounter;
    LOCK_spinlockLeave (&lockVariable);

#ifdef CLASSIC_BUS_LOCK
    while (LOCK_spinlockFailed (&lockVariable)) {++ltsp->numberOfFailures;}
#else
#ifdef WHILE_PRETEST
    while (1)
    {
      do
      {
        ++ltsp->numberOfPreTests;
      } while (!LOCK_spinlockPreTestIfFree (&lockVariable));

      if (!LOCK_spinlockFailed (&lockVariable)) break;
      ++ltsp->numberOfFailures;
    }
#else
    while (1)
    {
      ++ltsp->numberOfPreTests;
      if (LOCK_spinlockPreTestIfFree (&lockVariable))
      {
        if (!LOCK_spinlockFailed (&lockVariable)) break;
        ++ltsp->numberOfFailures;
      }
    }
#endif
#endif
    --lockCounter;
    LOCK_spinlockLeave (&lockVariable);

    n-=2;
  }
  clockCycles = CPU_rdtsc ()-clockCycles;

  ltsp->clocksPerLock =   (f8)clockCycles/           (f8)LOOPS;
  ltsp->failuresPerLock = (f8)ltsp->numberOfFailures/(f8)LOOPS;
  ltsp->preTestsPerLock = (f8)ltsp->numberOfPreTests/(f8)LOOPS;

//rwfence ();

  ltsp->idx = 4u;

  ExitThread (0);
  return 0;
}

int main (int argc, char *argv[])
{
  u8 processAffinityMask, systemAffinityMask;

  memset (tstr, 0u, usizeof(tstr));

  lts[0]->idx = 3;
  lts[1]->idx = 2;
  lts[2]->idx = 1;
  lts[3]->idx = 0;

  GetProcessAffinityMask (GetCurrentProcess(), &processAffinityMask, &systemAffinityMask);

  SetPriorityClass (GetCurrentProcess(), HIGH_PRIORITY_CLASS);
  SetThreadAffinityMask (GetCurrentThread (), 1ull);

  lts[0]->threadHandle = CreateThread (NULL, 65536u, (void *)locking_thread, (void *)lts[0], 0, (void *)&lts[0]->threadId);
#ifndef SINGLE_THREAD
  lts[1]->threadHandle = CreateThread (NULL, 65536u, (void *)locking_thread, (void *)lts[1], 0, (void *)&lts[1]->threadId);
  lts[2]->threadHandle = CreateThread (NULL, 65536u, (void *)locking_thread, (void *)lts[2], 0, (void *)&lts[2]->threadId);
  lts[3]->threadHandle = CreateThread (NULL, 65536u, (void *)locking_thread, (void *)lts[3], 0, (void *)&lts[3]->threadId);
#endif

  SetThreadAffinityMask (GetCurrentThread (), processAffinityMask);

  threadHold = 0;

#ifdef SINGLE_THREAD
  while (lts[0]->idx<4u) {Sleep (1);}
#else
  while (lts[0]->idx+lts[1]->idx+lts[2]->idx+lts[3]->idx<16u) {Sleep (1);}
#endif

  printf ("T0:%1.1f,%1.1f,%1.1f\n", lts[0]->clocksPerLock, lts[0]->failuresPerLock, lts[0]->preTestsPerLock);
  printf ("T1:%1.1f,%1.1f,%1.1f\n", lts[1]->clocksPerLock, lts[1]->failuresPerLock, lts[1]->preTestsPerLock);
  printf ("T2:%1.1f,%1.1f,%1.1f\n", lts[2]->clocksPerLock, lts[2]->failuresPerLock, lts[2]->preTestsPerLock);
  printf ("T3:%1.1f,%1.1f,%1.1f\n", lts[3]->clocksPerLock, lts[3]->failuresPerLock, lts[3]->preTestsPerLock);

  printf ("T*:%1.1f,%1.1f,%1.1f\n", (lts[0]->clocksPerLock+  lts[1]->clocksPerLock+  lts[2]->clocksPerLock+  lts[3]->clocksPerLock)/  4.,
                                    (lts[0]->failuresPerLock+lts[1]->failuresPerLock+lts[2]->failuresPerLock+lts[3]->failuresPerLock)/4.,
                                    (lts[0]->preTestsPerLock+lts[1]->preTestsPerLock+lts[2]->preTestsPerLock+lts[3]->preTestsPerLock)/4.);

  printf ("LC:%u\n", (ud)lockCounter);

  return 0;
}

Программа была запущена на компьютере на базе DELL i5-4310U с памятью DDR3-800, 2 ядрами / 2 HT с тактовой частотой 2,7 ГГц и общей кэш-памятью третьего уровня.

Во-первых, похоже, что влияние WOW64 было незначительным.

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

Когда четыре HT бомбардируют переменную блокировки попытками блокировки, ситуация радикально меняется. Время, необходимое для достижения успешной блокировки, увеличивается до 994 циклов, значительную часть из которых можно отнести к 2,2 неудачным попыткам блокировки. Другими словами, в ситуации высокой конкуренции в среднем необходимо попытаться выполнить 3,2 блокировки для достижения успешной блокировки. Очевидно, что 110 циклов не превратились в 110 * 3,2, а стали ближе к 110 * 9. Таким образом, здесь задействованы другие механизмы, как и в случае тестов на более старой машине. Кроме того, средние 994 цикла охватывают диапазон от 716 до 1157.

Варианты замков, реализующие предварительное тестирование, требовали около 95% циклов, необходимых для простейшего варианта (XCHG). В среднем они выполняли 17 CMP, чтобы найти возможность попытаться выполнить 1,75 блокировок, из которых 1 была успешной. Я рекомендую использовать предварительное тестирование не только потому, что оно быстрее: оно снижает нагрузку на механизм блокировки шины (3,2–1,75 = 1,45 меньше попыток блокировки), хотя это немного увеличивает сложность.

person Olof Forshell    schedule 19.10.2012
comment
xchg с операндом памяти и другими locked инструкциями далеко не , что медленнее на современных процессорах x86. Они согласованы с кешем, но не с DMA, поэтому им не нужно ждать доступа к DRAM. (И определенно не цикл шины PCI, если, возможно, вы не используете его на адресе ввода-вывода с отображением памяти!). например на Sandybridge (который был актуален в 2012 году) xchg с памятью имеет задержку 25 циклов (при попадании в кеш), согласно таблицам Агнера Фога. Если текущее ядро ​​имеет строку кэша в эксклюзивном состоянии, тогда атомарный RMW прост, и это часть барьера памяти, которая требует времени. - person Peter Cordes; 17.04.2016
comment
Наихудший случай для xchg - это если другое ядро ​​имеет строку кэша в состоянии Modified или Exclusive с locked инструкциями в полете, или, может быть, если ни одно ядро ​​не кэширует эту строку, поэтому оно должно читать из основной памяти. Обратите внимание, что он не должен записывать обратно в основную память; вы получите строку кэша в состоянии M в L1 текущего ядра. - person Peter Cordes; 17.04.2016
comment
@Peter Cordes: Итак, почему AF пишет - внизу страницы 1 файлаstruction_tables.pdf - префикс LOCK обычно стоит более сотни тактов, даже в однопроцессорной системе. Это также относится к инструкции XCHG с операндом памяти. Я полагаю, что Агнер Фог может считаться и правильным, и неправильным ... что это такое? - person Olof Forshell; 19.04.2016
comment
В лучшем случае около 25 центов для неконкурентного xchg с линией кэша, которая уже горячая на уровне L1, но типичный случай 100 центов звучит разумно, если учесть, что он заказывает другие магазины с нагрузками, и вы, вероятно, не будете использовать его, когда нет разногласий. Я сам просто рассчитал крошечный цикл, и даже на таком старом ЦП, как Merom / Conroe Core2, я получил один на ~ 25c пропускной способности. (Несколько xchg на разные адреса по-прежнему не могут быть распараллелены из-за эффекта барьера памяти). В любом случае, ему определенно не нужно выполнять циклы шины PCIe при использовании в обычных областях памяти. - person Peter Cordes; 19.04.2016
comment
Общая картина: настройка быстрой блокировки в неконкурентном случае может иметь смысл, потому что это может привести к довольно низким накладным расходам. Это происходит в ядре Linux довольно часто, IIRC: общий случай - это отсутствие конкуренции, но блокировка все равно необходима. - person Peter Cordes; 19.04.2016
comment
Вы в этом уверены? У вас есть личный опыт? Я делаю stackoverflow .com / questions / 4912467 / - person Olof Forshell; 19.04.2016
comment
Интересные цифры, спасибо за ссылку. Нет, у меня нет личного опыта настройки / измерения накладных расходов на блокировку. Ваш ответ там доказывает, что xchg не так медлителен, как если бы ему приходилось взаимодействовать с шиной PCI. Вы также тестируете цикл, который полностью ограничен пропускной способностью блокировки, поэтому в конвейере нет никакой другой работы, которую должен выполнять ЦП, пока происходит xchg. Я думаю, но не тестировал, этот xchg конвейер очень хорошо работает, поэтому в реальном коде, который требует много работы, помимо блокировки, накладные расходы в случае неконтролируемого случая не страшны. - person Peter Cordes; 19.04.2016
comment
Самые эффективные блокировки - это те, которых вам удается избежать, если вы достаточно квалифицированы. Новички и люди, которые их не понимают, будут думать, что они не требуют значительных затрат, и будут использовать их гораздо чаще, чем другие, точно так же, как большинство людей, пишущих многопоточные приложения, в конечном итоге получат более медленное приложение. В конце концов, вашему коду, возможно, придется отправить сигнал LOCK на шину и дождаться, пока самое медленное устройство управления шиной вернет LOCKA, чтобы сказать, что блокировка установлена. Или вы утверждаете, что таких ситуаций не может быть? Что делать, если ваша переменная блокировки была удалена из кеша? - person Olof Forshell; 20.04.2016
comment
На Intel Core2 (E6600) цикл clflush [rsp-128] / xchg [rsp-128], eax имеет ту же скорость, что и цикл clfush [rsp-128] / add [rsp-128], eax. (~ 410c на итерацию на частоте 2,4 ГГц, с DDR533 DRAM. clflush только работает ~ 86c на итерацию.) Таким образом, неявное lock в xchg не ухудшает двусторонний переход к DRAM. Очевидно, что в этом сценарии это все еще очень дорого. Я не пытаюсь утверждать, что блокировка в целом дёшево, и ваша точка зрения о том, что хороший дизайн, позволяющий избежать блокировки, - отличный вариант. - person Peter Cordes; 20.04.2016
comment
Я не думаю, что заблокированным insns нужно ждать устройств PCI. x86 имеет согласованный с кешем DMA. Это может быть недавняя вещь, включенная путем размещения контроллера памяти на ЦП, что упрощает отслеживание DMA кэшем. Я бы удалил свой голос против и, возможно, за, если бы вы убрали (или предоставили доказательства) эту часть о внешних шинах. - person Peter Cordes; 20.04.2016
comment
Мой ответ от 2011 года был сделан на двухъядерном процессоре, выпущенном в сентябре 2009 года. У этого процессора было два кэша L2 - по одному на каждое ядро ​​и без (общего) L3. В примере с двумя блокирующими потоками потребовалось бы много обращений к ОЗУ для выполнения цикла (чтение или чтение + запись для блокировки, чтение + запись для обновления числа, запись для разблокировки). Успешное завершение цикла привело бы к тому, что кэшированная информация другого потока стала бы недействительной, заставив ее начать с нуля. Я попытаюсь запустить код в системе с L3. - person Olof Forshell; 21.04.2016
comment
Мои однопоточные тесты clflush проводились на еще более старом процессоре (первое поколение Core2: большие кеши L2 на ядро), поскольку моя материнская плата Sandybridge вышла из строя, и у меня еще нет замены. Мой прогноз - более низкое время доступа, потому что это только круговой обход L3 вместо DRAM, когда другой поток аннулировал строку кэша в L1 / L2 первого ядра. - person Peter Cordes; 23.04.2016
comment
В этом документе support.amd.com/TechDocs/47414_15h_sw_opt_guide.pdf показан маловероятный случай, когда инструкция xchg приведет к блокировке шины в современной архитектуре. Опубликованный в 2014 году, он остается в силе. - person Olof Forshell; 24.04.2016
comment
Вы про раздел 11.1.4? Они говорят в некоторых аппаратных реализациях, что, как мне кажется, означает, что они говорят о процессорах, отличных от AMD Family 15h (семейство Bulldozer). Возможно, даже Bulldozer вернется к блокировке шины для locked insn, которая охватывает две строки кэша. Спасибо, что откопали это; интересное примечание. Однако этого почти наверняка не произойдет с невыровненной операцией, которая не пересекает границы строки кэша. Bulldozer имеет эффективную поддержку невыровненного доступа для AVX и целочисленных операций, поэтому им, вероятно, удастся избежать каких-либо ужасных откатов для невыровненных в строке кэша. - person Peter Cordes; 24.04.2016
comment
Выравнивать нужно не операцию, а саму переменную блокировки. В любом случае я не решаюсь классифицировать xchg (w lock) как операцию, как и любую другую: она имеет специфические, влияющие на систему свойства, которых нет у обычных целочисленных операций. - person Olof Forshell; 24.04.2016
comment
По поводу вашего теста (4 сообщения вверх) вы пробовали его с двумя потоками, привязанными к отдельным ядрам? Мой тест представлял собой два потока на двух ядрах (сходство), 32-битный код, работающий на 64-битном W7. - person Olof Forshell; 24.04.2016
comment
Нет, с двумя потоками не пробовал. Я намеренно пробовал другой подход к измерению задержки в основной памяти, но я думаю, что было бы более полезно иметь номера конкурирующих блокировок с того же оборудования. re: выравнивание замка: да, я это имел ввиду. то есть, когда люди говорят о выровненной нагрузке, они имеют в виду нагрузку с выровненного адреса. Таким образом, выровненная операция означает заблокированную операцию на выровненном адресе, то есть переменная блокировки выровнена. Мое предположение (которое легко может быть ошибочным) о невыровненных переменных блокировки, которые не пересекают строки кеша, основано на идее, что ЦП, вероятно, блокирует всю строку. - person Peter Cordes; 25.04.2016
comment
Отдельные ядра не знают о строках кэша. Механизм кэширования загружает строки кэша с памятью, запрошенной ядром, чтобы уменьшить медленное время отклика ОЗУ. На современных x86 строка кэша имеет длину 64 байта, выровненную по четному 64-байтовому адресу RAM. Даже если вам нужен только 43-й байт в строке кэша, механизм загружает все 64 байта. Если место в строке кэша изменяется, механизм записывает эту строку кэша в память. Точно так же ядро ​​не знает, что у обмена есть явная блокировка: оно выполняет инструкцию, вставляются состояния ожидания и, наконец, ядро ​​видит данные. - person Olof Forshell; 27.05.2016
comment
Частный кэш L1 является частью каждого ядра. Руководство AMD по оптимизации, которое вы связали ранее (стр. 202: 11.1.4 ... Граница строки кэша), явно ссылается на то, что ядро ​​блокирует свой собственный кеш вместо блокировки шины. Говорят, естественно выровнены, но в заголовке раздела указано граница строки кеша. Кроме того, какой части ядра не нужно знать, что xchg имеет неявную блокировку? Он должен запускать барьер памяти (например, MFENCE), а также сам быть атомарным. Ваше описание похоже на то, что могло бы произойти на простом исправном процессоре. - person Peter Cordes; 27.05.2016
comment
Приятное обновление, интересные цифры. Однако вы измеряете только пропускную способность блокировки, а не влияние блокировки на рабочую нагрузку, в конвейере которой находится много неблокирующей работы. Это тот случай, когда я предлагал избегать замедления в неконкурентном случае. Это похоже на измерение divps медленности с помощью цикла, который только запускает divps, по сравнению с циклом, который выполняет множество десятков addps / mulps операций и одну divps. Пока доступен параллелизм на уровне инструкций, divps не должен иметь большого влияния на общую пропускную способность, поскольку это всего лишь один uop port0. - person Peter Cordes; 27.05.2016
comment
Очевидно, xchg оказывает гораздо большее влияние на конвейер, чем divps, даже если он не является частью цепочки зависимостей критического пути. Но он все равно может частично совпадать с другими инструкциями. Конечно с операциями, не связанными с памятью. - person Peter Cordes; 27.05.2016
comment
OP по-прежнему писал: мне нужно использовать много блокировок при копировании небольших структур между потоками, для этого я решил использовать спин-блокировки. Хотя мой код показывает, что каждое из четырех ядер может достигать 2,7 миллиона блокировок / разблокировок в секунду, не исключено, что OP использует так много из них, что его многопоточный код будет лучше однопоточного без блокировок. - person Olof Forshell; 29.05.2016
comment
Избавьтесь от теоретической аргументации. Я представил действительные числа на реальных машинах, и вы тыкаете в них, потому что думаете, что те или иные рассуждения логичны или нелогичны, потому что вы где-то что-то читали. Вместо того, чтобы гадать, поэкспериментируйте сами. Вы можете начать с моей программы. Он показывает примеры, которые я тестировал, поэтому вам не нужно спрашивать меня, что я тестировал или не тестировал. - person Olof Forshell; 29.05.2016
comment
один из лучших и самых подробных ответов, которые я когда-либо читал, спасибо! - person Gukki5; 01.09.2020
comment
TL; DR; продайте свою душу ????, прежде чем использовать какой-либо замок, независимо от того, как он реализован, если только вы абсолютно НЕ ДОЛЖНЫ. - person Gukki5; 01.09.2020
comment
@OlofForshell: Я наконец-то дошел до проверки своего утверждения о неограниченном xchg масштабировании, т.е. если каждый поток принимает / освобождает другую блокировку (в другой строке кеша). godbolt.org/z/MEjboaqMf - это код C ++, который компилируется в цикл, который просто повторяет xchg [rdi], ecx. Общее время выполнения программы на моем i7-6700k составляет 463 мс вне зависимости от того, выполняется ли она в 1 или 4 потоках (с разными atomic<int> объектами), так что это исключает любые виды общесистемной блокировки шины, подтверждая, что это всего лишь блокировка кеша ( Может ли num ++ быть атомарным для 'int num'?) - person Peter Cordes; 29.03.2021
comment
Конечно, потоки, борющиеся за одну и ту же блокировку, замедляют друг друга, и все, что вы говорите о минимизации блокировок в целом, хорошо, но единственное, что не является ужасно медленным, - это освобождение потока и повторное получение блокировку, если она не нужна другим потокам. - person Peter Cordes; 29.03.2021
comment
Ранее я предположил, что было бы лучше проверить доступность только для чтения, чтобы увидеть, доступна ли блокировка, прежде чем даже пытаться в первый раз. Это, вероятно, нехорошо и может привести к запросу общего доступа для получения доступа только для чтения до того, как RFO получит исключительное право собственности. См. Обсуждение в Записывает ли cmpxchg строку кэша назначения в случае сбоя? Если нет, то лучше ли это, чем xchg для спин-блокировки? - вращение только для чтения хорошо после того, как вы уже потерпели неудачу один раз, но, вероятно, не в первый раз. - person Peter Cordes; 29.03.2021
comment
Я думаю, вам нужно их рассчитать. Я предлагаю вам, по крайней мере, увидеть действительно быстрые неконфликтные блокировки, менее быстрые блокировки, которые замедлялись из-за общесистемной конкуренции на шине, и медленное время, когда могло произойти прерывание. Если для каждого прогона требуется 463 миллисекунды, это может быть так, как вы говорите, или может случиться так, что общесистемные конфликты редки, что может иметь место в случае нечастых событий управления шиной. Загрузка прерывания выглядит довольно постоянной. Если вы хронометрируете события индивидуально, вы либо увидите полную однородность, либо нет, и в этом случае вам необходимо представить гипотезы относительно того, почему. - person Olof Forshell; 30.03.2021
comment
За блокировками могут напрямую бороться другие потоки, желающие их, или косвенно через общесистемный конфликт шины, который не имеет ничего общего с самой блокировкой, он просто говорит, что шина не может поручиться за блокировку в данный момент времени, будучи на 100% безопасной из-за устройства, которые могут перезаписать память, содержащую блокировку, если им сначала не разрешено завершить то, что они делают. Это может показаться нелепым, но с точки зрения системы механизм блокировки шины не может и не должен быть связан с тем, что делает исполняющее программное обеспечение - это просто чрезвычайно грубый и дорогостоящий, но абсолютно необходимый механизм. - person Olof Forshell; 30.03.2021
comment
Не видел ваших комментариев, так как @user не адресовал их мне. Ядро может выполнить блокировку кеша для выполнения атомарного RMW в любое время, когда оно имеет исключительное владение MESI на строку кэша. Это исключительное владение - то же самое, что требуется для фиксации обычных хранилищ в кеш L1d, поэтому со стороны нет видимых признаков (на любой шине вне ядра ЦП) того, что ядро ​​выполнило атомарный RMW для выровненного объекта в нормальной кэшируемой памяти. . Блокировка шины может потребоваться только для разделения строки кэша и, возможно, также для некэшируемой памяти. - person Peter Cordes; 08.04.2021
comment
Конечно, если ядро ​​еще не имеет исключительного владения строкой кэша (например, после того, как какое-то другое ядро ​​только что разблокировало его), получение исключительного владения займет от 40 до 70 наносекунд (сотни циклов) , в зависимости от соединения между жилами. Я думаю, это то, что вы описываете как менее быстрые блокировки; обычно блокировка шины не используется. x86 имеет согласованный с кешем DMA; мастеринг шины устройством, записывающим на физический адрес, не делает недействительными строки кэша, отличные от той, в которую устройство записывает. Блокировка шины нужна только для редких вещей, таких как блокировка с разделением линий. - person Peter Cordes; 08.04.2021

В Википедии есть хорошая статья о спин-блокировках, вот реализация x86

http://en.wikipedia.org/wiki/Spinlock#Example_implementation

Обратите внимание, что их реализация не использует префикс «lock», потому что он избыточен на x86 для инструкции «xchg» - он неявно имеет семантику блокировки, как обсуждалось в этом обсуждении Stackoverflow:

На многоядерном x86 LOCK необходим в качестве префикса для XCHG?

REP: NOP - это псевдоним для инструкции PAUSE, вы можете узнать больше об этом здесь.

Как команда паузы x86 работает в спин-блокировке * и * можно ли ее использовать в других сценариях?

Что касается барьеров памяти, вот все, что вам может быть интересно знать

Барьеры памяти: взгляд на оборудование для программных хакеров Пол Э. Маккенни

http://irl.cs.ucla.edu/~yingdi/paperreading/whymb.2010.06.07c.pdf

person amdn    schedule 14.08.2012
comment
Почему в коде, который вы связали с реализацией Википедии, инструкция по установке значения блокировки в памяти на 0 должна быть атомарной (с использованием инструкции xchg)? Казалось бы, даже если другой поток обращается к ячейке памяти до того, как она установлена ​​в 0, все, что он увидит, - это то, что блокировка все еще заблокирована - не сработает ли прямое mov в ячейку памяти? - person VF1; 01.02.2014
comment
Да, mov работает на новых процессорах x86, но не на некоторых старых, что объясняется в следующем разделе, озаглавленном «Значительные оптимизации On later implementations of the x86 architecture, spin_unlock can safely use an unlocked MOV instead of the slower locked XCHG. This is due to subtle memory ordering rules which support this, even though MOV is not a full memory barrier. However, some processors (some Cyrix processors, some revisions of the Intel Pentium Pro (due to bugs), and earlier Pentium and i486 SMP systems) will do the wrong thing and data protected by the lock could be corrupted.. - person amdn; 01.02.2014
comment
Спасибо что подметил это. Если вы не возражаете, я спрошу, какой проблемы можно избежать с помощью этих тонких правил упорядочивания памяти? - person VF1; 01.02.2014
comment
Упорядочивание памяти в X86 требует, чтобы записи одного потока были видны другим потокам в том же порядке, что означает, что другой поток не может получить блокировку и не увидеть записи, сделанные в критическом разделе перед блокировкой. был выпущен. См. Раздел (4) этого документа spinroot.com/spin/Doc/course/ x86_tso.pdf - person amdn; 01.02.2014
comment
Кстати, я обнаружил, что тот факт, что MOV достаточно (не требуется сериализация или барьер памяти), был официально разъяснен в ответе Линусу Торвальдсу архитектором Intel еще в 1999 году lkml.org/lkml/1999/11/24/90 Я думаю, что позже было обнаружено, что это не работает для некоторых старых процессоров x86 . - person amdn; 01.02.2014

Просто спрашиваю:

Прежде чем углубиться в спин-блокировку и почти безблокирующие структуры данных:

Удостоверились ли вы в своих тестах и ​​в приложении, что конкурирующие потоки гарантированно работают на разных ядрах?

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

Чтобы дать вам цифру: в Windows у вас есть стандартный временной интервал в 10 миллисекунд. Если вы не убедитесь, что два физических потока участвуют в блокировке / разблокировке, вы получите около 500 блокировок / разблокировок в секунду, и этот результат будет очень meh

person Nils Pipenbrinck    schedule 14.08.2012
comment
Конечно, один поток должен блокировать / разблокировать спин-блокировку. Иначе ничего не защитит. И нет причин, по которым поток будет получать блокировку, отменять планирование и затем разблокировать каждый временной интервал. - person dave; 15.08.2012