【发布时间】:2019-06-23 08:17:18
【问题描述】:
我有一个多核系统,有 4 个内核,每个内核都有私有 L1 和 L2 缓存以及共享 LLC。缓存具有包容性属性,这意味着高级缓存是低级缓存的超集。可以直接刷LLC上的块还是要先过低层?
我正在尝试了解刷新+重新加载和刷新+刷新缓存侧通道攻击。
【问题讨论】:
标签: caching intel flush cpu-cache instructions
我有一个多核系统,有 4 个内核,每个内核都有私有 L1 和 L2 缓存以及共享 LLC。缓存具有包容性属性,这意味着高级缓存是低级缓存的超集。可以直接刷LLC上的块还是要先过低层?
我正在尝试了解刷新+重新加载和刷新+刷新缓存侧通道攻击。
【问题讨论】:
标签: caching intel flush cpu-cache instructions
clflush 在架构上是必需的/保证会从所有级别的缓存中逐出该行,这对于将数据提交到非易失性 DIMM 非常有用。 (例如,电池支持的 DRAM 或 3D XPoint)。
The wording in the manual 似乎很清楚:
从缓存一致性域中缓存层次结构的每一级失效...如果该缓存行包含缓存层次结构中任何级别的修改数据,则该数据将被写回内存
我认为如果多个核心有一条处于共享状态的线路,则一个核心上的clflush / clflushopt 必须将其从所有核心的私有缓存中逐出。 (这无论如何都会作为从包含 L3 缓存中逐出的一部分发生,但 Skylake-X 更改为 NINE(非包含非独占)L3 缓存。)
可以直接刷LLC上的块还是要先通过低层?
不清楚你在问什么。您是在问是否可以要求 CPU 仅从 L3 刷新块 而不会干扰 L1/L2?您已经知道 L3 在大多数 Intel CPU 上都具有包容性,因此最终效果与clflush 相同。核心要与 L3 对话,它们必须经过自己的 L1d 和 L2。
clflush 如果数据仅存在于 L3 中而不存在于执行它的核心的私有 L1d 或 L2 中,则仍然有效。它不是像预取那样的“提示”,也不是本地唯一的东西。
在未来的 Silvermont 系列 CPU 中,将有一个 cldemote 指令让您将一个块刷新到 LLC,但不能一直刷新到 DRAM。 (而且这只是一个提示,因此如果回写路径忙于驱逐为需求负载腾出空间,它不会强制 CPU 服从它。)
【讨论】:
cldemote 不必打扰其他 CPU 内核;如果它在其他内核的 L1d 中很热,它就会停留在那里。不像 clflushopt 必须占用中间带宽核心,甚至可能是套接字间互连,以确保它不会丢失副本。)
CLFLUSH 不可能总是从每个缓存级别逐出。我刚刚写了一个小程序(C++17),在我的机器(3990X)上刷新 cachlines 总是低于 5ns:
#include <iostream>
#include <chrono>
#include <cstring>
#include <vector>
#include <charconv>
#include <sstream>
#include <cmath>
#if defined(_MSC_VER)
#include <intrin.h>
#elif defined(__GNUC__)
#include <x86intrin.h>
#endif
using namespace std;
using namespace chrono;
size_t parseSize( char const *str );
string blockSizeStr( size_t blockSize );
int main( int argc, char **argv )
{
static size_t const DEFAULT_MAX_BLOCK_SIZE = (size_t)512 * 1024;
size_t blockSize = argc < 2 ? DEFAULT_MAX_BLOCK_SIZE : parseSize( argv[1] );
if( blockSize == -1 )
return EXIT_FAILURE;
blockSize = blockSize >= 4096 ? blockSize : 4096;
vector<char> block( blockSize );
size_t size = 4096;
static size_t const ITERATIONS_64K = 100;
do
{
uint64_t avg = 0;
size = size <= blockSize ? size : blockSize;
size_t iterations = (size_t)((double)0x10000 / size * ITERATIONS_64K + 0.5);
iterations += (size_t)!iterations;
for( size_t it = 0; it != iterations; ++it )
{
// make cachlines to get modified for sure by
// modifying to a differnt value each iteration
for( size_t i = 0; i != size; ++i )
block[i] = (i + it) % 0x100;
auto start = high_resolution_clock::now();
for( char *p = &*block.begin(), *end = p + size; p < end; p += 64 )
_mm_clflush( p );
avg += duration_cast<nanoseconds>( high_resolution_clock::now() - start ).count();
}
double nsPerCl = ((double)(int64_t)avg / iterations) / (double)(ptrdiff_t)(size / 64);
cout << blockSizeStr( size ) << " " << nsPerCl << "ns" << endl;
} while( (size *= 2) <= blockSize );
}
size_t parseSize( char const *str )
{
double dSize;
from_chars_result fcr = from_chars( str, str + strlen( str ), dSize, chars_format::general );
if( fcr.ec != errc() )
return -1;
if( !*(str = fcr.ptr) || str[1] )
return -1;
static const
struct suffix_t
{
char suffix;
size_t mult;
} suffixes[]
{
{ 'k', 1024 },
{ 'm', (size_t)1024 * 1024 },
{ 'g', (size_t)1024 * 1024 * 1024 }
};
char cSuf = tolower( *str );
for( suffix_t const &suf : suffixes )
if( suf.suffix == cSuf )
{
dSize = trunc( dSize * (ptrdiff_t)suf.mult );
if( dSize < 1.0 || dSize >= (double)numeric_limits<ptrdiff_t>::max() )
return -1;
return (ptrdiff_t)dSize;
}
return -1;
}
string blockSizeStr( size_t blockSize )
{
ostringstream oss;
double dSize = (double)(ptrdiff_t)blockSize;
if( dSize < 1024.0 )
oss << blockSize;
else if( dSize < 1024.0 * 1024.0 )
oss << dSize / 1024.0 << "kB";
else if( blockSize < (size_t)1024 * 1024 * 1024 )
oss << dSize / (1024.0 * 1024.0) << "MB";
else
oss << (double)blockSize / (1024.0 * 1024.0 * 1024.0) << "GB";
return oss.str();
}
没有任何 DDR 内存可以处理刷新低于 5ns 的单个缓存线。
【讨论】: