如何最大化DDR3内存数据传输速率?

Tob*_*999 13 .net c# memory ram memory-management

我试图通过测试来测量DDR3内存数据传输速率.根据CPU规格.最大理论带宽为51.2 GB/s.这应该是四个通道的组合带宽,意味着12.8 GB /通道.然而,这是一个理论上的限制,我很好奇如何进一步提高这篇文章的实际限制.在下面描述的测试场景中,我实现了~14 GB/s的数据传输速率,我认为在杀死CPU L1,L2和L3高速缓存的大部分吞吐量增强时可能是近似的.

更新2014年第20/3期: 这种杀死L1-L3缓存的假设是错误的.内存控制器的硬件预取将分析数据访问模式,并且由于它是顺序的,因此它将具有将数据预取到CPU高速缓存中的简单任务.

具体问题在底部,但主要是我感兴趣的a)对导致该结果的假设的验证,以及b)是否有更好的方法在.NET中测量内存带宽.

我在.NET上用C#构建了一个测试作为入门者.虽然.NET从内存分配的角度来看并不理想,但我认为这对于这个测试是可行的(如果你不同意,请告诉我,为什么).测试是分配一个int64数组并用整数填充它.该数组应该在内存中对齐数据.然后,我使用与机器上的核心一样多的线程循环此数组,并从数组中读取int64值并将其设置为测试类中的本地公共字段.由于结果字段是公共的,我应该避免编译器优化循环中的东西.此外,这可能是一个弱假设,我认为结果保留在寄存器中,直到它再次被写入才被写入存储器.在每次读取数组中的元素之间,我在数组中使用10,100和1000的变量Step偏移量,以便无法在同一缓存块(64字节)中获取许多引用.

从数组中读取Int64应该意味着读取8个字节,然后读取实际值8个字节.由于数据是从64字节高速缓存行的内存中提取的,因此每次在循环中从RAM中读取的每个64字节应该对应于读取数据不在任何CPU高速缓存中.

以下是我初始化数据数组的方法:

_longArray = new long[Config.NbrOfCores][];
for (int threadId = 0; threadId < Config.NbrOfCores; threadId++)
{
    _longArray[threadId] = new long[Config.NmbrOfRequests];
    for (int i = 0; i < Config.NmbrOfRequests; i++)
        _longArray[threadId][i] = i;
}
Run Code Online (Sandbox Code Playgroud)

这是实际的测试:

GC.Collect();
timer.Start();
Parallel.For(0, Config.NbrOfCores, threadId =>
{
    var intArrayPerThread = _longArray[threadId];
    for (int redo = 0; redo < Config.NbrOfRedos; redo++)
        for (long i = 0; i < Config.NmbrOfRequests; i += Config.Step) 
            _result = intArrayPerThread[i];                        
});
timer.Stop();
Run Code Online (Sandbox Code Playgroud)

由于数据摘要对于结果非常重要,我也提供此信息(如果您信任我,可以跳过...)

var timetakenInSec = timer.ElapsedMilliseconds / (double)1000;
long totalNbrOfRequest = Config.NmbrOfRequests / Config.Step * Config.NbrOfCores*Config.NbrOfRedos; 
var throughput_ReqPerSec = totalNbrOfRequest / timetakenInSec;
var throughput_BytesPerSec = throughput_ReqPerSec * byteSizePerRequest;
var timeTakenPerRequestInNanos = Math.Round(1e6 * timer.ElapsedMilliseconds / totalNbrOfRequest, 1);
var resultMReqPerSec = Math.Round(throughput_ReqPerSec/1e6, 1);
var resultGBPerSec = Math.Round(throughput_BytesPerSec/1073741824, 1);
var resultTimeTakenInSec = Math.Round(timetakenInSec, 1);
Run Code Online (Sandbox Code Playgroud)

忽略给你实际的输出渲染代码我得到以下结果:

Step   10: Throughput:   570,3 MReq/s and         34 GB/s (64B),   Timetaken/request:      1,8 ns/req, Total TimeTaken: 12624 msec, Total Requests:   7 200 000 000
Step  100: Throughput:   462,0 MReq/s and       27,5 GB/s (64B),   Timetaken/request:      2,2 ns/req, Total TimeTaken: 15586 msec, Total Requests:   7 200 000 000
Step 1000: Throughput:   236,6 MReq/s and       14,1 GB/s (64B),   Timetaken/request:      4,2 ns/req, Total TimeTaken: 30430 msec, Total Requests:   7 200 000 000
Run Code Online (Sandbox Code Playgroud)

使用12个线程而不是6个(因为CPU是超线程的)我获得了几乎相同的吞吐量(我认为如预期的那样):32.9/30.2/15.5 GB/s.

可以看出,吞吐量随着步长的增加而下降,我认为这是正常的.部分我认为这是由于12 MB L3缓存会导致更多缓存未命中,部分原因可能是内存控制器预取机制在读取距离太远时无法正常工作.我进一步认为,步骤1000的结果是与实际实际内存速度最接近的结果,因为它应该杀死大多数CPU缓存并"希望"杀死预取机制.此外,我假设此循环中的大部分开销是内存提取操作而不是其他内容.

这项测试的硬件是: Intel Core I7-3930(规格:CPU breif , 更详细,详细的规格),总共使用32 GB的DDR3-1600内存.

打开问题

  1. 我在上面的假设中是否正确?

  2. 有没有办法增加内存带宽的使用?例如,通过在C/C++中执行此操作,并在堆上更多地分配内存分配,从而可以使用所有四个内存通道.

  3. 有没有更好的方法来衡量内存数据传输?

很有必要就此提出意见.我知道这是一个复杂的领域......

此处的所有代码均可从https://github.com/Toby999/ThroughputTest下载.请随时通过转发邮件联系我tobytemporary [at] gmail.com.

Tho*_*ser 5

增加步长时吞吐量的降低可能是因为如果不通过内存线性地跨越内存预取,则内存预取不再有效.

你可以做些什么来提高速度:

  • 测试速度将由循环本身人为地限制,占用CPU周期.正如Roy所说,通过展开循环可以实现更快的速度.
  • 你应该摆脱边界检查("未选中")
  • 而不是使用__CODE__,使用__CODE__和固定每个线程,而是从一个单独的核心开始(使用此处的代码:在Microsoft .Net中设置线程处理器关联)
  • 确保所有线程同时启动,因此您不会测量任何落后者(您可以通过__CODE__在所有线程运行和旋转时将内存地址旋转到新值来执行此操作)
  • 在NUMA机器上(例如2 Socket Modern Xeon),您可能需要采取额外的步骤在线程将存在的NUMA节点上分配内存.为此,您需要PInvoke__CODE__
  • 说到内存分配,使用大页面应该提供另一个提升

虽然.NET不是用于此类测试的最简单的框架,但它可以诱导它做你想做的事情.


Cal*_*ett 1

C/C++ 将提供更准确的内存性能指标,因为 .NET 有时会在内存处理方面做一些奇怪的事情,并且不会为您提供准确的图片,因为它不使用编译器内部函数或 SIMD 指令。

无法保证 CLR 会给您提供任何能够真正对 RAM 进行基准测试的功能。我确信可能已经编写了软件来执行此操作。啊,是的,PassMark 做了一些东西:http://www.bandwidthtest.net/memory_bandwidth.htm

这可能是你最好的选择,因为制作基准测试软件几乎就是他们所做的全部。另外,顺便说一句,处理器不错,我的一台机器上也有同样的处理器;)

更新(2/20/2014):我记得在 XNA 框架中看到过一些代码,它们在 C# 中进行了一些重型优化,这可能会给您带来您想要的结果。您是否尝试过使用“不安全”代码和指针?