Програмиране на производителността на Java, Част 2: Цената на кастинга

За тази втора статия от нашата поредица за производителността на Java фокусът се измества към кастинг - какво е това, какво струва и как можем (понякога) да го избегнем. Този месец започваме с бърз преглед на основите на класовете, обектите и препратките, след което проследяваме някои хардкорни цифри за изпълнение (в странична лента, за да не обидим скверните!) И насоки за типове операции, които най-вероятно причиняват лошо храносмилане на вашата Java Virtual Machine (JVM). Накрая завършваме с по-задълбочен поглед върху това как можем да избегнем често срещаните ефекти на структуриране на класа, които могат да причинят кастинг.

Програмиране на производителността на Java: Прочетете цялата поредица!

  • Част 1. Научете как да намалите режийните разходи на програмата и да подобрите производителността, като контролирате създаването на обекти и събирането на боклука
  • Част 2. Намаляване на режийните и грешките при изпълнението чрез безопасен за типа код
  • Част 3. Вижте как алтернативите на колекциите измерват ефективността и разберете как да извлечете максимума от всеки тип

Типове обекти и справки в Java

Миналия месец обсъдихме основното разграничение между примитивни типове и обекти в Java. Както броят на примитивните типове, така и връзките между тях (особено преобразуванията между типовете) се определят от дефиницията на езика. Обектите, от друга страна, са от неограничен тип и могат да бъдат свързани с произволен брой други видове.

Всяка дефиниция на клас в програма на Java дефинира нов тип обект. Това включва всички класове от библиотеките на Java, така че всяка дадена програма може да използва стотици или дори хиляди различни видове обекти. Няколко от тези типове са определени от дефиницията на езика Java като имащи определени специални употреби или обработка (като използването на java.lang.StringBufferза java.lang.Stringоперации по конкатенация). Освен тези няколко изключения обаче, всички типове се третират основно по един и същи начин от Java компилатора и JVM, използвани за изпълнение на програмата.

Ако дефиницията на клас не посочва (посредством extendsклаузата в заглавката на дефиницията на клас) друг клас като родител или суперклас, тя неявно разширява java.lang.Objectкласа. Това означава, че в крайна сметка всеки клас се разширява java.lang.Objectили директно, или чрез последователност от едно или повече нива на родителски класове.

Самите обекти винаги са екземпляри на класове, а типът на обекта е класът, на който е екземпляр. В Java обаче никога не се занимаваме директно с обекти; работим с препратки към обекти. Например линията:

 java.awt.Component myComponent; 

не създава java.awt.Componentобект; създава референтна променлива от тип java.lang.Component. Въпреки че референциите имат типове точно както обектите, няма точно съвпадение между референтни и типове обекти - референтната стойност може да бъде null, обект от същия тип като референцията или обект от който и да е подклас (т.е. от) вида на препратката. В този конкретен случай java.awt.Componentе абстрактен клас, така че знаем, че никога не може да има обект от същия тип като нашата референция, но със сигурност може да има обекти от подкласове от този референтен тип.

Полиморфизъм и отливка

Типът на референцията определя как референтният обект - т.е. обектът, който е стойността на референцията - може да бъде използван. Например, в примера по-горе, използването на код myComponentможе да извика някой от методите, дефинирани от класа java.awt.Component, или някой от неговите суперкласове, върху реферирания обект.

Обаче методът, действително изпълнен чрез извикване, се определя не от типа на самата препратка, а по-скоро от типа на препращания обект. Това е основният принцип на полиморфизма - подкласовете могат да заменят методите, дефинирани в родителския клас, за да реализират различно поведение. В случая с нашата примерна променлива, ако реферираният обект всъщност е екземпляр на java.awt.Button, промяната в състоянието в резултат на setLabel("Push Me")извикване би била различна от резултата, ако реферираният обект е екземпляр на java.awt.Label.

