Подтвердить что ты не робот

Почему Java не имеет истинных многомерных массивов?

Версия TL; DR для тех, кто не хочет фона, представляет собой следующий конкретный вопрос:

Вопрос

Почему Java не реализует истинные многомерные массивы? Есть ли твердая техническая причина? Что мне здесь не хватает?

Фон

Java имеет многомерные массивы на уровне синтаксиса, поскольку можно объявить

int[][] arr = new int[10][10];

но кажется, что это действительно не то, что можно было ожидать. Вместо того, чтобы JVM выделял непрерывный блок ОЗУ, достаточно большой для хранения 100 int s, он выводится как массив массивов int s: поэтому каждый уровень является непрерывным блоком ОЗУ, но вещь как в целом нет. Таким образом, доступ к arr[i][j] довольно медленный: JVM должен

  • найдите int[], сохраненный в arr[i];
  • укажите это, чтобы найти int, сохраненный в arr[i][j].

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

Почему Java делает это

На одном уровне нетрудно понять, почему это невозможно оптимизировать для простого масштабирования и добавления, даже если все они были выделены в одном фиксированном блоке. Проблема в том, что arr[3] является ссылкой, и может быть изменена. Поэтому, хотя массивы имеют фиксированный размер, мы могли бы легко написать

arr[3] = new int[11];

и теперь масштаб и добавление завинчиваются, потому что этот слой вырос. Во время работы вам нужно знать, все ли все те же размеры, что и раньше. Кроме того, конечно, это будет затем распределено в другом месте в ОЗУ (это должно быть, поскольку оно больше, чем то, что он заменяет), поэтому он даже не в нужном месте для масштабирования и добавления.

Какая проблема с этим

Мне кажется, что это не идеально, и это по двум причинам.

Во-первых, он медленный. Тест, который я запускал с помощью этих методов для суммирования содержимого одномерного или многомерного массива, занимал почти в два раза (714 секунд против 371 секунды) для многомерного случая (a int[1000000] и int[100][100][100] соответственно, заполненных случайным int, запустите 1000000 раз с теплым кешем).

public static long sumSingle(int[] arr) {
    long total = 0;
    for (int i=0; i<arr.length; i++)
        total+=arr[i];
    return total;
}

public static long sumMulti(int[][][] arr) {
    long total = 0;
    for (int i=0; i<arr.length; i++)
        for (int j=0; j<arr[0].length; j++)
            for (int k=0; k<arr[0][0].length; k++)
                total+=arr[i][j][k];
    return total;
}   

Во-вторых, поскольку он медленный, он тем самым поощряет неясное кодирование. Если вы столкнулись с чем-то критичным по производительности, которое было бы естественно сделано с помощью многомерного массива, у вас есть стимул писать его как плоский массив, даже если это делает неестественным и трудночитаемым. У вас остался неприятный выбор: скрытый код или медленный код.

Что можно сделать с этим

Мне кажется, что основная проблема может быть легко решена. Единственная причина, как мы видели ранее, в том, что она не может быть оптимизирована, заключается в том, что структура может измениться. Но у Java уже есть механизм для того, чтобы сделать ссылки неизменяемыми: объявите их как final.

Теперь, просто объявив его

final int[][] arr = new int[10][10];

недостаточно хорош, потому что только arr, то есть final здесь: arr[3] все еще нет и может быть изменен, поэтому структура может по-прежнему меняться. Но если бы у нас был способ объявить вещи так, чтобы он был final повсюду, за исключением нижнего уровня, где хранятся значения int, тогда у нас будет целая неизменяемая структура, и все это можно было бы выделить как один блок и индексируется с помощью масштабирования и добавления.

Как бы это выглядело синтаксически, я не уверен (я не разработчик языка). Может быть,

final int[final][] arr = new int[10][10];

хотя, по общему признанию, это выглядит немного странно. Это означало бы: final в верхнем слое; final на следующем уровне; а не final в нижнем слое (иначе сами значения int были бы неизменными).

Конечность во всей полноте позволяла компилятору JIT оптимизировать это, чтобы дать производительность производительности для одномерного массива, который затем уберет искушение закодировать этот способ, чтобы обойти медленность многомерных массивов.

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

