Относительная производительность подкачки и блокировки сравнения и обмена на x86 - PullRequest
24 голосов
/ 17 марта 2011

Две распространенные идиомы блокировки:

if (!atomic_swap(lockaddr, 1)) /* got the lock */

и

if (!atomic_compare_and_swap(lockaddr, 0, val)) /* got the lock */

, где val может быть просто константой или идентификатором для нового предполагаемого владельца замка.

Что я хотел бы знать, так это то, что между двумя компьютерами на компьютерах с архитектурой x86 (и x86_64) наблюдается значительная разница в производительности. Я знаю, что это довольно широкий вопрос, поскольку ответ может сильно различаться в разных моделях процессоров, но это одна из причин, по которой я задаю SO, а не просто тесты для нескольких процессоров, к которым у меня есть доступ.

Ответы [ 5 ]

13 голосов
/ 17 марта 2011

Я нашел этот документ Intel, заявив, что на практике нет никакой разницы:

http://software.intel.com/en-us/articles/implementing-scalable-atomic-locks-for-multi-core-intel-em64t-and-ia32-architectures/

Один распространенный миф состоит в том, что блокировка, использующая инструкцию cmpxchg, дешевле, чем блокировка, использующая инструкцию xchg. Это используется, потому что cmpxchg не будет пытаться получить блокировку в монопольном режиме, так как cmp пройдет сначала. На рисунке 9 показано, что cmpxchg столь же дорог, как и инструкция xchg.

13 голосов
/ 17 марта 2011

Я предполагаю, что atomic_swap (lockaddr, 1) преобразуется в команду xchg reg, mem, а atomic_compare_and_swap (lockaddr, 0, val) переводится в cmpxchg [8b | 16b].

Некоторые Разработчики ядра Linux считают, что cmpxchg работает быстрее, потому что префикс блокировки не подразумевается, как в xchg.Так что, если вы используете однопроцессорный, многопоточный или иным образом убедитесь, что блокировка не нужна, вам, вероятно, лучше использовать cmpxchg.

Но есть вероятность, что ваш компилятор переведет его в "lock cmpxchg" ив этом случае это не имеет значения.Также обратите внимание, что, хотя задержки для этих инструкций являются низкими (1 цикл без блокировки и около 20 с блокировкой), если вам случится использовать общую переменную синхронизации между двумя потоками, что является довольно обычным, будут применены некоторые дополнительные циклы шины, которые длятся последниминавсегда по сравнению с задержками инструкций.Скорее всего, они будут полностью скрыты с помощью Snoop / Sync / Mem Access / Bus Lock / кэша длиной 200 или 500 процессорных циклов.

3 голосов
/ 17 августа 2012

На x86 любая инструкция с префиксом LOCK выполняет все операции с памятью как циклы чтения-изменения-записи.Это означает, что XCHG (с неявным LOCK) и LOCK CMPXCHG (во всех случаях, даже если сравнение не удается) всегда получают эксклюзивную блокировку на строке кэша.В результате разница в производительности практически отсутствует.

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

2 голосов
/ 25 апреля 2017

