程序超过了理论内存传输速率

18

我有一台配备Intel Core 2 Duo 2.4GHz CPU和2x4Gb DDR3模块1066MHz的笔记本电脑。

我希望这个内存可以以1067 MiB/sec的速度运行,并且只要存在两个通道,最大速度就是2134 MiB/sec(在操作系统内存调度器允许的情况下)。

我制作了一个小的Java应用程序来测试:

private static final int size = 256 * 1024 * 1024; // 256 Mb
private static final byte[] storage = new byte[size];

private static final int s = 1024; // 1Kb
private static final int duration = 10; // 10sec

public static void main(String[] args) {
    long start = System.currentTimeMillis();
    Random rnd = new Random();
    byte[] buf1 = new byte[s];
    rnd.nextBytes(buf1);
    long count = 0;
    while (System.currentTimeMillis() - start < duration * 1000) {
        long begin = (long) (rnd.nextDouble() * (size - s));
        System.arraycopy(buf1, 0, storage, (int) begin, s);
        ++count;
    }
    double totalSeconds = (System.currentTimeMillis() - start) / 1000.0;
    double speed = count * s / totalSeconds / 1024 / 1024;
    System.out.println(count * s + " bytes transferred in " + totalSeconds + " secs (" + speed + " MiB/sec)");

    byte[] buf2 = new byte[s];
    count = 0;
    start = System.currentTimeMillis();
    while (System.currentTimeMillis() - start < duration * 1000) {
        long begin = (long) (rnd.nextDouble() * (size - s));
        System.arraycopy(storage, (int) begin, buf2, 0, s);
        Arrays.fill(buf2, (byte) 0);
        ++count;
    }
    totalSeconds = (System.currentTimeMillis() - start) / 1000.0;
    speed = count * s / totalSeconds / 1024 / 1024;
    System.out.println(count * s + " bytes transferred in " + totalSeconds + " secs (" + speed + " MiB/sec)");
}

我原本期望结果在2134 MiB/sec以下,但是我得到了以下的结果:

17530212352 bytes transferred in 10.0 secs (1671.811328125 MiB/sec)
31237926912 bytes transferred in 10.0 secs (2979.080859375 MiB/sec)

速度几乎达到3 GiB/sec,这是怎么可能的呢?

DDR3模块照片


6
你忘记了CPU缓存,有L1、L2甚至L3缓存...仅仅因为你随意地访问内存并不意味着你偶尔不会在缓存中得到一个命中。 - Marc B
1
DDRx内存通常也是64位宽。仅仅因为它运行在1066MHz并不意味着它的传输速率是1字节/赫兹... - Marc B
3
你对DDR频率的解释是错误的。请查看https://en.wikipedia.org/wiki/DDR_SDRAM 通常来说,你的数字相当低。 - Jason Hu
1
@HuStmpHrrr 你认为在我的情况下哪个是理论上的极限? - Anthony
2
对于初学者来说,你在执行代码的同时还进行复制操作,这使得你的测量从一开始就不准确。 - fge
显示剩余8条评论
4个回答

22

这里有几个要素。

首先:DDR3内存传输速率的公式是这样的

memory clock rate
× 4  (for bus clock multiplier)
× 2  (for data rate)
× 64 (number of bits transferred)
/ 8  (number of bits/byte)
=    memory clock rate × 64 (in MB/s)

对于DDR3-1066(时钟频率为133⅓ MHz),我们得到单通道的理论内存带宽为8533⅓ MB/s8138.02083333... MiB/s,双通道的理论内存带宽为17066⅔ MB/s16276.0416666... MiB/s

第二点:传输一个大块数据比传输许多小块数据更快。

第三点:测试忽略了可能发生的缓存效应。

第四点:如果进行时间测量,应使用System.nanoTime()。这种方法更精确。

这是测试程序的重写版本1

import java.util.Random;

public class Main {