Вопрос

Итак, почему Java не реализует истинные многомерные массивы? Есть ли твердая техническая причина? Что мне здесь не хватает?

Update

Причудливая сторона примечания: разница в таймингах снижается до нескольких процентов, если вы используете int для текущей цели, а не для long. Почему была бы такая небольшая разница с int, и такая большая разница с a long?

Код бенчмаркинга

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

public class Multidimensional {

    public static long sumSingle(final int[] arr) {
        long total = 0;
        for (int i=0; i<arr.length; i++)
            total+=arr[i];
        return total;
    }

    public static long sumMulti(final int[][][] arr) {
        long total = 0;
        for (int i=0; i<arr.length; i++)
            for (int j=0; j<arr[0].length; j++)
                for (int k=0; k<arr[0][0].length; k++)
                    total+=arr[i][j][k];
        return total;
    }   

    public static void main(String[] args) {
        final int iterations = 1000000;

        Random r = new Random();
        int[] arr = new int[1000000];
        for (int i=0; i<arr.length; i++)
            arr[i]=r.nextInt();
        long total = 0;
        System.out.println(sumSingle(arr));
        long time = System.nanoTime();
        for (int i=0; i<iterations; i++)
            total = sumSingle(arr);
        time = System.nanoTime()-time;
        System.out.printf("Took %d ms for single dimension\n", time/1000000, total);

        int[][][] arrMulti = new int[100][100][100];
        for (int i=0; i<arrMulti.length; i++)
            for (int j=0; j<arrMulti[i].length; j++)
                for (int k=0; k<arrMulti[i][j].length; k++)
                    arrMulti[i][j][k]=r.nextInt();
        System.out.println(sumMulti(arrMulti));
        time = System.nanoTime();
        for (int i=0; i<iterations; i++)
            total = sumMulti(arrMulti);
        time = System.nanoTime()-time;
        System.out.printf("Took %d ms for multi dimension\n", time/1000000, total);
    }

}
4b9b3361

Ответ 1

но кажется, что это действительно не то, что можно было ожидать.

Почему?

Учтите, что форма T[] означает "массив типа T", тогда как мы ожидаем, что int[] означает "массив типа int", мы ожидаем, что int[][] означает "массив массива типов type int", потому что нет меньше причин иметь int[] как T чем int.

Таким образом, учитывая, что можно иметь массивы любого типа, это следует только из того, как [ и ] используются при объявлении и инициализации массивов (и, если на то пошло, {, } и ,), что без какого-либо специального правила, запрещающего массивы массивов, мы получаем такое использование "бесплатно".

Теперь рассмотрим также, что есть вещи, которые мы можем сделать с зубчатыми массивами, которые мы не можем сделать иначе:

  • У нас могут быть "зубчатые" массивы, где разные внутренние массивы имеют разные размеры.
  • Мы можем иметь нулевые массивы во внешнем массиве, где необходимо сопоставление данных, или, возможно, для создания ленивого здания.
  • Мы можем преднамеренно использовать псевдоним в массиве, например, lookup[1] - это тот же массив, что и lookup[5]. (Это может обеспечить значительную экономию с некоторыми наборами данных, например, многие свойства Unicode могут быть отображены для полного набора 1112 064 кодовых точек в небольшом объеме памяти, потому что листовые массивы свойств могут повторяться для диапазонов с соответствующими шаблонами).
  • Некоторые реализации кучи могут обрабатывать множество объектов меньшего размера, чем один большой объект в памяти.

Есть, конечно, случаи, когда эти типы многомерных массивов полезны.

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

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

И наоборот, разновидность многомерного массива, где массив имеет определенный ранг, который может быть больше 1 и поэтому использоваться с набором индексов, а не с одним индексом, естественно не вытекает из того, что уже определено. Кому-то понадобится:

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

Также пользователям нужно будет изучить эту новую функцию.

Итак, это стоит того. Некоторые вещи, которые бы стоили этого, были бы следующими:

  • Если не было никакого способа сделать то же самое.
  • Если способ сделать то же самое было странным или не известным.
  • Люди ожидали бы от подобных контекстов.
  • Пользователи не могут предоставлять аналогичную функциональность.

