如何使用XACQUIRE,XRELEASE硬件锁定清除(HLE)前缀提示?

时间:2018-07-29 07:06:26

标签: assembly x86 x86-64 intel cpu-architecture

仅出于学习目的,我试图掌握如何使用HLE prefixes XACQUIREXRELEASE。阅读英特尔文档后,我的理解是,执行带有前缀XACQUIRE的指令后,CPU会进行某种写锁定,直到带有前缀XRELEASE的指令为止。因此,我编写了以下测试代码以查看我是否正确。好吧,由于我的代码示例失败,还有一些我不理解的地方。

那么有人可以告诉我那些HLE前缀是什么吗?

两个失败:

  1. xtest指令报告未启用HLE,并且

  2. 因为我假定的“互斥量”代码无法作为互斥量运行,所以并发失败。

接下来是Windows C ++项目,该项目是使用VS 2017和x64 .asm文件进行编译的,如下所示:

.code

testCPUID PROC
    push rbx

    ; CPUID.07h.EBX.HLE[bit 4]==1

    mov eax, 7h
    xor ecx, ecx
    cpuid
    and rbx, 1 shl 4

    mov rax, rbx
    pop rbx
    ret
testCPUID ENDP



testHLEWrite PROC
    ; RCX = pointer to TST91 struct:
    ;       void* pPtrToNextWrite;
    ;       int nNextValue;
    ;       void* pCutoffPtr;
    ;       void* pBeginPtr;

    xor edx, edx
    xacquire xchg [rcx], rdx        ; I'm assuming that this will work as a mutex ...

    xtest                           ; Sanity check to see if HLE got enabled?
    jnz lbl_00                      ; If HLE is on => ZF=0
    int 3                           ; we get here if HLE did not get enabled
lbl_00:

    ; Do some nonsensical stuff
    ; The idea is to write sequential values into a shared array
    ; to see if the lock above holds
    ; Format:
    ;       > --16 sequential bytes-- <

    mov r8d, dword ptr [rcx + 8]

    mov byte ptr [rdx], '>'
    inc rdx

    ; Write 16 sequential bytes

    mov rax, 10h
lbl_01:
    mov byte ptr [rdx], r8b
    inc r8
    inc rdx
    dec rax
    jnz lbl_01

    mov byte ptr [rdx], '<'
    inc rdx

    cmp rdx, [rcx + 10h]            ; check if reached the end of buffer
    jb lbl_02
    mov rdx, [rcx + 18h]            ; reset ptr to the beginning of buffer
lbl_02:

    mov dword ptr [rcx + 8], r8d
    xrelease mov [rcx], rdx         ; this will release the mutex

    ret
testHLEWrite ENDP





testHLEForCorrectness PROC
    ; RCX = pointer to TST91 struct:
    ;       void* pPtrToNextWrite;
    ;       int nNextValue;
    ;       void* pCutoffPtr;
    ;       void* pBeginPtr;

    xor edx, edx
    xacquire xchg [rcx], rdx        ; I'm assuming that this will work as a mutex ...

    xtest                           ; Sanity check to see if HLE got enabled?
    jnz lbl_00                      ; If HLE is on => ZF=0
    int 3                           ; we get here if HLE did not get enabled
lbl_00:

    mov r9, [rcx + 18h]

lbl_repeat:
    cmp r9, rdx
    jae lbl_out

    cmp byte ptr [r9], '>'
    jnz lbl_bad
    cmp byte ptr [r9 + 1 + 10h], '<'
    jnz lbl_bad

    mov r8b, byte ptr [r9 + 1]
    sub eax, eax
lbl_01:
    cmp [r9 + rax + 1], r8b
    jnz lbl_bad
    inc rax
    inc r8
    cmp rax, 10h
    jb lbl_01

    add r9, 2 + 10h
    jmp lbl_repeat

lbl_out:

    xrelease mov [rcx], rdx         ; this will release the mutex

    ret

lbl_bad:
    ; Verification failed
    int 3

testHLEForCorrectness ENDP

END

这是从用户模式C ++项目中调用的方式:

#include <assert.h>
#include <Windows.h>

struct TST91{
    BYTE* pNextWrite;
    int nNextValue;
    BYTE* pCutoffPtr;
    BYTE* pBeginPtr;
};

