【问题标题】:Java VM memory performance - Are Array writes faster than Array reads?Java VM 内存性能 - 数组写入是否比数组读取快?
【发布时间】:2013-01-31 22:04:26
【问题描述】:

我在 java 中对一个长数组执行了一个简短的基准测试,结果很奇怪。似乎带有随机写入的顺序读取比带有顺序写入的随机读取要快 - 一半的时间。有人知道为什么吗??

这里有两种方法,在顺序读取时随机写入一些longs的数组(使用-Xmx2G左右运行),在随机写入时顺序读取:

import java.util.Random;


public class Scratch {
static Random random = new Random();
static long[] arr = new long[100000000];

static void seqReadRandWrite() {
    for(int i=0;i<arr.length;i++) {
        int at = random.nextInt(arr.length);
        arr[at] = arr[i];
    }
}

static void seqWriteRandRead() {
    for(int i=0;i<arr.length;i++) {
        int at = random.nextInt(arr.length);
        arr[i] = arr[at];
    }
}

public static void main(String[] args) throws Exception {

    seqWriteRandRead(); // warm up

    long nanos = System.nanoTime();
    seqReadRandWrite();
    System.out.println("Time: " + (System.nanoTime()-nanos) + "ns");

    nanos = System.nanoTime();
    seqWriteRandRead();
    System.out.println("Time: " + (System.nanoTime()-nanos) + "ns");

}
}

我笔记本上的结果是

时间:2774662168ns

时间:6059499068ns

这意味着随机写入的速度是读取速度的两倍……还是?我的笔记本坏了吗?

ps.:这并不声称是一个基准,尽管有关基准的链接建议中的大部分要点都已涵盖。即使我多次运行已经 200,000,000 次操作,结果仍然保持不变。似乎(似乎!)将内存从随机位置移动到顺序块比将内存从顺序位置移动到随机块慢,至少在这种大小的内存和上述执行方式的情况下。我想知道为什么?

【问题讨论】:

  • 你为什么不预热seqReadRandWrite() 方法呢?你确定一次调用就足以调用 JIT 编译器吗?
  • 那只是为了记忆..试试看,没区别
  • 我的 WAG 会优化顺序读取,因为这通常在编程中完成。这就是第一个算法更快的原因。
  • @MiserableVariable - 我只是出于好奇做了那个测试 - 顺序读/写以几乎相同的速度运行(如预期的那样)。但随机读取似乎比随机写入慢两倍。我确实相信这是内存缓存的影响。
  • '短基准' => 无效基准。

标签: java performance memory


【解决方案1】:

您的基准测试产生的数字未能通过“它们有意义吗?”测试。在这种情况下,您应该始终双重/三重/四重检查您的方法......在将数字视为现实的真实反映之前。

编写可靠的基准测试很难。而在 Java 的情况下,特别很难,因为 Java 平台的某些方面可能会在您的基准测量中引入系统性失真......除非您特别允许/补偿它们。

但“检查您的方法”规则适用于所有实验……尤其是那些产生似乎没有意义的结果的实验​​。 (就像中微子比光速更快......)


要注意的另一件事是,一旦您重写了基准以考虑混杂因素,您可能仍然看到意想不到的数字。这里的问题是,像这样的基准测试的性能可能对 L1 和 L2 缓存的大小、缓存行的大小、不同级别内存的相对速度......以及它们与确切序列的相互作用很敏感。基准在紧密循环中产生的指令。

这些事情很复杂,难以分析,并且会产生违反直觉的行为。 (对我来说)不同的机器给出不同的测量性能也就不足为奇了。

因此,即使这些数字是真实的,从这个基准测试中得出关于读取速度与写入速度的任何一般性结论仍然是不安全的。即使您将它们限制在您的笔记本电脑上也不会。

【讨论】:

  • 你说的很对,但我没有得出任何结论,而是提出了一些问题。
  • 是的 .. 但@Miserable Variable 似乎确实得出了结论,而且是可疑的!!
【解决方案2】:

我相信这个基准对你来说完全没用。有很多测量参数需要考虑,你没有描述,你解决这个问题的方式,完全没有描述。要对有关 VM、计算机、RAM 速度、您同时处理的软件、您复制的对象类型或简单内容等方面的实现速度做出任何结论,您必须了解一种有条不紊的方法。这个问题无法回答。您必须缩小您想了解速度的具体情况。