В этом случае, хотя:

  • Но есть.
  • Использование шагов в массивах уже было известно программистам на C и С++ и Java, построенным на его синтаксисе, так что те же самые методы непосредственно применимы
  • Синтаксис Java был основан на С++, а С++ аналогично имеет непосредственную поддержку многомерных массивов как массивов массивов. (За исключением случаев, когда статически выделено, но это не то, что имеет аналогию в Java, где массивы являются объектами).
  • Можно легко написать класс, который обертывает массив и детали размеров шага и позволяет получить доступ через набор индексов.

Действительно, вопрос не в том, "почему Java не имеет истинных многомерных массивов"? Но "Почему это должно быть?"

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

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

Как и многие слухи, здесь есть элемент истины, но это не полная правда.

.NET-массивы могут иметь несколько рангов. Это не единственный способ, который более гибкий, чем Java. Каждый ранг также может иметь нижнюю границу, отличную от нуля. Таким образом, вы можете, например, иметь массив, который идет от -3 до 42 или двухмерный массив, где один ранг идет от -2 до 5, а другой от 57 до 100 или что-то еще.

С# не дает полного доступа ко всему этому из его встроенного синтаксиса (вам нужно вызывать Array.CreateInstance() для нижних границ, отличных от нуля), но он делает это, чтобы вы могли использовать синтаксис int[,] для двумерный массив int, int[,,] для трехмерного массива и т.д.

Теперь дополнительная работа, связанная с нижними границами, отличными от нуля, добавляет бремя производительности, и все же эти случаи относительно необычны. По этой причине массивы с одним рангами с нижней границей 0 рассматриваются как частный случай с более реалистичной реализацией. В самом деле, они внутренне представляют собой разную структуру.

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

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

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

Ответ 2

Это должен быть вопрос Джеймсу Гослину, я полагаю. Первоначальный дизайн Java был о ООП и простоте, а не скорости.

Если у вас есть лучшее представление о том, как многомерные массивы должны работать, есть несколько способов оживить его:

UPD. Конечно, вы не первый, кто ставит под вопрос проблемы проектирования массивов Java.
Например, проекты Sumatra и Panama также выиграют от истинных многомерных массивов.

"Arrays 2.0" - это разговор Джона Роуза на эту тему на JVM Language Summit 2012.

Ответ 3

Мне кажется, что вы сами ответили на вопрос:

... стимул писать его как плоский массив, даже если это делает неестественным и трудночитаемым.

Так напишите его как плоский массив, который легко читать. С помощью тривиального помощника, такого как

double get(int row, int col) {
    return data[rowLength * row + col];
}

и аналогичный установщик и, возможно, += -эквивалент, вы можете притвориться, что работаете с 2D-массивом. Это действительно неважно. Вы не можете использовать нотацию массива, и все становится многословным и уродливым. Но это похоже на путь Java. Это точно так же, как с BigInteger или BigDecimal. Вы не можете использовать фигурные скобки для доступа к Map, что очень похоже на случай.

Теперь вопрос в том, насколько важны все эти функции? Будут ли еще люди счастливы, если они смогут написать x += BigDecimal.valueOf("123456.654321") + 10; или spouse["Paul"] = "Mary";, или использовать 2D-массивы без шаблона, или что? Все это было бы хорошо, и вы могли бы пойти дальше, например, массивные фрагменты. Но нет настоящей проблемы. Вы должны выбирать между многословием и неэффективностью, как во многих других случаях. ИМХО, усилия, потраченные на эту функцию, могут быть лучше потрачены в другом месте. Ваши 2D-массивы являются лучшими...

На самом деле Java не имеет 2D примитивных массивов,...

это в основном синтаксический сахар, базовая вещь - это массив объектов.

double[][] a = new double[1][1];
Object[] b = a;

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

  • В настоящее время существует 8 примитивных типов, что означает 9 типов массивов, будет ли 2D-массив десятым? Как насчет 3D?
  • Для массивов существует отдельный тип заголовка специального объекта. Для 2D-массива может понадобиться еще один.
  • Как насчет java.lang.reflect.Array? Клонировать его для 2D-массивов?
  • Многие другие функции были бы адаптированы, например. сериализации.