extern "C" {
    BOOL testCPUID(void);
    void testHLEWrite(TST91* p);
    void testHLEForCorrectness(TST91* p);
};

DWORD WINAPI ThreadProc01(LPVOID lpParameter);

TST91* gpStruct = NULL;
BYTE* gpMem = NULL;             //Its size is 'gszcbMemSize' BYTEs
const size_t gszcbMemSize = 0x1000 * 8;

int main()
{
    if(testCPUID())
    {
        gpStruct = new TST91;
        gpMem = new BYTE[gszcbMemSize];

        gpStruct->pNextWrite = gpMem;
        gpStruct->nNextValue = 1;
        gpStruct->pBeginPtr = gpMem;
        gpStruct->pCutoffPtr = gpMem + gszcbMemSize - 0x100;

        for(int t = 0; t < 5; t++)
        {
            CloseThread(CreateThread(NULL, 0, 
                ThreadProc01, (VOID*)(1LL << t), 0, NULL));
        }

        _gettch();

        delete gpStruct;
        delete[] gpMem;
    }
    else
        _tprintf(L"Your CPU doesn't support HLE\n");

   return 0;
}

DWORD WINAPI ThreadProc01(LPVOID lpParameter)
{
    if(!SetThreadAffinityMask(GetCurrentThread(), (DWORD_PTR)lpParameter))
    {
        assert(NULL);
    }

    for(;;)
    {
        testHLEWrite(gpStruct);
        testHLEForCorrectness(gpStruct);
    }

    return 0;
}

2 个答案:

答案 0 :(得分:4)

您可以回答自己的问题,不是吗?

无论如何。我想我明白了。我会尽量坚持朴素的英语,或者遵循我的理解。如果我的陈述不正确,请随时进行编辑。 (顺便说一下,Hardware Lock Elision,这个名字真酷。听起来像是Matt Damon的一部电影。我什至不得不用Google单词“ elision”来理解它的含义……我仍然不记得它。)

因此,此HLE概念无非是暗示CPU以更优化的方式处理lock前缀。对于现代处理器以有效方式执行,lock前缀本身在某种程度上“昂贵”。因此,当支持它的CPU看到HLE前缀时,它最初将不会获取锁,而只有在发生读/写冲突时才会这样做。在这种情况下,CPU将发出HLE中止,这将需要稍后的常规锁定。

此外,XACQUIRE的HLE前缀为F2,而XRELEASE的HLE前缀为F3,仅是传统的REPNEREP前缀,不支持HLE的旧式CPU与支持lock的指令一起使用时,将被忽略。这意味着使用HLE无需检查CPUID指令是否提供支持,就可以安全地按原样使用它们。较旧的CPU将忽略它们,并将随附的lock前缀视为锁,而较新的CPU将把它们作为优化提示。换句话说,如果将前缀XACQUIREXRELEASE添加到自己的互斥量,信号量实现中,则不会造成任何损害。

因此,我不得不这样重写我的原始测试代码示例(只是非常基本的互斥锁类型的相关并发部分)。

用于输入锁的ASM代码:

testHLEWrite PROC
    ; RCX = pointer to TST91 struct:
    ;       void* pPtrToNextWrite;
    ;       int nNextValue;
    ;       void* pCutoffPtr;
    ;       void* pBeginPtr;
    ;       size_t lock;          <-- new member

lbl_retry:
    xacquire lock bts qword ptr [rcx + 20h], 1      ; Try to acquire lock (use HLE hint prefix)
    jnc lbl_locked
    pause                       ; Will issue an implicit HLE abort
    jmp lbl_retry


lbl_locked:

然后离开锁:

(请注意,XRELEASE前缀与lock前缀的不同之处在于,它支持具有内存目标操作数的mov指令。)

    xrelease mov qword ptr [rcx + 20h], 0       ; Release the lock (use HLE prefix hint)

    ret
testHLEWrite ENDP

此外,如果您想使用(Visual Studio)内部函数使用C编写它,

//Some variable to hold the lock
volatile long lock = 0;

然后是代码本身:

//Acquire the lock
while(_interlockedbittestandset_HLEAcquire((long *)&lock, 1))
{
    _mm_pause();
}

