Например:
-
a)
int [x][y][z]
vs
-
b)
int[x*y*z]
Первоначально предполагалось, что я поеду с a) для простоты
Я знаю, что Java не хранит массивы линейно в памяти, например, C. Но какие последствия имеет это для моей программы?
Например:
a) int [x][y][z]
vs
b) int[x*y*z]
Первоначально предполагалось, что я поеду с a) для простоты
Я знаю, что Java не хранит массивы линейно в памяти, например, C. Но какие последствия имеет это для моей программы?
Обычно самое лучшее, что нужно сделать при поиске пользователей для таких вопросов, - это посмотреть, как выбор компилируется в байт-код 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()
.
Вот результаты:
Линейная запись
Линейное чтение
Случайное чтение
Случайный один немного вводит в заблуждение, поскольку он генерирует 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, смешанный режим)
В текущих процессорах доступ к кешированной памяти в сотни раз медленнее, чем арифметика (см. эту презентацию и читайте Что каждый программист должен знать о памяти). Опция 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-мерный), потому что это проще для понимания, и есть меньше шансов сделать некоторую логическую ошибку (особенно, если вы используете его для моделирования трехмерного пространства)
Если вы выберете последний маршрут, вам придется выполнить арифметику для каждого доступа к одному массиву. Это будет болезненным и подверженным ошибкам (если вы не обернете его в класс, предоставляющий эту функциональность).
Я не считаю, что существует какая-либо (значительная) оптимизация при выборе вашего плоского массива (особенно учитывая, что арифметика взята для индексации в нее). Как всегда с оптимизацией, вам нужно будет выполнить некоторые измерения и определить, действительно ли это стоит.