И что бы

??? x = {new int[1], new int[2]};

быть? В старом стиле 2D int[][]? Как насчет совместимости?

Я думаю, все это выполнимо, но на Java отсутствуют более простые и важные вещи. Некоторым людям нужны двумерные массивы все время, но многие не могут вспомнить, когда они использовали какой-либо массив вообще.

Ответ 4

Я не могу воспроизвести преимущества производительности, которые вы требуете. В частности, программа тестирования:

public abstract class Benchmark {

    final String name;

    public Benchmark(String name) {
        this.name = name;
    }

    abstract int run(int iterations) throws Throwable;

    private BigDecimal time() {
        try {
            int nextI = 1;
            int i;
            long duration;
            do {
                i = nextI;
                long start = System.nanoTime();
                run(i);
                duration = System.nanoTime() - start;
                nextI = (i << 1) | 1;
            } while (duration < 1000000000 && nextI > 0);
            return new BigDecimal((duration) * 1000 / i).movePointLeft(3);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public String toString() {
        return name + "\t" + time() + " ns";
    }

    public static void main(String[] args) throws Exception {
        final int[] flat = new int[100*100*100];
        final int[][][] multi = new int[100][100][100];

        Random chaos = new Random();
        for (int i = 0; i < flat.length; i++) {
            flat[i] = chaos.nextInt();
        }
        for (int i=0; i<multi.length; i++)
            for (int j=0; j<multi[0].length; j++)
                for (int k=0; k<multi[0][0].length; k++)
                    multi[i][j][k] = chaos.nextInt();

        Benchmark[] marks = {
            new Benchmark("flat") {
                @Override
                int run(int iterations) throws Throwable {
                    long total = 0;
                    for (int j = 0; j < iterations; j++)
                        for (int i = 0; i < flat.length; i++)
                            total += flat[i];
                    return (int) total;
                }
            },
            new Benchmark("multi") {
                @Override
                int run(int iterations) throws Throwable {
                    long total = 0;
                    for (int iter = 0; iter < iterations; iter++)
                        for (int i=0; i<multi.length; i++)
                            for (int j=0; j<multi[0].length; j++)
                                for (int k=0; k<multi[0][0].length; k++)
                                    total+=multi[i][j][k];
                    return (int) total;
                }
            },
            new Benchmark("multi (idiomatic)") {
                @Override
                int run(int iterations) throws Throwable {
                    long total = 0;
                    for (int iter = 0; iter < iterations; iter++)
                        for (int[][] a : multi)
                            for (int[] b : a)
                                for (int c : b)
                                    total += c;
                    return (int) total;
                }
            }

        };

        for (Benchmark mark : marks) {
            System.out.println(mark);
        }
    }
}

запустить на моей рабочей станции с помощью

java version "1.8.0_05"
Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode)

печатает

flat              264360.217 ns
multi             270303.246 ns
multi (idiomatic) 266607.334 ns

То есть мы наблюдаем лишь 3% -ную разницу между одномерным и многомерным кодом, который вы предоставили. Это различие падает до 1%, если мы используем идиоматическую Java (в частности, расширенный для цикла) для обхода (вероятно, потому, что проверка границ выполняется на одном и том же объекте массива на разрывы циклов, позволяя компилятору только во времени ускорить проверку границ более полно).

Таким образом, производительность представляется недостаточным оправданием для увеличения сложности языка. В частности, для поддержки истинных многомерных массивов язык программирования Java должен был бы различать массивы массивов и многомерные массивы. Точно так же программисты должны были бы различать их и быть в курсе их различий. Разработчики API должны были бы задуматься, следует ли использовать массив массивов или многомерный массив. Компилятор, формат файла класса, верификатор файла класса, интерпретатор и просто компилятор времени должны быть расширены. Это было бы особенно сложно, потому что многомерные массивы с разным количеством измерений имели бы несовместимый макет памяти (поскольку размер их размеров должен быть сохранен для обеспечения проверки границ) и поэтому не может быть подтипами друг друга. Как следствие, методы класса java.util.Arrays, вероятно, должны быть дублированы для каждого подсчета измерений, как и все остальные полиморфные алгоритмы, работающие с массивами.

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

Ответ 5

Поскольку этот вопрос в значительной степени связан с производительностью, позвольте мне внести свой вклад в правильный тест на основе JMH. Я также изменил некоторые вещи, чтобы сделать ваш пример более простым, а производительность - более заметной.

В моем случае я сравниваю 1D-массив с 2D-массивом и использую очень короткое внутреннее измерение. Это самый худший случай для кэша.

Я пробовал как с аккумулятором long, так и int и не видел разницы между ними. Я отправлю версию с помощью int.

@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@OperationsPerInvocation(X*Y)
@Warmup(iterations = 30, time = 100, timeUnit=MILLISECONDS)
@Measurement(iterations = 5, time = 1000, timeUnit=MILLISECONDS)
@State(Scope.Thread)
@Threads(1)
@Fork(1)
public class Measure
{
  static final int X = 100_000, Y = 10;
  private final int[] single = new int[X*Y];
  private final int[][] multi = new int[X][Y];

