Java: многомерный массив против одномерного

Например:

  • a) int [x][y][z]

    vs

  • b) int[x*y*z]

Первоначально предполагалось, что я поеду с a) для простоты

Я знаю, что Java не хранит массивы линейно в памяти, например, C. Но какие последствия имеет это для моей программы?

Ответ 1

Обычно самое лучшее, что нужно сделать при поиске пользователей для таких вопросов, - это посмотреть, как выбор компилируется в байт-код JVM:

multi = new int[50][50];
single = new int[2500];

Это переведено на:

BIPUSH 50
BIPUSH 50
MULTIANEWARRAY int[][] 2
ASTORE 1
SIPUSH 2500
NEWARRAY T_INT
ASTORE 2

Итак, как вы можете видеть, JVM уже знает, что мы говорим о многомерном массиве.

Сохраняя это дальше:

for (int i = 0; i < 50; ++i)
    for (int j = 0; j < 50; ++j)
    {
        multi[i][j] = 20;
        single[i*50+j] = 20;
    }

Это переведено (пропуская циклы) в:

ALOAD 1: multi
ILOAD 3: i
AALOAD
ILOAD 4: j
BIPUSH 20
IASTORE

ALOAD 2: single
ILOAD 3: i
BIPUSH 50
IMUL
ILOAD 4: j
IADD
BIPUSH 20
IASTORE

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

Я не думаю, что производительность будет такой проблемой.

EDIT:

Я сделал несколько простых тестов, чтобы увидеть, что здесь происходит. Я решил попробовать разные примеры: линейное считывание, линейная запись, и произвольный доступ. Время выражается в миллисекундах (и рассчитывается с использованием System.nanoTime(). Вот результаты:

Линейная запись

  • Размер: 100x100 (10000) Multi: 5.786591 Single: 6.131748
  • Размер: 200x200 (40000) Multi: 1.216366 Single: 0.782041
  • Размер: 500x500 (250000) Multi: 7.177029 Single: 3.667017
  • Размер: 1000x1000 (1000000) Multi: 30.508131 Single: 18.064592
  • Размер: 2000x2000 (4000000) Multi: 185.3548 Single: 155.590313
  • Размер: 5000x5000 (25000000) Multi: 955.5299 Single: 923.264417
  • Размер: 10000x10000 (100000000) Multi: 4084.798753 Single: 4015.448829

Линейное чтение

  • Размер: 100x100 (10000) Multi: 5.241338 Single: 5.135957
  • Размер: 200x200 (40000) Multi: 0.080209 Single: 0.044371
  • Размер: 500x500 (250000) Multi: 0.088742 Single: 0.084476
  • Размер: 1000x1000 (1000000) Multi: 0.232095 Однолокальный: 0,167671
  • Размер: 2000x2000 (4000000) Multi: 0.481683 Одиночный: 0,33321
  • Размер: 5000x5000 (25000000) Multi: 1.222339 Однолокальный: 0,828118 Размер: 10000x10000 (100000000) Multi: 2.496302 Single: 1.650691

Случайное чтение

  • Размер: 100x100 (10000) Multi: 22.317393 Single: 8.546134
  • Размер: 200x200 (40000) Multi: 32.287669 Single: 11.022383
  • Размер: 500x500 (250000) Multi: 189.542751 Single: 68.181343
  • Размер: 1000x1000 (1000000) Multi: 1124.78609 Single: 272.235584
  • Размер: 2000x2000 (4000000) Multi: 6814.477101 Single: 1091.998395
  • Размер: 5000x5000 (25000000) Multi: 50051.306239 Single: 7028.422262

Случайный один немного вводит в заблуждение, поскольку он генерирует 2 случайных числа для многомерного массива, а только один для одномерных (и PNRG могут потреблять некоторый процессор).

Помните, что я попытался позволить JIT работать путем бенчмаркинга только после 20-го запуска того же цикла. Для полноты моей виртуальной машины Java является следующее:

версия java "1.6.0_17" Java (TM) SE Runtime Environment (сборка 1.6.0_17-b04) Java HotSpot (TM) 64-разрядная серверная VM (сборка 14.3-b01, смешанный режим)

Ответ 2

В текущих процессорах доступ к кешированной памяти в сотни раз медленнее, чем арифметика (см. эту презентацию и читайте Что каждый программист должен знать о памяти). Опция a) приведет к примерно 3 просмотрам памяти, тогда как опция b) приведет к примерно 1 просмотру памяти. Кроме того, алгоритмы предварительной выборки CPU могут не работать. Таким образом, опция b) может быть быстрее в некоторых ситуациях (это горячая точка, и массив не вписывается в кеш процессора). Насколько быстрее? - это будет зависеть от приложения.

Лично я бы сначала использовал параметр a), потому что это приведет к более простому коду. Если профилировщик показывает, что доступ к массиву является узким местом, я бы преобразовал его в параметр b), так что существует пара вспомогательных методов для чтения и записи значений массива (таким образом, беспорядочный код будет ограничен этими двумя методы).

Я сделал сравнительный тест для сравнения 3-мерных массивов int (столбец "Multi" ) с эквивалентными 1-мерными массивами int (столбец "Single" ). Код здесь и содержит тесты здесь. Я запускал его на 64-разрядных jdk1.6.0_18, Windows 7 x64, Core 2 Quad Q6600 @3.0 ГГц, 4 ГБ DDR2, используя параметры JVM -server -Xmx3G -verbose:gc -XX:+PrintCompilation (я удалил вывод отладки из следующих результатов). Результаты:

Out of 20 repeats, the minimum time in milliseconds is reported.

Array dimensions: 100x100x100 (1000000)
            Multi   Single
Seq Write   1       1
Seq Read    1       1
Random Read 99      90    (of which generating random numbers 59 ms)

Array dimensions: 200x200x200 (8000000)
            Multi   Single
Seq Write   14      13
Seq Read    11      8
Random Read 1482    1239    (of which generating random numbers 474 ms)

Array dimensions: 300x300x300 (27000000)
            Multi   Single
Seq Write   53      46
Seq Read    34      24
Random Read 5915    4418    (of which generating random numbers 1557 ms)

Array dimensions: 400x400x400 (64000000)
            Multi   Single
Seq Write   123     111
Seq Read    71      55
Random Read 16326   11144    (of which generating random numbers 3693 ms)

Это показывает, что 1-мерный массив будет быстрее. Хотя различия настолько малы, что для 99% приложений это не будет примечательно.

Я также сделал несколько измерений для оценки накладных расходов при генерации случайных чисел в тестовом методе Random Read, заменив preventOptimizingAway += array.get(x, y, z); на preventOptimizingAway += x * y * z; и добавил измерения в таблицу результатов выше. Генерация случайных чисел занимает 1/3 или менее от общего времени теста Random Read, поэтому доступ к памяти доминирует над эталоном, как ожидалось. Было бы интересно повторить этот тест с массивами из 4 и более измерений. Вероятно, это сделало бы разницу в скорости больше, потому что верхние уровни многомерных массивов будут вписываться в кеш процессора, и только другие уровни потребуют поиска в памяти.

Ответ 3

Используйте первый вариант (3-мерный), потому что это проще для понимания, и есть меньше шансов сделать некоторую логическую ошибку (особенно, если вы используете его для моделирования трехмерного пространства)

Ответ 4

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

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