  public static void main(String... args) {
    final int SIZE = 1024 * 1024 * 1024;
    final int RUNS = 8;
    final int THREADS = 8;
    final int TSIZE = SIZE / THREADS;
    assert (TSIZE * THREADS == THREADS) : "TSIZE must divide SIZE!";
    byte[] src = new byte[SIZE];
    byte[] dest = new byte[SIZE];
    Random r = new Random();
    long timeNano = 0;

    Thread[] threads = new Thread[THREADS];
    for (int i = 0; i < RUNS; ++i) {
      System.out.print("Initializing src... ");
      for (int idx = 0; idx < SIZE; ++idx) {
        src[idx] = ((byte) r.nextInt(256));
      }
      System.out.println("done!");
      System.out.print("Starting test... ");
      for (int idx = 0; idx < THREADS; ++idx) {
        final int from = TSIZE * idx;
        threads[idx]
            = new Thread(() -> {
          System.arraycopy(src, from, dest, 0, TSIZE);
        });
      }
      long start = System.nanoTime();
      for (int idx = 0; idx < THREADS; ++idx) {
        threads[idx].start();
      }
      for (int idx = 0; idx < THREADS; ++idx) {
        try {
          threads[idx].join();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
      timeNano += System.nanoTime() - start;
      System.out.println("done!");
    }
    double timeSecs = timeNano / 1_000_000_000d;

    System.out.println("Transfered " + (long) SIZE * RUNS
        + " bytes in " + timeSecs + " seconds.");

    System.out.println("-> "
        + ((long) SIZE * RUNS / timeSecs / 1024 / 1024 / 1024)
        + " GiB/s");
  }
}

这种方式尽可能减少了“其他计算”,而基本只有通过System.arraycopy(...)进行内存复制速率的测量。然而,该算法仍可能存在缓存方面的问题。

对于我的系统(双通道DDR3-1600),我得到的速率大约为6 GiB/s,而理论极限大约为25 GiB/s(包括双通道)。

正如Nick Mertin所指出的那样,JVM会引入一些开销。因此,无法达到理论上的速率极限是可以预料的。


1 顺带提一句:要运行该程序,必须给JVM更多的堆空间。在我的情况下,4096 MB就足够了。


2
@Antonio,你为什么要减小“SIZE”?为了接近理论极限,块的大小应该尽可能大。最好给JVM更多的堆空间(java -Xmx4096m ...)而不是减小块的大小。 - Turing85
我将存储大小设置为1GB,持续时间为30秒。结果变得几乎相同 - 1.42GiB / sec。CPU不是瓶颈(使用率为60%)。 - Anthony
2
@Antonio,你的理论极限(包括双通道)大约是16 GiB/s,因此没有超过理论极限。请记住,我的基准测试需要将所有内容传输两次(从内存到 CPU,再从 CPU 返回到内存)。因此,我的程序实际上只能传输2.84 GiB/s。然而,你的基准测试可能会从缓存中获得很大的优势(你的源数组仅有1KB大小,因此可能完全被缓存)。因此,基本上两个基准测试显示了相同的性能。 - Turing85
@Turing85 重要的是要记住,因为它在JVM中运行,而JVM又在操作系统上运行,所以存在额外的CPU开销,这与Java代码本身无关。此外,操作系统可能会防止单个进程使用大量的CPU,因此即使它只使用了60%,CPU仍然可能成为瓶颈。 - Nick Mertin
@MagikM18 至少对于我的系统来说,操作系统没有阻止程序高CPU负载(我能够将负载推高到90%以上)。也许这个评论是针对Antonio的? - Turing85
显示剩余6条评论

8
你的测试方法在很多方面都设计不当,以及你对RAM评级的解释也是如此。
让我们从评级开始;自SDRam问世以来,制造商将模块命名为它们的总线规格 - 即总线时钟频率和突发传输速率。那是最好的情况,在实际使用中无法持续保持。
标签省略的参数是实际的访问时间(又称延迟)和总周期时间(又称预充电时间)。这些可以通过查看“时间”规格(2-3-3等)来计算得出。请查阅一篇详细解释这些内容的文章。实际上,CPU通常不会传输单个字节,而是整个高速缓存行(例如每8字节8个条目=64字节)。
你的测试代码设计不良,因为你正在使用相对较小的块进行随机访问,而且未对实际数据边界进行对齐。这种随机访问还会在MMU中引起频繁的页面缺失(学习一下TLB是什么/做什么)。因此,你测量到的是各种不同系统方面的混合结果。

2
实际上,我的目标是在所需的内存块不在缓存中时测试速度。让我看看时间表,然后再回来。谢谢你的答案。 - Anthony
1
我添加了一张DDR模块的照片。据我所见,没有时间参数。 - Anthony
1
经过一些谷歌搜索,我发现CL=7。那么你如何计算极限呢? - Anthony
2
@Antonio,请从你的问题评论中提到的维基条目开始,CL代表列访问延迟,这只是其中一个参数。模块“知道”它们的时序,也就是主板的BIOS读取参数并调整其时序以匹配RAM。有一些工具可以显示这些值。关于如何从时序中实际计算出有用的东西的“公式”,请认真阅读维基百科,可能还要补充DRAM访问基础知识。这是一个相对复杂和广泛的话题。我自己也不知道每个细节。 - Durandal

1
这可能与硬件配置有关。根据提供的信息,有两个内核和两个内存模块,但内存通道数量不清楚。虽然我从未见过笔记本电脑规模的测试,在更大的系统上,DIMM在内存通道中的配置可以对内存传输速率产生重大影响。
例如,在现代服务器上,可以使用每通道一个DIMM(ODPC)或每通道两个DIMM(TDPC)内存配置。每个物理CPU可以将多个内存通道分配给该CPU上的物理内核,并且每个服务器可能潜在地具有多个物理CPU(通常是2-4个现代服务器)。
内存如何在这些通道、内核和CPU/芯片之间分配,可以根据正在测量的内容对内存性能产生重大影响。例如,在TDPC配置中,如果TDPC系统中的内存量等于或大于ODPC配置中的内存量,则采用ODPC配置的系统的传输时间(以每秒传输次数或兆传输次数/秒(MT/s)表示)将显着提高。
基于这些知识,可以想象使用ODPC和每个内核一个通道的方式设置的笔记本电脑理论上可以实现所描述的性能。

说了这么多,其实有很多预先打包的内存分析工具可以进行非侵入式运行,以获取有关系统内存性能的信息。Memtest是一个非常强大、易于理解和文档完善的测试内存工具。它可以下载到某种可启动磁盘(USB、DVD、软盘等)上,安全地用于对系统内存进行压力测试,而不会损坏或干扰操作系统。它也包含在某些Linux发行版的安装DVD和救援DVD/镜像中。这是一个非常强大的工具,我在许多情况下都使用它来调试和分析内存性能,尤其是在服务器上。


1
在维基百科上有一个传输速率表。这款笔记本电脑的规格如下:
  • 模块类型:PC3-8500 DDR3 SDRAM
  • 芯片类型:DDR3-1066
  • 内存时钟:133兆赫
  • 总线速度:1.066GT/s
  • 传输速率(比特/秒):64千兆位每秒
  • 传输速率(十进制字节/秒):8千兆字节每秒

这是每个通道的单个DDR3模块。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接