Я изучаю атомарные особенности процессора Intel на моем процессоре Haswell (4/8 ядро 2.3-3.9 ГГц i7-4790M), и считаю, что его действительно сложно построить, например.надежные операции mutex_lock () и mutex_unlock (), как предлагается, например, в руководстве GCC:
6,53 x86-специфичные расширения модели памяти для транзакционной памяти
Архитектура x86 поддерживаетдополнительные флаги упорядочения памяти для обозначения критических секций блокировки для устранения аппаратной блокировки.Они должны быть указаны в дополнение к существующей модели памяти для атомарных встроенных функций.
'__ATOMIC_HLE_ACQUIRE'
Start lock elision on a lock variable. Memory model must be
'__ATOMIC_ACQUIRE' or stronger.
'__ATOMIC_HLE_RELEASE'
End lock elision on a lock variable. Memory model must be
'__ATOMIC_RELEASE' or stronger.
При сбое получения блокировки требуется для хорошей производительности, чтобы быстро прервать транзакцию.Это можно сделать с помощью '_mm_pause'
#include <immintrin.h> // For _mm_pause
int lockvar;
/* Acquire lock with lock elision */
while (__atomic_exchange_n(&lockvar, 1,
__ATOMIC_ACQUIRE|__ATOMIC_HLE_ACQUIRE))
_mm_pause(); /* Abort failed transaction */
...
/* Free lock with lock elision */
__atomic_store_n(&lockvar, 0, __ATOMIC_RELEASE|__ATOMIC_HLE_RELEASE);
Итак, прочитав это, и в разделе 8.1 Руководства разработчика программного обеспечения Intel, раздел 8.1, «Заблокированные атомарные операции», в частности, в разделе 8.1.4, «Влияние операции LOCK на внутренние кэши процессора», побудило меня сначала реализовать мой тест mutex_lock () mutex_unlock (), например:
...
static inline <strong>attribute</strong>((always_inline,const))
bool ia64_has_clflush(void)
{ register unsigned int
ebx=0;
asm volatile
( "MOV $7, %%eax\n\t"
"MOV $0, %%ecx\n\t"
"CPUID\n\t"
"MOV %%ebx, %0\n\t"
: "=r" (ebx) :
: "%eax", "%ecx", "%ebx"
);
return ((ebx & (1U<<23)) ? true : false);
}</p>
<pre><code>#define _LD_SEQ_CST_ __ATOMIC_SEQ_CST
#define _ST_SEQ_CST_ __ATOMIC_SEQ_CST
#define _ACQ_SEQ_CST_ (__ATOMIC_SEQ_CST|__ATOMIC_HLE_ACQUIRE)
#define _REL_SEQ_CST_ (__ATOMIC_SEQ_CST|__ATOMIC_HLE_RELEASE)
static bool has_clflush=false;
static
void init_has_clflush(void)
{ has_clflush = ia64_has_clflush();
}
static
void init_has_clflush(void) __attribute__((constructor));
static inline __attribute__((always_inline))
void mutex_lock( register _Atomic int *ua )
{ // the SDM states that memory to be used as semaphores
// should not be in the WB cache memory, but nearest we
// can get to uncached memory is to explicitly un-cache it:
if(has_clflush)
asm volatile
( "CLFLUSHOPT (%0)"
:: "r" (ua)
);
// why isn't the cache flush enough?
else
asm volatile
( "LFENCE" :: );
register unsigned int x;
x = __atomic_sub_fetch( ua, 1, _ACQ_SEQ_CST_);
_mm_pause();
if(has_clflush)
asm volatile
( "CLFLUSHOPT (%0)"
:: "r" (ua)
);
else
asm volatile
( "SFENCE" :: );
while((x = __atomic_load_n(ua,_LD_SEQ_CST_)) != 0)
switch(syscall( SYS_futex, ua, FUTEX_WAIT, x, nullptr,nullptr,0))
{case 0:
break;
case -1:
switch( errno )
{ case EINTR:
case EAGAIN:
continue;
default:
fprintf(stderr,"Unexpected futex error: %d : '%s'.", errno,
strerror(errno));
return;
}
}
}
static inline __attribute__((always_inline))
void mutex_unlock( register _Atomic int *ua )
{ if(has_clflush)
asm volatile
( "CLFLUSHOPT (%0)"
:: "r" (ua)
);
else
asm volatile( "LFENCE" :: );
register unsigned int x;
x = __atomic_add_fetch( ua, 1, _REL_SEQ_CST_);
_mm_pause();
if(has_clflush)
asm volatile
( "CLFLUSHOPT (%0)"
:: "r" (ua)
);
else
asm volatile ( "SFENCE" :: );
if(x == 0)
while( (1 < syscall( SYS_futex, ua, FUTEX_WAKE, 1,
nullptr,nullptr,0)) && (errno == EINTR));
}
Теперь интересно то, что критические операции вычитания mutex_lock () и сложения mutex_unlock () заканчиваются в виде инструкций:
mutex_lock:
# 61 "intel_lock1.c" 1
CLFLUSHOPT (%rbx)
# 0 "" 2
#NO_APP
.L7:
lock xacquire subl $1, lck(%rip)
rep nop
cmpb $0, has_clflush(%rip)
je .L8
#APP
# 72 "intel_lock1.c" 1
CLFLUSHOPT (%rbx)
# 0 "" 2
mutex_unlock:
#APP
# 98 "intel_lock1.c" 1
CLFLUSHOPT (%rbx)
# 0 "" 2
#NO_APP
.L24:
movl $1, %eax
lock xacquire xaddl %eax, lck(%rip)
rep nop
addl $1, %eax
cmpb $0, has_clflush(%rip)
je .L25
#APP
# 109 "intel_lock1.c" 1
CLFLUSHOPT (%rbx)
# 0 "" 2
#NO_APP
Но эта реализация, похоже, требует, чтобы LFENCE / SFENCE работали надежно (CLFLUSHOPT недостаточно), в противном случае оба потока могут оказаться заблокированными в futex () со значением блокировки, равным -1.
Из прочтения документации Intel не видно, как может получиться, что два потока, вводящие последовательность команд:
# %rbx == $lck
CLFLUSHOPT (%rbx)
lock xacquire subl $1, lck(%rip)
rep nop
, могут оба завершиться с результатом '-1' в *lck, если * lck был 0;конечно, один поток ДОЛЖЕН получить -1, а другой - -2?
Но Стрейс не говорит:
strace: Process 11978 attached with 2 threads
[pid 11979] futex(0x60209c, FUTEX_WAIT, 4294967295, NULL <unfinished ...>
[pid 11978] futex(0x60209c, FUTEX_WAIT, 4294967295, NULL^C
, это тупиковая ситуация.Где я ошибся?
Пожалуйста, любые специалисты Intel по блокировке и кэшированию процессоров Intel могут объяснить, как два атомных уменьшения или приращения одного и того же некэшированного местоположения * lck, оба утверждают сигнал шины #LOCK (эксклюзивный доступ к шине)и XACQUIRE может в итоге получить тот же результат в * lck?
Я думал, что это то, что префикс #LOCK (и HLE) должен был предотвратить?Я пытался НЕ использовать HLE и просто __ATOMIC_SEQ_CST для всех обращений (это просто добавляет префикс LOCK, а не XACQUIRE), но это не имеет значения - тупик по-прежнему приводит к отсутствию {L, S} FENCE-ов.
Я прочитал отличную статью Ульриха Дреппера [Futexes is Tricky]: http://www.akkadia.org/drepper/futex.pdf, но он представляет реализацию мьютекса, которая записывает только жестко запрограммированные константы в память блокировки.Я понимаю почему.Очень трудно заставить мьютекс работать надежно с счетчиком официантов или любым другим арифметическим, сделанным на значении блокировки.Кто-нибудь нашел способы сделать надежную блокированную арифметику такой, чтобы результат соответствовал значению блокировки / семафора в Linux x86_64?Больше всего интересно обсудить их ...
Так что после нескольких тупиков, исследующих HLE & CLFLUSH, ЕДИНСТВЕННАЯ рабочая версия блокировки / разблокировки, на которую я смог прийти, использует жестко закодированные константы и __atomic_compare_exchange_n -Полный источник тестовой программы, который увеличивает счетчик (без блокировки) до получения сигнала + / выхода, находится по адресу:
Рабочий пример: intel_lock3.c
[]: https://drive.google.com/open?id=1ElB0qmwcDMxy9NBYkSXVxljj5djITYxa
enum LockStatus
{ LOCKED_ONE_WAITER = -1
, LOCKED_NO_WAITERS = 0
, UNLOCKED=1
};
static inline __attribute__((always_inline))
bool mutex_lock( register _Atomic int *ua )
{ register int x;
int cx;
lock_superceded:
x = __atomic_load_n( ua, _LD_SEQ_CST_ );
cx = x;
x = (x == UNLOCKED)
? LOCKED_NO_WAITERS
: LOCKED_ONE_WAITER;
if (! __atomic_compare_exchange_n
( ua, &cx, x, false, _ACQ_SEQ_CST_, _ACQ_SEQ_CST_) )
goto lock_superceded;
if( x == LOCKED_ONE_WAITER )
{ do{
switch(syscall( SYS_futex, ua, FUTEX_WAIT, x, nullptr,nullptr,0))
{case 0:
break;
case -1:
switch( errno )
{ case EINTR:
return false;
case EAGAIN:
break;
default:
fprintf(stderr,"Unexpected futex WAIT error: %d : '%s'.",
errno, strerror(errno));
return false;
}
}
x = __atomic_load_n(ua,_LD_SEQ_CST_);
} while(x < 0);
}
return true;
}
static inline __attribute__((always_inline))
bool mutex_unlock( register _Atomic int *ua )
{ register int x;
int cx;
unlock_superceded:
x = __atomic_load_n( ua, _LD_SEQ_CST_ );
cx = x;
x = (x == LOCKED_ONE_WAITER)
? LOCKED_NO_WAITERS
: UNLOCKED;
if (! __atomic_compare_exchange_n
( ua, &cx, x, false, _ACQ_SEQ_CST_, _ACQ_SEQ_CST_) )
goto unlock_superceded;
if(x == LOCKED_NO_WAITERS)
{ while((1 <
syscall( SYS_futex, ua, FUTEX_WAKE, 1, nullptr,nullptr,0))
||( UNLOCKED != __atomic_load_n( ua, _LD_SEQ_CST_ ))
) // we were a waiter, so wait for locker to unlock !
{ if( errno != 0 )
switch(errno)
{case EINTR:
return false;
case EAGAIN:
break;
default:
fprintf(stderr,
"Unexpected futex WAKE error: %d : '%s'.",
errno, strerror(errno));
return false;
}
}
}
return true;
}
Build & Test (GCC 7.3.1 & 6.4.1 & 5.4.0) used:
$ gcc -std=gnu11 -march=x86-64 -mtune=native -D_REENTRANT \
-pthread -Wall -Wextra -O3 -o intel_lock3 intel_lock3.c
$ ./intel_lock3
# wait a couple of seconds and press ^C
^C59362558
Нерабочая версия с использованием арифметики:
https://drive.google.com/open?id=10yLrohdKLZT4p3G1icFHdjF5eHY68Yws
Компилировать, например, с:
$ gcc -std=gnu11 -march=x86_64 -mtune=native -O3 -Wall -Wextra
-o intel_lock2 intel_lock2.c
$ ./intel_lock2
# wait a couple of seconds and press ^C
$ ./intel_lock2
^Cwas locked!
446
Не должно быть печати "былозаблокирован!»и в течение пары секунд должно быть превышено число, напечатанное в конце, @ 5e8: 5x10 ^ 8, а не 446.
Запуск с помощью strace показывает, что два потока блокируют в ожидании значения блокировки -1 становится 0:
$ strace -f -e trace=futex ./intel_lock2
strace: Process 14481 attached
[pid 14480] futex(0x602098, FUTEX_WAIT, 4294967295, NULL <unfinished ...>
[pid 14481] futex(0x602098, FUTEX_WAKE, 1 <unfinished ...>
[pid 14480] <... futex resumed> ) = -1 EAGAIN (Resource temporarily
unavailable)
[pid 14481] <... futex resumed> ) = 0
[pid 14480] futex(0x602098, FUTEX_WAKE, 1 <unfinished ...>
[pid 14481] futex(0x602098, FUTEX_WAIT, 4294967295, NULL <unfinished ...>
[pid 14480] <... futex resumed> ) = 0
[pid 14481] <... futex resumed> ) = -1 EAGAIN (Resource temporarily
unavailable)
[pid 14480] futex(0x602098, FUTEX_WAIT, 4294967295, NULL <unfinished ...>
[pid 14481] futex(0x602098, FUTEX_WAIT, 4294967295, NULL^C <unfinished
...>
[pid 14480] <... futex resumed> ) = ? ERESTARTSYS (To be restarted
if SA_RESTART is set)
strace: Process 14480 detached
strace: Process 14481 detached
was locked!
7086
$
Обычно WAIT следует планировать до WAKE, но каким-то образом GCC интерпретирует семантику упорядочения памяти так, чтоWAKE всегда назначается раньше, чем WAIT;но даже если это произойдет, код должен просто задерживаться и никогда не заканчиваться тем, что два потока получат значение -1 lck при входе в futex (... FUTEX_WAIT ..).
Почти идентичный алгоритм, использующий арифметику для значения блокировки, ВСЕГДА блокируется, когда оба потока получают (-1, -1) - обратите внимание, значение -2 никогда не будет замечено никаким потоком:
static inline __attribute__((always_inline))
bool mutex_lock( register _Atomic volatile int *ua )
{ register int x;
x = __atomic_add_fetch( ua, -1, _ACQ_SEQ_);
if( x < 0 )
{ do{
// here you can put:
// if( x == -2) { .. NEVER REACHED! }
switch(syscall( SYS_futex, ua, FUTEX_WAIT, x, nullptr,nullptr,0))
{case 0:
break;
case -1:
switch( errno )
{ case EINTR:
return false; // interrupted - user wants to exit?
case EAGAIN:
break;
default:
fprintf(stderr,"Unexpected futex WAIT error: %d : '%s'.",
errno, strerror(errno));
return false;
}
}
x = __atomic_load_n(ua,_LD_SEQ_);
} while(x < 0);
}
return true;
}
static inline __attribute__((always_inline))
bool mutex_unlock( register _Atomic volatile int *ua )
{ register int x;
x = __atomic_add_fetch( ua, 1, _REL_SEQ_);
if(x == 0) // there was ONE waiter
while( (1 <
syscall( SYS_futex, ua, FUTEX_WAKE, 1, nullptr,nullptr,0)
)
||(1 < __atomic_load_n(ua, _LD_SEQ_)
) // wait for first locker to unlock
)
{ if( errno != 0 )
switch(errno)
{case EINTR:
return false;
case EAGAIN:
break;
default:
fprintf(stderr,"Unexpected futex WAKE error: %d : '%s'.",
errno, strerror(errno));
return false;
}
}
return true;
}
Итак, я думаю, если бы арифметические операции работали так, как ожидалось, т.е.были бы сериализованными и атомарными, тогда вышеприведенный код не блокировалсяарифметика должна генерировать те же числа, что и значения перечисления LockStatus, используемые в рабочем примере.
Но что-то не так с арифметикой, которая теперь выдает инструкции:
mutex_lock:
movl $-1, %eax
lock xaddl %eax, (%rdx)
mutex_unlock:
movl $1, %eax
lock xaddl %eax, (%rdx)
Theсам код не вставляет никаких ограждений, но каждый __atomic_store_n (ua, ...) генерирует единицу.
AFAICS, нет допустимого расписания для этого кода, в результате которого оба потока получают одинаковое значение -1.
Таким образом, мой вывод заключается в том, что использование префикса Intel LOCK в арифметических инструкциях небезопасно и приводит к ошибочному поведению в пользовательских режимах скомпилированных программ Linux x86_64 gcc - только запись постоянных значений из текстовой памяти в память данных атомарна и последовательно упорядочена поПлатформы Intel Haswell i7-4790M с gcc и Linux и арифметика на таких платформах не могут быть сделаны атомарными и последовательно упорядочены с использованием любой комбинации HLE / XACQUIRE, префикса блокировки или инструкций FENCE.
Мои догадкиявляется то, что предсказание ветвления почему-то терпит неудачу и добавляет дополнительную арифметическую операцию / неудачу к pвыполнить арифметическую операцию на этой платформе с установленным префиксом LOCK и несколькими потоками на разных физических ядрах.Поэтому все арифметические операции с заявленным префиксом LOCK являются подозрительными и их следует избегать.