Чи вводить Java кастинг накладні витрати? Чому?


105

Чи є накладні витрати, коли ми перекидаємо предмети одного типу до іншого? Або компілятор просто вирішує все, і немає витрат на час виконання?

Це взагалі речі, або бувають різні випадки?

Наприклад, припустимо, у нас є масив Object [], де кожен елемент може мати інший тип. Але ми завжди точно знаємо, що, скажімо, елемент 0 є подвійним, елемент 1 - це рядком. (Я знаю, що це неправильна конструкція, але давайте припустимо, що мені довелося це зробити.)

Чи зберігається інформація про тип Java ще під час виконання? Або все забуто після компіляції, і якщо ми зробимо (Double) елементи [0], ми просто слідуємо за вказівником і інтерпретуємо ці 8 байтів як подвійне, що б там не було?

Мені дуже незрозуміло, як типи робляться на Java. Якщо у вас є рекомендації щодо книг чи статей, то також дякую.


Продуктивність instanceof та кастингу є досить хорошою. Я відправив трохи часу в 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 років, тому я б хотів взяти будь-які заяви, які вони стосуються продуктивності з дуже великим зерном вашої найкращої солі.
скафман

1
@skaffman, Насправді я б взяв будь-яку заяву, яку він робить (незалежно від продуктивності роботи) із зерном солі.
Pacerier

чи буде це той самий випадок, якщо я не призначу закиданий об'єкт посиланням і просто викличу його метод? як((String)o).someMethodOfCastedClass()
Parth Vishvajit

4
Зараз статті майже 20 років. І відповіді також багато років. На це питання потрібна сучасна відповідь.
Расланове

Як щодо примітивних типів? Я маю на увазі, чи, наприклад, переведення з int на short викликає подібні накладні витрати?
luke1985

44

Для розумної реалізації Java:

Кожен об'єкт має заголовок, що містить, серед іншого, вказівник на тип виконання (наприклад, Doubleабо String, але він ніколи не може бути CharSequenceабо AbstractList). Якщо припустити, що компілятор виконання (як правило, HotSpot у випадку Sun) не може визначити тип статично, то деякі перевірки потрібно виконати створеним машинним кодом.

По-перше, цей вказівник на тип виконання повинен бути прочитаний. Це необхідно для виклику віртуального методу в подібній ситуації в будь-якому випадку.

Для кастингу до типу класу точно відомо, скільки суперкласів існує, поки ви не потрапите java.lang.Object, тому тип можна читати з постійним зміщенням від покажчика типу (фактично першої вісімки в HotSpot). Знову це аналогічно зчитуванням покажчика методу для віртуального методу.

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

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

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


1
цікаво - так це означає, що для неінтерфейсних класів, якщо я пишу підклас Superclass sc = (Superclass); що компілятор (jit, тобто: час завантаження) буде "статично" вводити зміщення від Object у кожному з Superclass і Subclass у своїх заголовках "Class", а потім за допомогою простого додавання + порівняння зможе вирішити речі? - це приємно і швидко :) Що стосується інтерфейсів, я б вважав, не гірше, ніж маленький хештейн або btree?
peterk

@peterk Для кастингу між класами адреса об'єкта та "vtbl" (таблиця покажчиків методів, плюс таблиця ієрархії класів, кеш-інтерфейс тощо) не змінюються. Тож [клас] актор перевіряє тип, і якщо він відповідає, нічого іншого не повинно відбуватися.
Том Хотін - тайклін

8

Наприклад, припустимо, у нас є масив Object [], де кожен елемент може мати інший тип. Але ми завжди точно знаємо, що, скажімо, елемент 0 є подвійним, елемент 1 - це рядком. (Я знаю, що це неправильна конструкція, але давайте припустимо, що мені довелося це зробити.)

Компілятор не відзначає типи окремих елементів масиву. Він просто перевіряє, що тип кожного виразу елемента призначається типу елемента масиву.

Чи зберігається інформація про тип Java ще під час виконання? Або все забуто після компіляції, і якщо ми зробимо (Double) елементи [0], ми просто слідуємо за вказівником і інтерпретуємо ці 8 байтів як подвійне, що б там не було?

Деяка інформація зберігається навколо, але не статичні типи окремих елементів. Про це можна сказати, переглянувши формат файлу класу.

Теоретично можливо, що компілятор JIT міг би використати "аналіз аналізу" для усунення непотрібних перевірок типів у деяких завданнях. Однак зробити це в тій мірі, яку ви пропонуєте, було б поза межами реалістичної оптимізації. Окупність аналізу типів окремих елементів була б занадто малою.

Крім того, люди все одно не повинні писати такий код програми.


1
А як щодо примітивів? (float) Math.toDegrees(theta)Чи буде тут також значне накладне покриття?
SD

2
Є накладні витрати на деякі примітивні ролі. Чи є вона суттєвою, залежить від контексту.
Стівен С

6

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

Для масивів Java зберігає інформацію про тип під час виконання. Більшу частину часу компілятор вловлює для вас помилки типу, але є випадки, коли ви зіткнетесь з a, 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інструкції, де це необхідно.

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


2

Теоретично існує накладні витрати. Однак сучасні JVM розумні. Кожна реалізація є різною, але нерозумно припускати, що може існувати реалізація, яка JIT оптимізувала перевірку лиття, коли це може гарантувати, що конфлікту ніколи не буде. Щодо того, які конкретні JVM пропонують це, я не міг вам сказати. Я мушу визнати, що хотів би сам дізнатися специфіку оптимізації JIT, але це стосується інженерів JVM.

Мораль історії - спершу написати зрозумілий код. Якщо у вас виникають сповільнення, профайлюйте та визначте свою проблему. Коефіцієнти хороші, що це не буде через кастинг. Ніколи не жертвуйте чистим, безпечним кодом, не намагаючись оптимізувати його, поки ви не знаєте, що вам потрібно.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.