在前两篇中,分别介绍了通过数据分片和避免伪共享两种方法来增加程序性能。现在来看一个实际的代码案例。
在Windows内核中,有一些带CacheAware的函数,包括RundownProtection和PushLock系列的函数。顾名思义,这些函数是进行了Cache友好的优化,其中包括了对多核进行数据分片以及CacheLine大小对齐的内存块。
我们来看一下这些函数是如何工作的吧,这里以RundownProtection为例,这个锁可以被翻译为销毁保护吧,具体用途可以参考MSDN,这里不细介绍。
首先看Cache友好的数据结构:
typedef struct _EX_RUNDOWN_REF_CACHE_AWARE {
//这个结构就包含了缓存友好的结构
PEX_RUNDOWN_REF RunRefs;
//如果申请的内存不和Cacheline对齐,那么这个就保存不对齐的内存地址
PVOID PoolToFree;
//每一个RundownRef结构的大小,MP下就是Cacheline大小
ULONG RunRefSize;
//总共有多少个RundownRef结构,MP下就是CPU Group0中Core的个数
ULONG Number;
} EX_RUNDOWN_REF_CACHE_AWARE, *PEX_RUNDOWN_REF_CACHE_AWARE;
有一个函数专门用于初始化这个结构,看看是如何初始化这个结构的:
NTKERNELAPI
PEX_RUNDOWN_REF_CACHE_AWARE
ExAllocateCacheAwareRundownProtection(
__in POOL_TYPE PoolType,
__in ULONG PoolTag
)
{
PEX_RUNDOWN_REF_CACHE_AWARE RunRefCacheAware;
PEX_RUNDOWN_REF RunRefPool;
PEX_RUNDOWN_REF CurrentRunRef;
ULONG PaddedSize;
ULONG Index;
PAGED_CODE();
//首先申请EX_RUNDOWN_REF_CACHE_AWARE的内存,这个是没有什么需要处理的
RunRefCacheAware = ExAllocatePoolWithTag (PoolType,
sizeof( EX_RUNDOWN_REF_CACHE_AWARE ),
PoolTag
);
if (NULL != RunRefCacheAware) {
//获得CPU的个数,XP是不支持CPU组的
RunRefCacheAware->Number = KeNumberProcessors;
//计算每个RundownRef结构的大小,对于MP,这个是Cacheline大小,否则就是原始大小
if (RunRefCacheAware->Number > 1) {
PaddedSize = KeGetRecommendedSharedDataAlignment ();
ASSERT ((PaddedSize & (PaddedSize - 1)) == 0);
} else {
PaddedSize = sizeof (EX_RUNDOWN_REF);
}
ASSERT (sizeof (EX_RUNDOWN_REF) <= PaddedSize);
RunRefCacheAware->RunRefSize = PaddedSize;
//尝试申请一块刚刚好的内存
RunRefPool = ExAllocatePoolWithTag (PoolType,
PaddedSize * RunRefCacheAware->Number,
PoolTag);
if (RunRefPool == NULL) {
ExFreePool (RunRefCacheAware);
return NULL;
}
//如果是MP,但内存起始地址不和Cacheline大小对齐
if ((RunRefCacheAware->Number > 1) &&
!EXP_IS_ALIGNED_ON_BOUNDARY (RunRefPool, PaddedSize)) {
//先把这块内存释放掉
ExFreePool (RunRefPool);
//在增加一块Cacheline的大小
RunRefPool = ExAllocatePoolWithTag (PoolType,
PaddedSize * RunRefCacheAware->Number + PaddedSize,
PoolTag);
if (RunRefPool == NULL) {
ExFreePool (RunRefCacheAware);
return NULL;
}
//获得对齐后的RundownRef数组的起始地址
CurrentRunRef = EXP_ALIGN_UP_PTR_ON_BOUNDARY (RunRefPool, PaddedSize);
} else {
//否则申请内存的起始地址就是RundownRef数组的起始地址
CurrentRunRef = RunRefPool;
}
//保存申请的内存地址和对齐后的地址
RunRefCacheAware->PoolToFree = RunRefPool;
RunRefCacheAware->RunRefs = CurrentRunRef;
//对每块CPU初始化其对应的RundownRef结构
for (Index = 0; Index < RunRefCacheAware->Number; Index++) {
CurrentRunRef = EXP_GET_PROCESSOR_RUNDOWN_REF (RunRefCacheAware, Index);
ExInitializeRundownProtection (CurrentRunRef);
}
}
return RunRefCacheAware;
}
在这个函数中,会至少申请CachelineSize*ProcessorNumber大小的内存,即对每个CPU单独申请一块内存,而内存的大小至少是缓存行大小,这样就形成了一个不会产生缓存同步的RundownRef数组。如果这个内存无法和缓存行大小对齐,那么就重新申请内存,这次在加上一个缓存行大小的空间,这样就可以让RundownRef数组的起始地址在缓存行大小上对齐。最后分别对数组中每个RundownRef结构进行初始化。
然后每次在获取这个锁的时候,就可以以当前运行的CPU的序号作为下标来操作对应的数组元素。
NTKERNELAPI
BOOLEAN
FASTCALL
ExAcquireRundownProtectionCacheAware (
__inout PEX_RUNDOWN_REF_CACHE_AWARE RunRefCacheAware
)
{
//请求当前CPU对应数组元素的Rundwon锁
return ExAcquireRundownProtection (EXP_GET_CURRENT_RUNDOWN_REF (RunRefCacheAware));
}
PEX_RUNDOWN_REF
FORCEINLINE
EXP_GET_CURRENT_RUNDOWN_REF(
IN PEX_RUNDOWN_REF_CACHE_AWARE RunRefCacheAware
)
{
//计算数组元素的字节偏移
return ((PEX_RUNDOWN_REF) (((PUCHAR) RunRefCacheAware->RunRefs) +
(KeGetCurrentProcessorNumber() % RunRefCacheAware->Number) * RunRefCacheAware->RunRefSize));
}
每次申请一个RundownRef锁时,首先获取当前的CPUNumber,然后计算出需要操作的数组元素,最后只需要操作对应的元素即可。
如果没有分片,获取锁时将不得不进行加锁操作,而大小如果不满足缓存行大小,那么两个core同时获取锁时,也将会产生伪共享问题。