Освен дефинициите на класове, програмите Java също използват дефиниции на интерфейси. Разликата между интерфейс и клас е, че интерфейсът задава само набор от поведения (и в някои случаи константи), докато клас дефинира изпълнение. Тъй като интерфейсите не дефинират реализации, обектите никога не могат да бъдат екземпляри на интерфейс. Те обаче могат да бъдат екземпляри от класове, които реализират интерфейс. Препратките могат да бъдат от типове интерфейс, като в този случай референтните обекти могат да бъдат екземпляри на всеки клас, който реализира интерфейса (или директно, или чрез някакъв клас предшественик).

Кастингът се използва за преобразуване между типове - по-специално между референтните типове, за типа операция на леене, от която се интересуваме тук. Upcast операции (наричани още разширяващи се преобразувания в спецификацията на езика Java) преобразуват препратка към подклас в препратка към клас предшественик. Тази операция на леене обикновено е автоматична, тъй като винаги е безопасна и може да бъде приложена директно от компилатора.

Даунскастичните операции (наричани също стесняване на преобразувания в спецификацията на езика Java) преобразуват препратка към клас предшественик към препратка към подклас. Тази операция на кастинг създава режийни, тъй като Java изисква проверяването на изпълнението по време на изпълнение, за да се увери, че е валидно. Ако реферираният обект не е екземпляр нито на целевия тип за гласовете, нито на подклас от този тип, опитът за кастиране не е разрешен и трябва да хвърли a java.lang.ClassCastException.

Най- instanceofоператор в Java позволява да се определи дали има или няма конкретна операция леене е разрешено, без всъщност да се опитва операцията. Тъй като цената на производителността на проверката е много по-малка от тази на изключението, генерирано от неразрешен опит за гласуване, обикновено е разумно да използвате instanceofтест по всяко време, когато не сте сигурни, че видът на препратка е такъв, какъвто бихте искали да бъде . Преди това обаче трябва да се уверите, че имате разумен начин да се справите с референция от нежелан тип - в противен случай можете просто да оставите изключението да бъде хвърлено и да го обработвате на по-високо ниво във вашия код.

Внимавайте с ветровете

Кастингът позволява използването на общо програмиране в Java, където кодът е написан за работа с всички обекти на класове, произлезли от някакъв основен клас (често java.lang.Objectза помощни класове). Използването на кастинг обаче създава уникален набор от проблеми. В следващия раздел ще разгледаме въздействието върху производителността, но нека първо разгледаме ефекта върху самия код. Ето пример, използващ родовия java.lang.Vectorклас за събиране:

private Vector someNumbers; ... public void doSomething () {... int n = ... Integer number = (Integer) someNumbers.elementAt (n); ...}

Този код представя потенциални проблеми по отношение на яснотата и поддръжката. Ако някой, различен от оригиналния разработчик, трябва да модифицира кода в даден момент, той може разумно да помисли, че може да добави a java.lang.Doubleкъм someNumbersколекциите, тъй като това е подклас на java.lang.Number. Всичко би се компилирало добре, ако той опита това, но в някакъв неопределен момент на изпълнение той вероятно ще бъде java.lang.ClassCastExceptionхвърлен, когато опитът за хвърляне към a java.lang.Integerе изпълнен за добавената му стойност.

Проблемът тук е, че използването на кастинг заобикаля проверките за безопасност, вградени в Java компилатора; програмистът в крайна сметка търси грешки по време на изпълнение, тъй като компилаторът няма да ги хване. Това само по себе си не е пагубно, но този тип грешка при използването често се крие доста хитро, докато тествате кода си, само за да се разкрие, когато програмата бъде пусната в производство.

Не е изненадващо, че поддръжката на техника, която би позволила на компилатора да открие този тип грешка при използване, е едно от най-силно исканите подобрения на Java. В процес на общностния процес в момента има проект, който разследва добавянето само на тази поддръжка: номер на проект JSR-000014, Добавяне на общи типове към езика за програмиране на Java (вижте раздела Ресурси по-долу за повече подробности.) В продължение на тази статия, идвайки следващия месец, ще разгледаме този проект по-подробно и ще обсъдим както възможността да помогне, така и къде е вероятно да ни остави да искаме повече.

Проблем с изпълнението

