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

时间:2013-12-12 20:59:12

标签: c# .net memory memory-management ram

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

2014年2月20日更新: 这种杀死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;
}

这是实际测试:

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();

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

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);

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

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

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

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

此测试的硬件是: 英特尔酷睿I7-3930(规格:CPU breifmore detailedreally detailed spec)使用总共32 GB的DDR3-1600内存。

打开问题

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

  2. 有没有办法增加内存带宽的使用?例如,用C / C ++代替,并在堆上扩展内存分配,使所有四个内存通道都可以使用。

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

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

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

3 个答案:

答案 0 :(得分:5)

如果不增加步长,则吞吐量的减少很可能是因为如果你没有线性地通过内存,内存预取将不再正常工作。

您可以采取哪些措施来提高速度:

  • 测试速度将由循环本身人为地限制,占用CPU周期。正如Roy所说,通过展开循环可以实现更快的速度。
  • 您应该摆脱边界检查(使用“未选中”)
  • 使用Parallel.For而不是使用Thread.Start,并将您开始的每个线程固定在一个单独的核心上(使用此处的代码:Set thread processor affinity in Microsoft .Net
  • 确保所有线程同时启动,因此您不会测量任何straggler(您可以通过在所有线程运行和旋转时将Interlock.Exchange的内存地址旋转到新值来执行此操作)
  • 在NUMA计算机上(例如2 Socket Modern Xeon),您可能需要采取额外步骤在NUMA节点上分配线程将存在的内存。为此,您需要PInvoke VirtualAllocExNuma
  • 说到内存分配,使用大页面应该提供另一个提升

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

答案 1 :(得分:2)

报告的RAM结果(128 MB)用于i7 3820上的bus8thread64.exe基准测试,最大内存带宽为51.2 GB / s,从1个线程的15.6,2个线程的28.1到8个线程的38.7。代码是:

   void inc1word(IDEF data1[], IDEF ands[], int n)
    {
       int i, j;

       for(j=0; j<passes1; j++)
       {
           for (i=0; i<wordsToTest; i=i+64)
           {
               ands[n] = ands[n] & data1[i   ] & data1[i+1 ] & data1[i+2 ] & data1[i+3 ]
                                 & data1[i+4 ] & data1[i+5 ] & data1[i+6 ] & data1[i+7 ]
                                 & data1[i+8 ] & data1[i+9 ] & data1[i+10] & data1[i+11]
                                 & data1[i+12] & data1[i+13] & data1[i+14] & data1[i+15]
                                 & data1[i+16] & data1[i+17] & data1[i+18] & data1[i+19]
                                 & data1[i+20] & data1[i+21] & data1[i+22] & data1[i+23]
                                 & data1[i+24] & data1[i+25] & data1[i+26] & data1[i+27]
                                 & data1[i+28] & data1[i+29] & data1[i+30] & data1[i+31]
                                 & data1[i+32] & data1[i+33] & data1[i+34] & data1[i+35]
                                 & data1[i+36] & data1[i+37] & data1[i+38] & data1[i+39]
                                 & data1[i+40] & data1[i+41] & data1[i+42] & data1[i+43]
                                 & data1[i+44] & data1[i+45] & data1[i+46] & data1[i+47]
                                 & data1[i+48] & data1[i+49] & data1[i+50] & data1[i+51]
                                 & data1[i+52] & data1[i+53] & data1[i+54] & data1[i+55]
                                 & data1[i+56] & data1[i+57] & data1[i+58] & data1[i+59]
                                 & data1[i+60] & data1[i+61] & data1[i+62] & data1[i+63];
           }
        }
    }

这也测量突发读取速度,其中max DTR基于此,为46.9 GB / s。基准和源代码位于:

http://www.roylongbottom.org.uk/quadcore.zip

对于使用L3缓存的有趣速度的结果如下:

http://www.roylongbottom.org.uk/busspd2k%20results.htm#anchor8Thread

答案 2 :(得分:1)

C / C ++会提供更准确的内存性能指标,因为.NET有时可以通过内存处理做一些奇怪的事情,并且由于它不使用编译器内在函数或SIMD指令而无法为您提供准确的图像。

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

这可能是你最好的选择,因为制作基准测试软件几乎就是他们所做的一切。 另外,很好的处理器顺便说一下,我的机器中有一个;)

更新(2014年2月20日): 我记得在XNA Framework中看到一些代码在C#中做了一些重要的优化,可能会给你你想要的东西。你尝试过使用“不安全”的代码和指针吗?