Производительность памяти Java VM. Является ли Array быстрее, чем считывает Array?

Я выполнил короткий тест на длинном массиве в java с довольно странными результатами. Кажется, что последовательные чтения со случайными записями быстрее - в полтора раза - чем случайные чтения с последовательной записью. Кто-нибудь подсказывает, почему?

Вот два метода, которые записывают массив из нескольких длин (запускается с -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 операций несколько раз, рестаты остаются довольно постоянными. Кажется (кажется!), Что перемещение памяти из случайных позиций в последовательные блоки происходит медленнее, чем перемещение памяти из последовательных позиций в случайные блоки, по крайней мере, с памятью этого размера и вышеописанным способом ее выполнения. и мне интересно, почему?

Ответ 1

В вашем тесте есть цифры, которые не соответствуют "Они имеют смысл?" контрольная работа. В такой ситуации вы всегда должны удвоить/утроить/четверть проверить свою методологию... ПЕРЕД обработкой чисел как истинным отражением реальности.

Написание надежных тестов сложно. И в случае с Java это особенно сложно, поскольку некоторые аспекты платформы Java могут вводить систематические искажения в ваши контрольные измерения... если вы специально не разрешаете/не компенсируете их.

Но правило "проверить свою методологию" применяется ко всем экспериментам... особенно тем, которые дают результаты, которые, похоже, не имеют смысла. (Как нейтрино, движущиеся быстрее света...)


Другое дело, что после того, как вы перепишете контрольный показатель для учета смешающих факторов, вы все равно можете увидеть неожиданные цифры. Проблема в том, что производительность таких тестов, скорее всего, будет чувствительна к таким вещам, как размер кэшей L1 и L2, размер строк кэша, относительная скорость разных уровней памяти... и их взаимодействие с точными последовательностями инструкции, которые эталонный результат производит в узких петлях.

Эти вещи сложны, трудно анализируются и могут давать противоречивое поведение. И неудивительно (для меня), что разные машины дают разные измеренные характеристики.

Таким образом, даже если цифры реальны, по-прежнему небезопасно делать какие-либо общие выводы о скорости чтения и записи из этого теста. Даже если вы ограничиваете их только своим ноутбуком.

Ответ 2

Таким образом, заголовок вопроса немного неверен. По-видимому, истина заключается в том, что в некоторых средах (например, my и 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");
    }

}
}

UPDATE

@tomcarchrae сделал тот же тест на Linux, что значительно отличалось от результатов. Ниже, первый столбец - это номера из моего теста, а второй - от Tom's:

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 

Ответ 3

Я считаю, что этот тест абсолютно бесполезен для вас. Существует множество параметров измерений, которые вы не описали, и то, как вы приближаетесь к этой проблеме, полностью не описано. Чтобы вообще сделать вывод о скорости реализации виртуальных машин, компьютеров, скорости ОЗУ, программного обеспечения, которое вы обрабатываете одновременно, типа объектов или простых вещей, которые вы копируете, и т.д., Вы должны узнать о методическом методе. Этот вопрос не подлежит обсуждению. Вы должны сузить, какие конкретные обстоятельства вы хотите знать о скорости.

В частности, вы не можете делать никаких заключений при использовании случайных чисел. Это значительно увеличивает проблему наилучшего, худшего или среднего случая сложности.

Пожалуйста, проверьте сложность алгоритмов, затем продолжите поиск, как сделать научные измерения производительности во время выполнения. Надеюсь, я немного помогу.

Этот первый ответ потрясающий и поможет вам понять. Как написать правильный микро-тест в Java?

С уважением,

Ответ 4

Ответ в предыдущих комментариях и сводится к эффектам доступа к памяти. Этот блог post описывает эффекты случайных чтений. Писания не страдают аналогичным образом.

Это не проблема Java (или даже проблема с языком), а реальность аппаратного обеспечения, на котором вы работаете (и общая реальность). Это не значит, что вы должны его игнорировать! Хотя ваш первоначальный тест, возможно, был испорчен, он по-прежнему попадает в реальную проблему для некоторого программного обеспечения, поэтому при этом это ценный урок.

Вывод заключается не в том, что чтение стоит дороже, чем пишет. Это то, что случайный доступ к памяти плохо обслуживается аппаратным обеспечением. В основном это связано с тем, что производительность LinkedList намного хуже, чем ArrayList для последовательного доступа, они имеют одинаковую вычислительную сложность, но доступ к массиву воспроизводится с помощью аппаратной силы, где нет связанного списка.

Ответ 5

Ваш эксперимент сломан, а не ваш ноутбук. См. Здесь для обсуждения и некоторые инструменты, которые помогут измерить производительность: Библиотека синхронизации производительности Java

Ниже приведены некоторые результаты, которые заключают контракт с вами. Также я изменил ваш код, чтобы быть более строгим и осторожным в том, как он измеряет.


Моя среда - Linux (Mint 14, основанный на Ubuntu 12.10) с использованием Sun JDK 1.6.0_38

С 1.5G кучи для большого примера, т.е. -Xmx1512


Примечание: интересно. Может быть, мой результат отличается от другого, потому что размер массива ниже. Будет повторно запускаться и обновляться.

Нет: результат аналогичен, в среднем нет большой разницы. Но интереснее отличие от короткого пробега, то есть 21092.5 (/10 = 2109.2) против 1645.2, что может быть медленнее из-за пейджинга в памяти.

результат с static long[] arr = new long[100000000]; (исходный размер массива)

Write: DescriptiveStatistics: n: 10 min: 20893.0 max: 22190.0 среднее значение: 21092.5 std dev: 390.90727800848117 медиана: 20953,5 асимметрия: 3.0092198852491543 kurtosis: 9.264808973899097

Читать: DescriptiveStatistics: n: 10 min: 21668.0 max: 22736.0 среднее значение: 21892.5 std dev: 318.31509546359877 медиана: 21766,5 асимметрия: 2.5034216544466124 куртоз: 6.560838306717343


Я не вижу огромной разницы в чтении и записи. Я изменил эксперимент, чтобы измерить 10 раз на немного меньшем массиве (результат - такое же количество чтения/записи). Не забудьте повторно запустить с большим размером массива или размером выборки.

Write: DescriptiveStatistics: n: 10 min: 1584.0 max: 1799.0 среднее значение: 1645.2 std dev: 59.51619760853156 медиана: 1634,5 асимметрия: 2.137918517160786 куртоз: 5.764166551997385

Читать: DescriptiveStatistics: n: 10 мин: 1568.0 макс: 2202.0 среднее значение: 1689.0 std dev: 186.93908693000031 медиана: 1623,0 асимметрия: 2.770215113912315 kurtosis: 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);


    }
}

Ответ 6

на моем ПК: (ns per r/w)

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

и seqReadRandWrite и seqWriteRandRead одинаково быстрые на 100 нс на цикл.

поэтому это может зависеть от аппаратного обеспечения. также настройки VM. попробуйте java -server и посмотрите, улучшится ли скорость.