Вы уверены, что не имели в виду

 if (!atomic_load(lockaddr)) {
       if (!atomic_swap(lockaddr, val)) /* got the lock */

для второго?

Тестирование и тестирование и установка блокировок (см. Википедия https://en.wikipedia.org/wiki/Test_and_test-and-set) довольнообщая оптимизация для многих платформ.

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

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

Рисунок 8 из документа, который обнаружил Бо Перссон http://software.intel.com/en-us/articles/implementing-scalable-atomic-locks-for-multi-core-intel-em64t-and-ia32-architectures/, показывает, что блокировки «Тестировать и тестировать» и «Установить» превосходят производительность.

1 голос
/ 07 июля 2017

С точки зрения производительности на процессорах Intel это то же самое, но для простоты, чтобы было проще понять, я предпочитаю первый способ из приведенных вами примеров.Нет никакой причины использовать cmpxchg для получения блокировки, если вы можете сделать это с xchg.

Согласно принципу бритвы Оккама, простые вещи лучше.

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

Нет единого мнения о том, должно ли снятие блокировки быть обычным хранилищем или lock -ед магазин.Например, LeaveCriticalSection в Windows 10 использует lock -ed-хранилище для снятия блокировки даже на процессоре с одним сокетом;в то время как на нескольких физических процессорах с неоднородным доступом к памяти (NUMA) проблема снятия блокировки: обычное хранилище против хранилища с lock может быть даже более важной.

См. этоПример более безопасных функций блокировки, которые проверяют данные на достоверность и отлавливают попытки снять блокировки, которые не были получены:

const
  cLockAvailable = 107; // arbitrary constant, use any unique values that you like, I've chosen prime numbers
  cLockLocked    = 109;
  cLockFinished  = 113;

function AcquireLock(var Target: LONG): Boolean; 
var
  R: LONG;
begin
  R := InterlockedExchange(Target, cLockByteLocked);
  case R of
    cLockAvailable: Result := True; // we've got a value that indicates that the lock was available, so return True to the caller indicating that we have acquired the lock
    cLockByteLocked: Result := False; // we've got a value that indicates that the lock was already acquire by someone else, so return False to the caller indicating that we have failed to acquire the lock this time
      else
        begin
          raise Exception.Create('Serious application error - tried to acquire lock using a variable that has not been properly initialized');
        end;
    end;
end;

procedure ReleaseLock(var Target: LONG);
var
  R: LONG;
begin
  // As Peter Cordes pointed out (see comments below), releasing the lock doesn't have to be interlocked, just a normal store. Even for debugging we use normal load. However, Windows 10 uses locked release on LeaveCriticalSection.
  R := Target;
  Target := cLockAvailable;
  if R <> cLockByteLocked  then
  begin
    raise Exception.Create('Serious application error - tried to release a  lock that has not been actually locked');
  end;
end;

Ваше основное приложение находится здесь:

var
  AreaLocked: LONG;
begin
  AreaLocked := cLockAvailable; // on program initialization, fill the default value

  .... 

 if AcquireLock(AreaLocked) then
 try
   // do something critical with the locked area
   ... 

 finally
   ReleaseLock(AreaLocked); 
 end;

....

  AreaLocked := cLockFinished; // on program termination, set the special value to catch probable cases when somebody will try to acquire the lock

end.

Вы также можете использоватьследующий код в виде спин-цикла, он использует нормальную загрузку при вращении для экономии ресурсов, как это предложил Питер Кордес.После 5000 циклов он вызывает функцию API Windows SwitchToThread ().Это значение 5000 циклов является моим эмпирическим.Значения от 500 до 50000 также кажутся нормальными, в некоторых случаях более низкие значения лучше, а в других - лучше.Обратите внимание, что вы можете использовать этот код только на процессорах, которые поддерживают SSE2 - вы должны проверить соответствующий бит CPUID перед вызовом инструкции pause - в противном случае будет просто потеря энергии.На процессорах без pause просто используйте другие средства, такие как EnterCriticalSection / LeaveCriticalSection или Sleep (0), а затем Sleep (1) в цикле.Некоторые люди говорят, что на 64-битных процессорах вы можете не проверять SSE2, чтобы убедиться, что инструкция pause реализована, потому что оригинальная архитектура AMD64 приняла SSE Intel и SSE2 в качестве основных инструкций, и, практически, если вы запускаете 64-битовый код, у вас уже есть SSE2 и, следовательно, инструкция pause.Тем не менее, Intel не рекомендует полагаться на особую функцию присутствия и прямо заявляет, что определенная функция может исчезнуть в будущих процессорах, и приложения всегда должны проверять функции через CPUID.Однако инструкции SSE стали вездесущими, и многие 64-разрядные компиляторы используют их без проверки (например, Delphi для Win64), поэтому шансы на то, что в некоторых будущих процессорах не будет SSE2, не говоря уже о pause, очень малы.

// on entry rcx = address of the byte-lock
// on exit: al (eax) = old value of the byte at [rcx]
@Init:
   mov  edx, cLockByteLocked
   mov  r9d, 5000
   mov  eax, edx
   jmp  @FirstCompare
@DidntLock:
@NormalLoadLoop:
   dec  r9
   jz   @SwitchToThread // for static branch prediction, jump forward means "unlikely"
   pause
@FirstCompare:
   cmp  [rcx], al       // we are using faster, normal load to not consume the resources and only after it is ready, do once again interlocked exchange
   je   @NormalLoadLoop // for static branch prediction, jump backwards means "likely"
   lock xchg [rcx], al
   cmp  eax, edx        // 32-bit comparison is faster on newer processors like Xeon Phi or Cannonlake.
   je   @DidntLock
   jmp  @Finish
@SwitchToThread:
   push  rcx
   call  SwitchToThreadIfSupported
   pop   rcx
   jmp  @Init
@Finish:
...