  @Setup public void setup() {
    final ThreadLocalRandom rnd = ThreadLocalRandom.current();
    for (int i=0; i<single.length; i++) single[i] = rnd.nextInt();
    for (int i=0; i<multi.length; i++)
      for (int j=0; j<multi[0].length; j++)
          multi[i][j] = rnd.nextInt();
  }

  @Benchmark public long sumSingle() { return sumSingle(single); }

  @Benchmark public long sumMulti() { return sumMulti(multi); }

  public static long sumSingle(int[] arr) {
    int total = 0;
    for (int i=0; i<arr.length; i++)
      total+=arr[i];
    return total;
  }

  public static long sumMulti(int[][] arr) {
    int total = 0;
    for (int i=0; i<arr.length; i++)
      for (int j=0; j<arr[0].length; j++)
          total+=arr[i][j];
    return total;
  }
}

Разница в производительности больше, чем вы измеряли:

Benchmark                Mode  Samples  Score  Score error  Units
o.s.Measure.sumMulti     avgt        5  1,356        0,121  ns/op
o.s.Measure.sumSingle    avgt        5  0,421        0,018  ns/op

Это фактор выше трех. (Обратите внимание, что время указывается для элемента массива.)

Я также отмечаю, что не происходит никакого разминки: первые 100 мс так же быстро, как и остальные. По-видимому, это такая простая задача, что интерпретатор уже делает все возможное, чтобы сделать ее оптимальной.

Update

Изменение внутреннего цикла sumMulti на

      for (int j=0; j<arr[i].length; j++)
          total+=arr[i][j];

(примечание arr[i].length) привело к значительному ускорению, как предсказано maaartinus. Использование arr[0].length делает невозможным устранение проверки диапазона индекса. Теперь результаты следующие:

Benchmark                Mode  Samples  Score   Error  Units
o.s.Measure.sumMulti     avgt        5  0,992 ± 0,066  ns/op
o.s.Measure.sumSingle    avgt        5  0,424 ± 0,046  ns/op

Ответ 6

Если вам нужна быстрая реализация истинного многомерного массива, вы можете написать подобную реализацию, подобную этой. Но вы правы... это не так хруст, как обозначение массива. Хотя, аккуратная реализация может быть довольно дружелюбной.

public class MyArray{
    private int rows = 0;
    private int cols = 0;
    String[] backingArray = null;
    public MyArray(int rows, int cols){
        this.rows = rows;
        this.cols = cols;
        backingArray  = new String[rows*cols];
    }
    public String get(int row, int col){
        return backingArray[row*cols + col];
    }
    ... setters and other stuff
}

Почему это не стандартная реализация?

Разработчики Java, вероятно, должны были решить, как будет выглядеть стандартная запись обычного синтаксиса массива C. У них была единственная запись в массиве, которая могла либо реализовать массивы массивов, либо истинные многомерные массивы.

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

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

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

Почему он еще не включен в язык?

Даже сегодня настоящие многомерные массивы представляют собой риск с точки зрения возможного потери/неправильного использования памяти.