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

Posted

技术标签:

【中文标题】如何最大化 DDR3 内存数据传输率?【英文标题】:How to maximize DDR3 memory data transfer rate? 【发布时间】:2013-12-31 11:47:15 【问题描述】:

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

2014 年 3 月 20 日更新: 这种杀死 L1-L3 缓存的假设是错误的。内存控制器的硬件预取将分析数据访问模式,由于它是顺序的,因此将数据预取到 CPU 缓存中很容易。

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

我已经在 .NET 上用 C# 构建了一个测试作为初学者。尽管从内存分配的角度来看 .NET 并不理想,但我认为它对于这个测试是可行的(如果你不同意,请告诉我为什么)。测试是分配一个 int64 数组并用整数填充它。该数组应在内存中对齐数据。然后,我只需使用与机器上的内核一样多的线程循环该数组,并从数组中读取 int64 值并将其设置为测试类中的本地公共字段。由于结果字段是公开的,我应该避免编译器优化循环中的内容。此外,这可能是一个薄弱的假设,我认为结果会保留在寄存器中并且不会写入内存,直到它再次被覆盖。在每次读取数组中的元素之间,我在数组中使用 10、100 和 1000 的可变步长偏移量,以便无法在同一个缓存块(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 缓存并“希望”杀死预取机制。此外,我假设此循环中的大部分开销是内存获取操作,而不是其他。

此测试的硬件是: Intel Core I7-3930(规格:CPU breif、more detailed 和 really detailed spec)使用 32 GB DDR3-1600 内存。

开放式问题

    我的上述假设是否正确?

    有没有办法增加内存带宽的使用?例如,改为在 C/C++ 中进行,并在堆上分散内存分配,使所有四个内存通道能够可以使用。

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

非常有义务为此提供意见。我知道这是一个复杂的领域......

这里的所有代码都可以在https://github.com/Toby999/ThroughputTest 下载。请随时通过转发电子邮件与我联系 tobytemporary[at]gmail.com。

【问题讨论】:

好问题,如果它有一些代码,其中包含您尝试过的内容、预期内容以及实际得到的内容。 @Prashant:我认为预期/实际得到的已经存在(51.2GB/s vs. ~10GB/s)。 @Oli Charlesworth 啊,对。所以只是代码。 您将很难使用 .NET 实现全部内存带宽。通常这是为那些使用 SIMD 的人保留的,.NET 不提供任何访问权限。 我刚刚在 C++ 中实现了一个 SSE 实现,作为这个测试项目的一部分。但是无论平台如何,内存带宽利用率仍然很有趣/重要的是要了解更多信息。也许将相同的测试转换为 C++ 会带来更好的信息和更多的可能性。这是第二个问题。 :) 【参考方案1】:

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

不能保证 CLR 会为您提供任何能够真正对您的 RAM 进行基准测试的东西。我敢肯定,可能已经编写了软件来执行此操作。啊,是的,PassMark 有所作为:http://www.bandwidthtest.net/memory_bandwidth.htm

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

更新(2014 年 2 月 20 日): 我记得在 XNA 框架中看到一些代码在 C# 中进行了一些重载优化,这可能会为您提供您想要的东西。您是否尝试过使用“不安全”的代码和指针?

【讨论】:

感谢 Caleb 的意见。我将把它包括在我希望即将到来的进一步调查中。是的,处理器很好,但现在我意识到我需要一个基于 Haswell 的架构,以便能够尝试一些 AVX2 (SIMD) 内在方法。 :( 我的家用电脑中有一个 Haswell CPU。酷睿 i7 4770K。如果您愿意,我可以为您运行基准测试。 嗯。谢谢。那很好啊。如果值得升级,它可以给我输入。虽然这不是真正的基准,但更多的是我正在做的当前调查的全部规模。但是,如果您有兴趣,也许我可以通过邮件告诉您更多信息。我可以通过 tobytemporary[at]gmail.com 联系到我(我会回复我的真实地址)。 关于不安全的代码和指针。没有。还没有。我想我可以尝试一下,因为我很可能也会测试用 C++ 编写它。虽然我之前的经验是,与 C#/JIT 编译器相比,单纯的 C++ 编译器会产生巨大的差异。【参考方案2】:

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

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

这还测量突发读取速度,基于此,最大 DTR 为 46.9 GB/s。基准和源代码在:

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

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

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

【讨论】:

忘了说每个线程都有一个单独的数组分配为(X = 1到8): arrayX = (IDEF *)_aligned_malloc(memoryBytes[sizes-1], 16);对于 32 位或 64 位版本,IDEF 是 int 或 __int64 感谢您的意见。我很快就会给你的基准测试一个旋转,也许它已经足够满足我的需要了。很抱歉,我花了这么长时间才回到这条轨道上。希望很快我就能反思你的工作。【参考方案3】:

随着步长的增加,吞吐量下降可能是由于如果您不线性跨过内存,则内存预取无法正常工作。

提高速度的方法:

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

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

【讨论】:

感谢托马斯的意见。尤其是支持我的假设,即在 .NET 上是可能的。 :) 抱歉,我还没有时间评论或尝试您的建议,但我希望现在能够尽快完成。

以上是关于如何最大化 DDR3 内存数据传输率?的主要内容,如果未能解决你的问题,请参考以下文章

DDR3中DQS是啥意思?

ddr3内存频率怎么看

DDR3 内存计算详解

如何设置 WebRTC 数据通道最大比特率?

如何区分DDR1 DDR2 DDR3内存条?

内存类型UDIMMRDIMMLRDIMM