订阅以接收新文章的通知:

你的计算机内存每7.8微秒会有一个小停顿

2018-11-23

5 分钟阅读时间
这篇博文也有 EnglishDeutschEspañolFrançais版本。
640px-2013_Transcend_TS512MLK72V6N--straightened-

现代 DDR3 SDRAM. 资料来源: BY-SA/4.0 by Kjerish

最近,在访问山景城的计算机历史博物馆的时,我深深地被过去的一些磁芯内存所吸引。

240px-KL_CoreMemory

资料来源: BY-SA/3.0 by Konstantin Lanzet

我发现我完全不知道这些东西曾经是如何运作的。我想知道环是否旋转(它们没有),以及为什么每个环都有三根电线穿过(我仍然不明白它们是如何工作的)。更重要的是,我意识到我对现代计算机内存——动态RAM的工作方式知之甚少!

cpumemory.8

资料来源: Ulrich Drepper's series about memory

我对动态RAM工作原理的结果之一特别感兴趣。如你所见,每一位数据都是通过RAM芯片内的一个小电容充电(或低电平)存储的。但随着时间的推移,这些电容逐渐失去电荷。为避免丢失存储的数据,必须定期刷新以将电荷(如果存在)恢复到其原始级别。此刷新过程包括读取每个位的值,然后将其写回。在此“刷新”期间,内存被占用,无法执行加载或存储位等正常操作。

这困扰了我很长一段时间,我想知道......我们有没有捕捉到软件中刷新带来的延迟?

动态RAM刷新入门

每个DIMM模块由“单元(cells)”和“行(rows)”,“列(columns)”,“面(sides)”和/或“模组(ranks)”组成。犹他大学的这个演讲诠释了命名法。您可以使用 decode-dimms命令检查计算机中的内存配置。举例如下:

$ decode-dimms
Size                                       4096 MB
Banks x Rows x Columns x Bits              8 x 15 x 10 x 64
Ranks                                      2

如今我们不需要进入整个DDR DIMM布局,我们只需要知道单个存储单元(存储一位信息)。具体来说,我们只关心储存单元如何执行刷新过程。

我们来看两份文档:

存储在动态存储器中的每个位都必须经过刷新,通常为每64ms一次(称为静态刷新)。这是一项相当耗时的操作。为避免每64ms出现一次大的停顿,这一过程被分为8192个较小的刷新操作。在每个操作中,计算机的存储器控​​制器向DRAM芯片发送刷新命令。在接收到指令后,芯片将刷新其单元总数的1/8192。计算一下: 64ms / 8192 = 7812.5 ns或7.81μs。

这意味着:

  • 每7812.5 ns有一次刷新命令,称为tREFI。

  • 芯片执行刷新和恢复需要一些时间,因此它可以再次执行正常的读写操作。这一次称为tRFC,为75ns或120ns(参考提到的Micron数据表)。

当存储器很热(> 85C)时,存储器单元电荷保持时间下降,静态刷新时间减半到32ms,tREFI下降到3906.25 ns。

典型的内存芯片用于刷新的时间占其运行时间的很大一部分——介于0.4%到5%之间。此外,存储器芯片功耗是典型计算机功耗中的一大部分,并且大部分功率都用于执行刷新。

在刷新动作的持续时间内,整个存储器芯片被封锁。这意味着每过7812ns,内存中的每个位都被封锁超过75ns。让我们来测量一下吧。

准备实验

    for (i = 0; i < ...; i++) {
		// Perform memory load. Any load instruction will do
		*(volatile int *) one_global_var;

		// Flush CPU cache. This is relatively slow
		_mm_clflush(one_global_var);

		// mfence is needed, otherwise sometimes the loop
		// takes very short time (25ns instead of like 160). I
		// blame reordering.
		asm volatile("mfence");

		// Measure and record time
		clock_gettime(CLOCK_MONOTONIC, &ts);
    }

要以纳秒级间隔测量操作,我们必须编写一个紧密循环(也许用C语言编写)。它看起来是这样的:

Github上提供了完整代码

代码非常简单直接:执行内存读取, CPU刷新缓存数据,测量时间。

(注:在第二个实验中,我尝试使用MOVNTDQA来执行数据加载,但这需要特殊的不可缓存的内存页面,这种页面需要root访问权限。)

# timestamp, loop duration
3101895733,     134
3101895865,     132
3101896002,     137
3101896134,     132
3101896268,     134
3101896403,     135
3101896762,     359
3101896901,     139
3101897038,     137

在我的计算机上,它生成的数据是这样的:

通常我每循环得到140ns,循环持续时间周期性地跳到360ns。有时我会得到超过3200ns的奇数读数。

不幸的是,数据结果非常杂乱,很难看出是否存在由刷新周期导致的明显延迟。

快速傅里叶变换

从某种程度上来说是可以计算到的。由于我们想要找到一个固定时间间隔事件,我们可以将数据输入FFT(快速傅立叶变换)算法,该算法能够揭示频率域的结果。

我并不是第一个想到这一点的人——以Rohamham成名的Mark Seaborn 在2015年就采用了这项技术。即使在看过了Mark的代码之后,利用快速傅里叶变换计算也比我预想的更难。但最后我完成了这一工作。

首先,我们需要准备数据。快速傅里叶计算要求对输入数据进行恒定的采样间隔采样。我们还需过滤数据以减少噪音。通过反复试验,我发现数据被预处理后的计算效果最好:

  • 忽略循环迭代的小值(小于平均值* 1.8),并用“0”读数替换。我们不希望将噪音输入算法。

  • 所有剩余的读数都替换为“1”,因为我们不需要考虑由某些噪声引起的延迟幅度。

  • 我设定的采样间隔为100ns,但实际设置为任意达到奈奎斯特值(双倍预期频率)的数字也能正常计算

  • 在数据输入FFT之前,我们需要使用固定的时序对数据进行采样。所有合理的采样方法都可以正常计算,我最终选择了基本的线性插值。