Отдавна е признато, че кастингът може да навреди на производителността в Java и че можете да подобрите производителността, като намалите кастинга в силно използван код. Извикванията на методи, особено повикванията през интерфейси, също често се споменават като потенциални тесни места в производителността. Настоящото поколение JVM обаче е изминало далеч от своите предшественици и си струва да се провери колко добре тези принципи се държат днес.

За тази статия разработих поредица от тестове, за да видя колко важни са тези фактори за ефективността при настоящите JVM. Резултатите от теста са обобщени в две таблици в страничната лента, Таблица 1 показва режийни повиквания на метода и Таблица 2 режийни разходи. Пълният изходен код за тестовата програма също е достъпен онлайн (вижте раздела Ресурси по-долу за повече подробности).

За да обобщим тези заключения за читателите, които не искат да разглеждат подробностите в таблиците, някои видове извиквания и излъчвания на методи все още са доста скъпи, като в някои случаи отнемат почти толкова дълго, колкото обикновено разпределение на обекти. Където е възможно, тези видове операции трябва да се избягват в код, който трябва да бъде оптимизиран за изпълнение.

По-специално, извикванията към заменени методи (методи, които се заменят във всеки зареден клас, а не само в действителния клас на обекта) и повикванията през интерфейси са значително по-скъпи от обикновените извиквания на методи. Използваният в теста HotSpot Server JVM 2.0 бета дори ще преобразува много извиквания на прост метод във вграден код, като избягва всякакви режийни разходи за подобни операции. HotSpot обаче показва най-лошата производителност сред тестваните JVM за заменени методи и обаждания чрез интерфейси.

За кастинг (разбира се), тестваните JVM обикновено поддържат постигнатото ниво на изпълнение на разумно ниво. HotSpot върши изключителна работа с това в по-голямата част от бенчмарк тестването и, както при извикванията на методите, в много прости случаи е в състояние почти напълно да елиминира режийните разходи за кастинг. За по-сложни ситуации, като замятания, последвани от повиквания към заменени методи, всички тествани JVM показват забележимо влошаване на производителността.

Тестваната версия на HotSpot също показа изключително лоша производителност, когато обектът беше последователно прехвърлен към различни референтни типове (вместо винаги да бъде прехвърлян към един и същ целеви тип). Тази ситуация редовно възниква в библиотеки като Swing, които използват дълбока йерархия на класовете.

In most cases, the overhead of both method calls and casting is small in comparison with the object-allocation times looked at in last month's article. However, these operations will often be used far more frequently than object allocations, so they can still be a significant source of performance problems.

In the remainder of this article, we'll discuss some specific techniques for reducing the need for casting in your code. Specifically, we'll look at how casting often arises from the way subclasses interact with base classes, and explore some techniques for eliminating this type of casting. Next month, in the second part of this look at casting, we'll consider another common cause of casting, the use of generic collections.

Base classes and casting

There are several common uses of casting in Java programs. For instance, casting is often used for the generic handling of some functionality in a base class that may be extended by a number of subclasses. The following code shows a somewhat contrived illustration of this usage:

 // simple base class with subclasses public abstract class BaseWidget { ... } public class SubWidget extends BaseWidget { ... public void doSubWidgetSomething() { ... } } ... // base class with subclasses, using the prior set of classes public abstract class BaseGorph { // the Widget associated with this Gorph private BaseWidget myWidget; ... // set the Widget associated with this Gorph (only allowed for subclasses) protected void setWidget(BaseWidget widget) { myWidget = widget; } // get the Widget associated with this Gorph public BaseWidget getWidget() { return myWidget; } ... // return a Gorph with some relation to this Gorph // this will always be the same type as it's called on, but we can only // return an instance of our base class public abstract BaseGorph otherGorph() { ... } } // Gorph subclass using a Widget subclass public class SubGorph extends BaseGorph { // return a Gorph with some relation to this Gorph public BaseGorph otherGorph() { ... } ... public void anyMethod() { ... // set the Widget we're using SubWidget widget = ... setWidget(widget); ... // use our Widget ((SubWidget)getWidget()).doSubWidgetSomething(); ... // use our otherGorph SubGorph other = (SubGorph) otherGorph(); ... } }