然后:

//Leave the lock
_Store_HLERelease(&lock, 0);

最后,我想指出的是,我没有对带有或不带有HLE前缀的代码性能进行任何时序/基准测试。因此,如果有人愿意这样做(并了解HLE概念的有效性),那么欢迎您。我也很高兴学习它。

答案 1 :(得分:2)

您说您的CPU是Haswell。

对于所有Haswell CPU,通过微码更新禁用了

TSX(HLE和RTM)。您正在运行Windows,因此我们可以安全地假设您的系统自动使用最新的微码。 (您不必刷新BIOS;操作系统可以在每次引导时安装更新的CPU微代码。)

另请参见Which CPUs support TSX, with the erratum fixed?https://en.wikipedia.org/wiki/Transactional_Synchronization_Extensions。我不能排除Haswell可以使用TSX的新步伐,但是xtest设置ZF的最可能解释是微代码更新不会禁用TSX指令的解码(否则xtest会# UD),但是确实禁止实际进入交易区域。 (即立即将每笔交易都视为中止。)

如果是这种情况,那么xacquire xchg的执行方式与普通xchg相同,以非事务方式运行后面的指令。 (Unlike with RTM (xbegin),中止地址是单独给出的。)


但是,如果我错了,并且您以某种方式在处理HLE方面确实遇到问题,那么我们可以查看有关中止交易的其他可能解释(当我们通过关键操作时会导致到达int3进行非交易部分,然后到达xtest)。

我不认为您的事务太大(接触过多的缓存行会导致异常中止,但我认为情况并非如此)。当Haswell发行时,David Kanter的guess about the internal implementation使用L1d作为事务缓冲区turned out to be correct。 (而且AFAIK,Skylake仍仅使用L1d,而不跟踪L2或L3中的读取集或写入集)。但是您只触及1或2行。包含指针的行和指向的行。

事务内部的中断可能会导致偶尔的中止,因此不要惊讶地发现 some 事务中止。仅当它们总是中止时,这表示您正在执行事务无法处理的事情,或者CPU禁用了HLE。

用作锁定的变量还必须满足某些属性

  

XACQUIRE manual entry

     

锁定成功必须满足Intel®64和IA-32体系结构软件开发人员手册,卷1,第16.3.3节中描述的准则,才能成功进行清除,否则可能会发出HLE中止的信号。

从SDM第1卷开始:

  

16.3.3 HLE锁的要求

     

要使HLE执行成功以事务方式提交,锁必须满足某些属性,并且   进入锁必须遵循某些准则。

     
      
  • XRELEASE前缀的指令必须将被消除的锁的值恢复到   获取锁之前的值。这使硬件能够   通过不将锁添加到写入集中来安全地删除锁。数据大小   和锁释放指令(带前缀XRELEASE)的数据地址   必须与获取的锁(带XACQUIRE前缀)和锁的匹配   一定不能越过缓存行边界。

  •   
  • 软件不应写入   带有任何指令的事务性HLE区域内的隐藏锁   XRELEASE前缀指令以外的其他指令,否则可能会导致   事务中止。另外,递归锁(其中一个线程   多次获取相同的锁,而无需先释放   锁定)也可能导致事务中止。请注意,该软件可以   观察在关键区域内获取锁的结果   部分。这样的读取操作将返回写入的值   锁。

  •   
     

处理器会自动检测到这些违规情况   准则,并安全过渡到非交易执行   没有省略。由于英特尔TSX可以细粒度地检测到冲突   高速缓存行的数据,写入与相同高速缓存行并置的数据   其他逻辑可能会将删除的锁检测为数据冲突   拥有相同锁的处理器

因此,只有在pPtrToNextWrite == pBeginPtr时才能提交事务,因为这是您用来解锁的值,而不是您用rdx读入xchg的原始值。看起来像在进行xchg保存该值然后在循环中递增之前仅复制寄存器会更容易。

但是除此之外,它还具有惊人的灵活性。听起来好像硬件不在意0是否表示锁定,而0xdeadbeef(或指针值)表示是否可用。

由程序员来设计一个正确的锁定方案,如果它发现已经获得了锁定,则该方案不会存储回先前的值,并在非事务性运行时保护关键部分。