UNIT=100ns
A = [(timestamp, loop_duration),...] 
p = 1
for curr_ts in frange(fist_ts, last_ts, UNIT):
    while not(A[p-1].timestamp <= curr_ts < A[p].timestamp):
        p += 1
    v1 = 1 if avg*1.8 <= A[p-1].duration <= avg*4 else 0
    v2 = 1 if avg*1.8 <= A[p].duration <= avg*4 else 0
    v = estimate_linear(v1, v2, A[p-1].timestamp, curr_ts, A[p].timestamp)
    B.append( v )

该算法大致是:

[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...]

结果会在我的数据中产生相当单调的矢量,如下所示:

C = numpy.fft.fft(B)
C = numpy.abs(C)
F = numpy.fft.fftfreq(len(B)) * (1000000000/UNIT)

矢量很长,通常约为20万个数据点。通过这样的数据准备,我们准备将其输入FFT!

很简单吧?结果将会产生两个向量:

  • C中包含复杂的频率分量。我们对复杂的数字不感兴趣,我们可以通过调用 abs()函数将它们处理成单调的分量。

  • F中包含各频率采样率(采样点数)对应的矢量C中位置的标签。我们需要将其标准化为Hz单位——通过将其乘以输入矢量采样频率。

fft1a

结果可以绘制成下图:

fft2a

由于我们将延迟时间归一化,因此Y轴是无单位的。尽管如此,它仍然清楚地显示了某些固定频率间隔的峰值。让我们放大来看:

127850.0
127900.0
127950.0
255700.0
255750.0
255800.0
255850.0
255900.0
255950.0
383600.0
383650.0

我们可以清楚地看到前三个峰值。经过一些无意义的算术,包括将读数至少是平均值10倍的滤除,我们可以提取出基础频率:

计算:1000000000(ns / s)/ 127900(Hz)= 7818.6 ns

太棒了!第一个频率尖峰确实是我们正在寻找的,并且确实与刷新时间相关。

256kHz,384kHz,512kHz等的其他尖峰是我们128kHz基频的倍数(称为谐波)。这些是预期之内对像方波这样的东西进行快速傅里叶变换的副产物。

~/2018-11-memory-refresh$ make
gcc -msse4.1 -ggdb -O3 -Wall -Wextra measure-dram.c -o measure-dram
./measure-dram | python3 ./analyze-dram.py
[*] Verifying ASLR: main=0x555555554890 stack=0x7fffffefe2ec
[ ] Fun fact. I did 40663553 clock_gettime()'s per second
[*] Measuring MOVQ + CLFLUSH time. Running 131072 iterations.
[*] Writing out data
[*] Input data: min=117 avg=176 med=167 max=8172 items=131072
[*] Cutoff range 212-inf
[ ] 127849 items below cutoff, 0 items above cutoff, 3223 items non-zero
[*] Running FFT
[*] Top frequency above 2kHz below 250kHz has magnitude of 7716
[+] Top frequency spikes above 2kHZ are at:
127906Hz    7716
255813Hz    7947
383720Hz    7460
511626Hz    7141

为了便于实验,我们准备了此工具的命令行版本。您可以自己运行代码。这是在我的服务器上运行的示例:

我必须承认代码并不完全稳定。如果出现问题,请考虑禁用Turbo Boost,或进行CPU频率调整和性能调整。

最后

这项工作有两个主要的要点。

我们已经看到低电平数据很难分析,而且看起来很杂乱。但是我们可以采用良好而久经考验的快速傅里叶变换,而不是试图用肉眼看出来。在准备数据时我们需要按照一些主观的想法来处理。

最重要的是,我们发现通常情况下,从简单的用户空间流程中测量细微的硬件行为是可行的。这种想法促成了原始的Rowhammer漏洞的发现,被用于Meltdown / Spectre攻击,并在最近的ECC击败Rowhammer再生中再次出现

还有很多话题还可以聊。我们现在仅仅是了解到内存子系统内部工作的表面。我建议阅读以下内容来进一步了解:

最后,这里有一个关于旧磁芯存储器的好读物:

我们保护整个企业网络,帮助客户高效构建互联网规模的应用程序,加速任何网站或互联网应用程序抵御 DDoS 攻击,防止黑客入侵,并能协助您实现 Zero Trust 的过程

从任何设备访问 1.1.1.1,以开始使用我们的免费应用程序,帮助您更快、更安全地访问互联网。要进一步了解我们帮助构建更美好互联网的使命,请从这里开始。如果您正在寻找新的职业方向,请查看我们的空缺职位
开发人员Programming

在 X 上关注

Marek Majkowski|@majek04
Cloudflare|@cloudflare

相关帖子

2024年10月24日 13:00

Durable Objects aren't just durable, they're fast: a 10x speedup for Cloudflare Queues

Learn how we built Cloudflare Queues using our own Developer Platform and how it evolved to a geographically-distributed, horizontally-scalable architecture built on Durable Objects. Our new architecture supports over 10x more throughput and over 3x lower latency compared to the previous version....

2024年10月09日 13:00

Improving platform resilience at Cloudflare through automation

We realized that we need a way to automatically heal our platform from an operations perspective, and designed and built a workflow orchestration platform to provide these self-healing capabilities across our global network. We explore how this has helped us to reduce the impact on our customers due to operational issues, and the rich variety of similar problems it has empowered us to solve....