Вносит ли кастинг Java накладные расходы? Зачем?

105

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

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

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

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

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

Фил
источник
Производительность instanceof и casting неплохая. Я опубликовал некоторые сроки в Java7 для различных подходов к проблеме здесь: stackoverflow.com/questions/16320014/…
Wheezil
На этот другой вопрос есть очень хорошие ответы stackoverflow.com/questions/16741323/…
user454322

Ответы:

78

Есть 2 типа литья:

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

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

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

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

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

Взято из JavaWorld: стоимость кастинга

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

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

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

Алекс Нтусиас
источник
102
Этой статье JavaWorld более 10 лет, поэтому я бы воспринял любые ее утверждения о производительности с очень большой долей вашей доброй воли.
skaffman
1
@skaffman, На самом деле я бы отнесся к любому его заявлению (независимо от его производительности) с недоверием.
Pacerier
Будет ли это тот же случай, если я не назначу приведенный объект ссылке и просто вызову для него метод? нравится((String)o).someMethodOfCastedClass()
Парт Вишваджит
4
Сейчас статье почти 20 лет. И ответам тоже много лет. На этот вопрос нужен современный ответ.
Раслановое
А как насчет примитивных типов? Я имею в виду, например, что приведение из int к short вызывает аналогичные накладные расходы?
luke1985
44

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

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

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

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

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

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

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

Том Хотин - tackline
источник
1
интересно - значит ли это, что для неинтерфейсных классов, если я напишу Superclass sc = (Superclass) subclass; что компилятор (jit, т.е. время загрузки) «статически» вставит смещение от объекта в каждый из суперкласса и подкласса в своих заголовках «Класс», а затем с помощью простого добавления + сравнения сможет разрешить проблемы? - это красиво и быстро :) Для интерфейсов я бы предположил не хуже, чем небольшая хеш-таблица или btree?
peterk
@peterk При преобразовании между классами адрес объекта и "vtbl" (таблица указателей на методы, плюс таблица иерархии классов, кеш интерфейса и т. д.) не меняются. Таким образом, приведение [class] проверяет тип, и если он подходит, ничего другого не должно происходить.
Том Хотин - tackline
8

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

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

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

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

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

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

Стивен С
источник
1
А как насчет примитивов? (float) Math.toDegrees(theta)Будут ли здесь значительные накладные расходы?
SD
2
Для некоторых примитивных приведений есть накладные расходы. Насколько это важно, зависит от контекста.
Stephen C
6

Вызывается инструкция байт-кода для выполнения приведения во время выполнения 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инструкции.

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

JesperE
источник
2

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

Мораль этой истории - сначала написать понятный код. Если вы столкнулись с замедлением, профилируйте и определите свою проблему. Велика вероятность, что это не из-за кастинга. Никогда не жертвуйте чистым и безопасным кодом, пытаясь его оптимизировать, ДО тех пор, пока ВЫ НЕ ЗНАЕТЕ, ЧТО ВАМ НУЖНО.

HesNotTheStig
источник