Вводит ли Java-литье накладные расходы? Зачем? - PullRequest
94 голосов
/ 31 января 2010

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

Это общие вещи, или есть разные случаи?

Например, предположим, у нас есть массив Object [], где каждый элемент может иметь свой тип. Но мы всегда знаем наверняка, что, скажем, элемент 0 - это Double, элемент 1 - это String. (Я знаю, что это неправильный дизайн, но давайте просто предположим, что я должен был это сделать.)

Сохраняется ли информация о типах Java во время выполнения? Или все забыто после компиляции, и если мы сделаем (Double) elements [0], мы просто последуем за указателем и интерпретируем эти 8 байтов как двойной, что бы это ни было?

Мне очень неясно, как создаются типы в Java. Если у вас есть какие-либо рекомендации по поводу книг или статей, тогда спасибо.

Ответы [ 5 ]

71 голосов
/ 31 января 2010

Существует 2 типа приведения:

Неявное приведение при приведении типа к более широкому типу, что выполняется автоматически без накладных расходов:

String s = "Cast";
Object o = s; // implicit casting

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

Object o = someObject;
String s = (String) o; // explicit casting

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

Взято из JavaWorld: стоимость приведения

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

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

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

43 голосов
/ 31 января 2010

Для разумной реализации Java:

Каждый объект имеет заголовок, содержащий, помимо прочего, указатель на тип времени выполнения (например, Double или String, но он никогда не может быть CharSequence или AbstractList). Предполагая, что компилятор времени выполнения (как правило, HotSpot в случае Sun) не может определить тип статически, необходимо выполнить некоторую проверку сгенерированного машинного кода.

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

Для приведения к типу класса точно известно, сколько существует суперклассов, пока вы не нажмете java.lang.Object, поэтому тип можно прочитать с постоянным смещением от указателя типа (фактически, первых восьми в HotSpot). Опять же, это аналогично чтению указателя метода для виртуального метода.

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

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

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

7 голосов
/ 31 января 2010

Например, предположим, у нас есть массив Object [], где каждый элемент может иметь различный тип. Но мы всегда знаем наверняка, что, скажем, элемент 0 - это Double, элемент 1 - это String. (Я знаю, что это неправильный дизайн, но давайте просто предположим, что я должен был это сделать.)

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

Сохраняется ли информация о типах Java во время выполнения? Или все забыто после компиляции, и если мы сделаем (Double) elements [0], мы просто последуем за указателем и интерпретируем эти 8 байтов как двойной, что бы это ни было?

Некоторая информация хранится во время выполнения, но не статические типы отдельных элементов. Вы можете сказать это, посмотрев на формат файла класса.

Теоретически возможно, что JIT-компилятор может использовать «escape-анализ», чтобы исключить ненужные проверки типов в некоторых присваиваниях. Однако делать это в той степени, в которой вы предлагаете, было бы за пределами реалистичной оптимизации. Окупаемость анализа типов отдельных элементов была бы слишком мала.

Кроме того, люди не должны так писать код приложения.

5 голосов
/ 31 января 2010

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

Для массивов Java хранит информацию о типе во время выполнения. Большую часть времени компилятор будет отлавливать ошибки типов для вас, но в некоторых случаях вы сталкиваетесь с ArrayStoreException при попытке сохранить объект в массиве, но тип не совпадает (а компилятор не Лови). Спецификация языка Java дает следующий пример:

class Point { int x, y; }
class ColoredPoint extends Point { int color; }
class Test {
    public static void main(String[] args) {
        ColoredPoint[] cpa = new ColoredPoint[10];
        Point[] pa = cpa;
        System.out.println(pa[1] == null);
        try {
            pa[0] = new Point();
        } catch (ArrayStoreException e) {
            System.out.println(e);
        }
    }
}

Point[] pa = cpa действителен, поскольку ColoredPoint является подклассом Point, но pa[0] = new Point() недопустим.

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

Это различие в типизации для универсальных типов и массивов делает невозможным смешивание массивов и универсальных типов.

1 голос
/ 10 ноября 2016

В теории вводятся накладные расходы. Однако современные JVM умны. Каждая реализация индивидуальна, но не исключено, что может существовать реализация, которая JIT оптимизировала бы при отсутствии проверок приведения, когда это могло бы гарантировать, что никогда не будет конфликта. Что касается того, какие конкретные JVM предлагают это, я не могу вам сказать. Должен признать, что сам хотел бы знать особенности оптимизации JIT, но об этом следует беспокоиться инженерам JVM.

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

...