HLE(即Hardware Lock Elision,硬件锁省略)以及RTM(即Restricted Transactional Memory,受限的事务性存储器)是Intel在x86微架构中所引入的两条指令集系统,它们均属于TSX(Transactional Synchronization Extensions,事务性同步扩展)指令集扩展。这套指令集扩展往往用于包含原子操作代码的临界区(Critical Section),通过将原子锁进行省略而使得多核多线程并行对此临界区的操作能进行提速。
下图比较详细地介绍了这两套指令集的执行逻辑以及使用方式。
这个图取自于Muttik等人的一份paper,各位可以参考此文:CREATING A SPIDER GOAT: USING TRANSACTIONAL MEMORY SUPPORT FOR SECURITY。
下面我们将分别基于RTM和HLE来给出一些评测。各位要注意的是,尽管TSX在Intel Haswell微架构上就引入了,但那时候的实现尚不成熟,而且还有较严重的BUG。直到Skylake时代,部分处理器能正常时候该特性了。不过为了安全可靠起见,笔者建议各位在Kabylake或在此之后的处理器上运行以下代码。
由于之前关于“幽灵”、“熔断”等CPU高危漏洞的爆出,因此像更注重安全性的Mac已经把TSX全面屏蔽了,包括汇编器也直接不支持 XACQUIRE/XRELEASE 指令。因此笔者这里只能在装有Windows 10的联想笔记本上通过Visual Studio 2017 Community Edition进行测试。使用的处理器为Core i5 8250U。不过可惜的是,这款CPU没能支持TSX指令集扩展,我们只能稍作演示。
在Windows 10上如何通过Visual Studio 2017创建一个普通的C语言控制台项目,请参考这篇博文。我们这里就使用最基本的MSVC编译器即可。
下面先给出用于测试的test.asm汇编文件内容:
; test.asm
.code
; void cpu_pause(void)
cpu_pause proc public
pause
ret
cpu_pause endp
; void NormalAddTest(int *pArray, int count)
NormalAddTest proc public
; pArray => rcx
; count => edx
mov eax, 1
NormalAddTest_LOOP:
add [rcx], eax
add rcx, 4
sub edx, 1
jne NormalAddTest_LOOP
ret
NormalAddTest endp
; void AtomicAddTest(int *pArray, int count)
AtomicAddTest proc public
; pArray => rcx
; count => edx
mov eax, 1
AtomicAddTest_LOOP:
lock add [rcx], eax
add rcx, 4
sub edx, 1
jne AtomicAddTest_LOOP
ret
AtomicAddTest endp
; void HLEAtomicAddTest(int *pArray, int count)
HLEAtomicAddTest proc public
; pArray => rcx
; count => edx
mov eax, 1
HLEAtomicAddTest_LOOP:
xacquire lock add [rcx], eax
add rcx, 4
sub edx, 1
jne HLEAtomicAddTest_LOOP
ret
HLEAtomicAddTest endp
; void FlagSetTest(volatile int *pFlag, int *pValue, int repeatCount)
FlagSetTest proc public
; pFlag => rcx
; pArray => rdx
; count => r8d
mov eax, 1
xor r9d, r9d
jmp FlagSetTest_LOOP
FlagSetTest_FAIL_HANDLER:
pause
FlagSetTest_LOOP:
xacquire lock bts [rcx], r9d
jc FlagSetTest_FAIL_HANDLER
add [rdx], eax
xrelease mov [rcx], r9d
sub r8d, 1
jne FlagSetTest_LOOP
ret
FlagSetTest endp
END
下面给出main.c源文件内容:
// 你好,世界
#include <Windows.h>
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#define TEST_LOOP_COUNT 10
extern void cpu_pause(void);
extern void NormalAddTest(int *pArray, int count);
extern void AtomicAddTest(int *pArray, int count);
extern void HLEAtomicAddTest(int *pArray, int count);
extern void FlagSetTest(volatile int *pFlag, int *pValue, int repeatCount);
static volatile bool sIsComplete = false;
struct MyFuncCallParam
{
void(*pFunc)(int*, int);
int *pArray;
int count;
};
static DWORD WINAPI TestThreadProc(LPVOID lpParam)
{
struct MyFuncCallParam *callInfo = (struct MyFuncCallParam*)lpParam;
void(*const pTestFunc)(int*, int) = callInfo->pFunc;
int* const pBuffer = callInfo->pArray;
const int count = callInfo->count;
for (int i = 0; i < 100; i++)
pTestFunc(pBuffer, count);
sIsComplete = true;
return 0;
}
static void TestAddFunction(void(*pTestFunc)(int*, int), int *pArray, int count)
{
sIsComplete = false;
struct MyFuncCallParam param = { pTestFunc, pArray, count };
HANDLE hThread = CreateThread(NULL, 0, TestThreadProc, ¶m, 0, NULL);
for (int i = 0; i < 100; i++)
pTestFunc(pArray, count);
while (!sIsComplete)
cpu_pause();
CloseHandle(hThread);
}
int main(int argc, const char * argv[])
{
// 数据初始化
const int count = 1024 * 1024;
int *data = malloc(count * sizeof(data[0]));
for (int i = 0; i < count; i++)
data[i] = i;
// 数据完整性测试
TestAddFunction(AtomicAddTest, data, count);
int errCount = 0;
for (int i = 0; i < count; i++)
{
if (data[i] != i + 200)
errCount++;
}
// 数据处理性能测试
DWORD tBegin[TEST_LOOP_COUNT], tEnd[TEST_LOOP_COUNT];
for (int i = 0; i < TEST_LOOP_COUNT; i++)
{
tBegin[i] = GetTickCount();
TestAddFunction(AtomicAddTest, data, count);
tEnd[i] = GetTickCount();
}
DWORD timeSpent = tEnd[0] - tBegin[0];
for (int i = 1; i < TEST_LOOP_COUNT; i++)
{
const DWORD ts = tEnd[0] - tBegin[0];
if (timeSpent > ts)
timeSpent = ts;
}
printf("Time spent: %ums\n", timeSpent);
printf("Error count: %d\n", errCount);
volatile int flag = 0;
data[0] = 0;
FlagSetTest(&flag, data, 1);
free(data);
}
各位在编译构建之后,最好在命令行下执行,这样能保证应用程序的执行不受其他剖析器等进程的影响。