尤其是在使用随机数时,您无法得出任何结论。这大大增加了最佳、最坏或平均情况复杂性的问题。

请检查算法的复杂性,然后继续搜索如何进行科学的运行时性能测量。希望能帮到你。

第一个答案很棒,可以帮助您理解。 How do I write a correct micro-benchmark in Java?

最好的问候,

【讨论】:

    【解决方案3】:

    总而言之,问题标题略有错误。事实似乎是 在某些环境(例如我的和 OP 的)上,随机数组写入比随机数组读取更快。但请注意,对于其他一些人来说,情况并非如此。

    基于@JustinKSU 的comment,我将读取和写入分开,发现随机写入比随机读取更快。结果如下。这似乎是原因,这里的集体意见似乎是缓存上的读取未命中比写入未命中更昂贵(如果写入涉及任何缓存的话)。

    在生产中,虽然有其他活动,但热点可能会发挥作用。

    /cygdrive/c/Java/jdk1.7.0/bin/javac.exe Scratch.java && /cygdrive/c/Java/jdk1.7.0/bin/java Scratch
    Starting
    seqRead: 1273719725ns
    seqRead: 1243055271ns
    seqRead: 1245022497ns
    seqRead: 1242868527ns
    seqRead: 1241655611ns
    randRead: 6900959912ns
    randRead: 6965196004ns
    randRead: 7379623094ns
    randRead: 7020390995ns
    randRead: 6938997617ns
    seqWrite: 1266963940ns
    seqWrite: 1250599487ns
    seqWrite: 1246471685ns
    seqWrite: 1230472648ns
    seqWrite: 1246975416ns
    randWrite: 3898382192ns
    randWrite: 3897441137ns
    randWrite: 3939947844ns
    randWrite: 4207906037ns
    randWrite: 4103594207ns
    
    Compilation finished at Thu Jan 31 14:38:57
    

    我修改后的代码如下:

    import java.util.Random;
    
    
    public class Scratch {
    static Random random = new Random();
    static long[] arr = new long[100000000];
    
    static void seqReadRandWrite() {
        for(int i=0;i<arr.length;i++) {
            int at = Math.abs(random.nextInt() % arr.length);
            arr[at] = arr[i];
        }
    }
    
    static void seqWriteRandRead() {
        for(int i=0;i<arr.length;i++) {
            int at = Math.abs(random.nextInt() % arr.length);
            arr[i] = arr[at];
        }
    }
    
    
    static void seqRead() {
        int x = 0;
        for(int i=0;i<arr.length;i++) {
            int at = Math.abs(random.nextInt() % arr.length);
            x += arr[i];
        }
    }
    
    static void randRead() {
        int x = 0;
        for(int i=0;i<arr.length;i++) {
            int at = Math.abs(random.nextInt() % arr.length);
            x += arr[at];
        }
    }
    
    static void seqWrite() {
        for(int i=0;i<arr.length;i++) {
            int at = Math.abs(random.nextInt() % arr.length);
            arr[i] = at;
        }
    }
    
    static void randWrite() {
        for(int i=0;i<arr.length;i++) {
            int at = Math.abs(random.nextInt() % arr.length);
            arr[at] = at;
        }
    }
    
    
    public static void main(String[] args) throws Exception {
    
        // seqWriteRandRead(); // warm up
        System.out.println("Starting");
    
        long nanos =  -1;
        /*
        for (int i = 0; i < 5; i++) {       
            nanos = System.nanoTime();
            seqWriteRandRead();
            System.out.println("WriteRandRead Time: " + (System.nanoTime()-nanos) + "ns");
    
            nanos = System.nanoTime();
            seqReadRandWrite();
            System.out.println("ReadRandWrite Time: " + (System.nanoTime()-nanos) + "ns");
        }
        */
    
        for (int i = 0; i < 5; i++) {       
            nanos = System.nanoTime();
            seqRead();
            System.out.println("seqRead: " + (System.nanoTime()-nanos) + "ns");
        }
    
        for (int i = 0; i < 5; i++) {       
            nanos = System.nanoTime();
            randRead();
            System.out.println("randRead: " + (System.nanoTime()-nanos) + "ns");
        }
    
    
        for (int i = 0; i < 5; i++) {       
            nanos = System.nanoTime();
            seqWrite();
            System.out.println("seqWrite: " + (System.nanoTime()-nanos) + "ns");
        }
    
        for (int i = 0; i < 5; i++) {       
            nanos = System.nanoTime();
            randWrite();
            System.out.println("randWrite: " + (System.nanoTime()-nanos) + "ns");
        }
    
    }
    }
    

    更新

    @tomcarchrae 在 Linux 上进行了相同的测试,结果明显不同。下面,第一列是我测试的数字,第二列是汤姆的:

    seqRead:   1273719725ns   2810487542ns  
    seqRead:   1243055271ns   2780504580ns  
    seqRead:   1245022497ns   2746663894ns  
    seqRead:   1242868527ns   2746094469ns  
    seqRead:   1241655611ns   2763107970ns  
    randRead:  6900959912ns   23093543703ns 
    randRead:  6965196004ns   22458781637ns 
    randRead:  7379623094ns   24421031646ns 
    randRead:  7020390995ns   25880250599ns 
    randRead:  6938997617ns   26873823898ns 
    seqWrite:  1266963940ns   4226886722ns  
    seqWrite:  1250599487ns   4537680602ns  
    seqWrite:  1246471685ns   3880372295ns  
    seqWrite:  1230472648ns   4160499114ns  
    seqWrite:  1246975416ns   4008607447ns  
    randWrite: 3898382192ns   25985349107ns 
    randWrite: 3897441137ns   22259835568ns 
    randWrite: 3939947844ns   22556465742ns 
    randWrite: 4207906037ns   22143959163ns 
    randWrite: 4103594207ns   21737397817ns 
    

    【讨论】:

    • 咯咯,你比我早了大约 15 秒才发布这个。我很确定这些结果是由于主缓存上的读取未命中比写入未命中更昂贵。将其添加到您的答案中,并为您 +1。
    • 我编辑了我的答案。值得互联网积分,我的意思是知识:)
    • 似乎与硬件有关。或者可能依赖于 JVM。我也可以复制(使用最新的 Java 7),但有些不能。
    • 从我的机器(Linux)的结果开始seqRead:2810487542ns seqRead:2780504580ns seqRead:2746663894ns seqRead:2746094469ns seqRead:2763107970ns randRead:23093543703ns randRead:22458781637ns randRead:24421031646ns randRead:25880250599ns randRead:26873823898ns seqWrite:4226886722ns seqWrite :4537680602NS SEQWRITE:3880372295NS SEQWRITE:4008607447S RANDWRITE:25985349107NS RANDWRITE:22556465742S RANDWRITE:22143959163NS RANDWRITE:21737397817NS SPAN>
    • 我将把这个添加到我的答案中进行比较。
    【解决方案4】:

    答案在以前的 cmets 中,归结为内存访问模式的影响。这个博客post 涵盖了随机读取的影响。写入不会受到类似的影响。

    这不是 Java 问题(或者实际上是任何语言问题),而是您所运行的硬件的现实(以及常见的现实)。这并不意味着您应该忽略它!虽然您最初的基准测试可能存在缺陷,但它仍然对某些软件造成了真正的问题,因此这是一个宝贵的教训。

    结论并不是读取比写入更昂贵。硬件不能很好地满足随机内存访问的需求。这就是为什么 LinkedList 在顺序访问方面的性能比 ArrayList 差得多的原因,它们的计算复杂度相同,但数组访问发挥了链表所没有的硬件强度。

    【讨论】:

      【解决方案5】:

      我的电脑上的结果:(ns per r/w)

      seq read :     1.4 
      rnd read :   10x.x   
      seq write:     3.3 
      rnd write:   10x.x
      

      seqReadRandWrite 和 seqWriteRandRead 在每个循环 100ns 时同样快。

      所以这可能取决于硬件。还有VM设置。试试java -server 看看速度有没有提高。

      【讨论】:

        【解决方案6】:

        你的实验坏了,不是你的笔记本电脑。在这里查看讨论和一些有助于衡量性能的工具:Java performance timing library

        以下是一些与您的结果相关的结果。我还修改了您的代码,使其在测量方式上更加严格和谨慎。


        我的环境是使用 Sun JDK 1.6.0_38 的 Linux(基于 Ubuntu 12.10 的 Mint 14)

        以 1.5G 的堆为例,即 -Xmx1512


        注意:有趣。可能是我的结果不同,因为下面的数组大小不同。将重新运行和更新。

        不:结果相似,平均值没有太大差异。但有趣的是与短期的差异,即 21092.5 (/10 = 2109.2) 与 1645.2 可能会因为内存分页而变慢。

        static long[] arr = new long[100000000]; 的结果(有问题的原始数组大小)

        写入:DescriptiveStatistics:n:10 分钟:20893.0 最大值:22190.0 均值: 21092.5 标准开发:390.90727800848117 中值:20953.5 偏度:3.0092198852491543 峰度:9.264808973899097

        阅读:DescriptiveStatistics:n:10 分钟:21668.0 最大值:22736.0 平均值: 21892.5 标准开发:318.31509546359877 中值:21766.5 偏度:2.5034216544466124 峰度:6.560838306717343


        我没有看到读取与写入的巨大差异。我将实验更改为在一个稍小的阵列上测量 10 次(结果是相同的读/写次数)。随意使用更大的数组或样本大小重新运行。

        写入:DescriptiveStatistics:n:10 分钟:1584.0 最大:1799.0 平均值: 1645.2 标准开发:59.51619760853156 中值:1634.5 偏度:2.137918517160786 峰度:5.764166551997385

        读取:DescriptiveStatistics:n:10 分钟:1568.0 最大值:2202.0 平均值: 1689.0 标准开发:186.93908693000031 中值:1623.0 偏度:2.770215113912315 峰度:8.12245132320571

        这是您的代码的修改版本,其中包含更多示例:

        import java.util.Random;
        
        import org.apache.commons.lang.time.StopWatch;
        import org.apache.commons.math.stat.descriptive.DescriptiveStatistics;
        
        public class Test {
            static Random random = new Random();
        //  static long[] arr = new long[100000000];
            static long[] arr = new long[10000000];
        
            static void seqReadRandWrite() {
                for (int i = 0; i < arr.length; i++) {
                    int at = Math.abs(random.nextInt()) % arr.length;
                    arr[at] = arr[i];
                }
            }
        
            static void seqWriteRandRead() {
                for (int i = 0; i < arr.length; i++) {
                    int at = Math.abs(random.nextInt()) % arr.length;
                    arr[i] = arr[at];
                }
            }
        
            public static void main(String[] args) throws Exception {
        
                StopWatch timer = new StopWatch();
                int count = 10;
        
                // warm up
                for (int i=0; i<3; i++){
                    seqReadRandWrite();
                }
                DescriptiveStatistics write = new DescriptiveStatistics();
                for (int i=0; i<count; i++){
                    timer.reset();
                    timer.start();
                    seqReadRandWrite();
                    timer.stop();
                    write.addValue(timer.getTime());
                }
                System.out.println("Write: " + write);
        
                // warm up
                for (int i=0; i<3; i++){
                    seqWriteRandRead(); 
                }
                DescriptiveStatistics read = new DescriptiveStatistics();
                for (int i=0; i<count; i++){
                    timer.reset();
                    timer.start();
                    seqWriteRandRead();
                    timer.stop();
                    read.addValue(timer.getTime());
                }
        
                System.out.println("Read: " + read);
        
        
            }
        }
        

        【讨论】:

        • 在我的笔记本电脑上,您的程序(没有任何更改)测量写入速度是读取速度的两倍多。也许每个人的笔记本电脑都坏了,但你的:)
        • 汤姆:你的环境是什么?你能试试我的答案中的测试吗?
        • 有趣——你用的是什么操作系统?
        • @MiserableVariable - 当然,我会将结果作为评论发布在您的帖子中
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-04-21
        • 1970-01-01
        • 2014-12-28
        • 1970-01-01
        • 2016-01-05
        相关资源
        最近更新 更多