【问题标题】:How to use XACQUIRE, XRELEASE Hardware Lock Elision (HLE) prefix hints?如何使用 XACQUIRE、XRELEASE Hardware Lock Elision (HLE) 前缀提示?
【发布时间】:2019-01-05 18:22:04
【问题描述】:

只是为了学习这个,我试图掌握如何使用HLE prefixesXACQUIREXRELEASE。阅读英特尔文档后,我的理解是,在执行带有 XACQUIRE 前缀的指令后,CPU 会进入某种写锁定状态,直到带有 XRELEASE 前缀的指令。所以我写了下面的测试代码,看看我是否正确。嗯,还有一些我不明白的地方,因为我的代码示例失败了。

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

两次失败:

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

  2. 因为我假设的“mutex-ed”代码不作为互斥体运行,所以并发失败。

接下来是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;
}

【问题讨论】:

  • 你在哪个 CPU 上运行它?
  • 请注意,对于 HLE(与 RTM 不同),the abort address is the address of the xacquire-enabled instruction。因此,到达int3 可能发生在事务由于某种原因中止之后。
  • @PeterCordes:支持它的是 haswell i7。我不得不在下面发布答案。至于带有 HLE 的 xtest 指令,它的工作原理对我来说仍然是个谜。也许您可以对此有所了解?
  • 所以您禁用了 Haswell 上的微码更新以进行测试?或者他们只是在 Haswell 中禁用了 RTM,然后在早期的 Broadwell 中再次禁用,在发现极端情况错误之后? Which CPUs support TSX, with the erratum fixed?。当使用最新的微码时,我认为没有带有工作 TSX (RTM + HLE) 的 Haswell CPU。
  • 要么是这样,要么你试图在单个事务中做太多事情。我看到你有一个循环,但我没有详细了解它有多少缓存行将成为事务的一部分。

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


【解决方案1】:

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

无论如何。我想我明白了。我会尽量坚持简单的英语,或者按照我的理解去。如果我做出不正确的陈述,请随时编辑它。 (顺便说一句,Hardware Lock Elision,多么酷的名字。听起来像是马特·达蒙的电影。我什至不得不用谷歌搜索“省略”这个词来理解它的含义……但我还是不记得了。)

所以这个 HLE 概念无非是提示 CPU 以更优化的方式处理 lock 前缀。 lock 前缀本身对于现代处理器以有效的方式执行来说有点“昂贵”。因此,当支持它的 CPU 看到 HLE 前缀时,它最初不会获取锁,但只有在存在读/写冲突时才会这样做。在这种情况下,CPU 将发出 HLE 中止,这反过来又需要后续的常规锁。

此外,XACQUIRE 的 HLE 前缀是 F2XRELEASE 的 HLE 前缀是 F3,这只不过是老式的 REPNEREP 前缀,当不支持 HLE 的旧 CPU 与 lock-able 指令一起使用。这一切意味着使用 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 概念的有效性),欢迎您使用它。我也很乐意学习它。

【讨论】:

  • 是的,回答您自己的问题是允许的,实际上,当您解决自己的问题时,我们鼓励这样做。
  • Matt Damon 的评论对于答案来说有点啰嗦,但我想我们可以让它过去,因为你确实有一点:) 无论如何它可能来自这里 - pages.cs.wisc.edu/~rajwar/papers/micro01.pdf (虽然一般概念所谓的“事务性记忆”早于)
【解决方案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 的 Haswell,那么我们可以查看其他可能的中止事务的解释(当我们通过关键部分时,这将导致到达 int3非事务性并到达xtest)。

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

事务中的中断可能会导致偶尔中止,所以不要对发现一些事务中止感到震惊。只有当它们总是中止时,才意味着您正在执行事务无法处理的事情,或者 CPU 可能禁用了 HLE。

您用作锁的变量也必须满足某些属性

XACQUIRE manual entry:

锁定变量必须满足英特尔® 64 和 IA-32 架构软件开发人员手册第 1 卷第 16.3.3 节中描述的准则,才能成功进行省略,否则可能会发出 HLE 中止信号。

来自 SDM vol.1:

16.3.3 HLE 锁的要求

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

  • 以 XRELEASE 为前缀的指令必须将省略的锁的值恢复为 获取锁之前的值。这允许硬件 通过不将锁添加到写入集中来安全地消除锁。数据大小 和锁释放(XRELEASE前缀)指令的数据地址 必须与锁获取(以 XACQUIRE 为前缀)和锁相匹配 不得跨越缓存线边界。

  • 软件不应写入 带有任何指令的事务性 HLE 区域内的省略锁 除了 XRELEASE 前缀指令,否则可能会导致 事务中止。此外,递归锁(其中一个线程 多次获取同一个锁而没有首先释放 lock) 也可能导致事务中止。注意软件可以 观察关键内部的省略锁获取的结果 部分。这样的读操作会返回写到的值 锁。

处理器自动检测违反这些 指导方针,并安全地过渡到非交易执行 没有省略。由于 Intel TSX 在粒度上检测冲突 高速缓存行,写入与位于同一高速缓存行的数据 省略的锁可能会被其他逻辑检测为数据冲突 处理器省略了相同的锁

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

但除此之外,它还非常灵活。听起来硬件并不关心0 是否意味着锁定,0xdeadbeef(或指针值)是否意味着可用。

程序员有责任设计一个正确的锁定方案,如果发现锁定已被占用,则不存储先前的值,并在非事务性运行时保护临界区。

【讨论】:

  • 谢谢。我在自己的答案中更正了它。我也通读了 Intel 文档,我有些惊讶的是,这些 TSX 事务区域可以被许多事情中止,其中之一是中断。然后,我尝试为这些 TSX 锁想出任何现实生活中的应用程序,但找不到一个可以使用它们的应用程序。可能只是作为一种反调试机制(或使用 TF/trap 标志进行反单步执行)。除此之外,如果在发生中断的情况下可以中止(即失败)锁定对我有什么好处?我无法控制?
  • @MikeF:HLE 的用例是平均性能!就像我说的,它避免了实际上将包含锁的缓存线保持在修改状态,因此避免了来自共享相同锁但修改不同非冲突部分的多个内核的缓存线乒乓。大数据结构。 (例如树或哈希表)。 10000 次中的 1 笔交易被虚假中止几乎不会影响吞吐量。
  • @MikeF:如果您想在退回到非事务方法之前重试事务几次,请使用带有 xbegin / xend 的 RTM 而不是 HLE:您可以选择在 @ 上发生的事情987654340@ abort,因为它的作用类似于条件分支(请参阅此答案顶部的链接)。您可以轻松地保留重试计数器。
  • 另外关于您关于 TSX 在我的 Haswell CPU 上被分流的声明,我刚刚使用我的答案(不是原始帖子)中的代码中的更新锁进行了快速和肮脏的基准测试。一项测试: 5 个线程运行 5 秒,使用锁上的 xacquire/xrelease 提示,正如我在那里展示的那样。我的 testHLEWrite() 函数平均执行了大约 416k 次。第二个测试:如果我注释掉 xacquire/xrelease 提示,我将执行大约 405k 次。所以它毕竟一定有什么东西。
  • @MikeF:一个好的测试是两个线程都更新不冲突的内存,所以唯一实际共享的内存是锁本身。理论上,HLE 可以提供超过 2 倍的加速。或者把xor eax,eax/xtest/setz al/add [abort_counter], eax放到事务里面,看看非事务执行的次数是不是小于总执行次数。 (或者在需要时使用条件分支仅add dword [abort_counter], 1。)
猜你喜欢
  • 2018-03-11
  • 2011-03-21
  • 1970-01-01
  • 2020-11-25
  • 2012-06-19
  • 2010-09-17
  • 2022-08-10
  • 2010-11-21
  • 1970-01-01
相关资源
最近更新 更多