자바 성능 프로그래밍, Part 2 : 캐스팅 비용

Java 성능에 대한 시리즈의이 두 번째 기사에서는 캐스팅에 초점을 맞 춥니 다. 즉, 그것이 무엇인지, 비용이 얼마인지, 그리고 그것을 피할 수있는 방법 (때로는)입니다. 이번 달에는 클래스, 객체 및 참조의 기본 사항에 대한 빠른 검토로 시작한 다음 몇 가지 하드 코어 성능 수치를 살펴 봅니다. JVM (Java Virtual Machine) 소화 불량을 유발할 가능성이 가장 높은 작업 유형. 마지막으로 캐스팅을 유발할 수있는 일반적인 클래스 구조 효과를 피할 수있는 방법을 심층적으로 살펴 보겠습니다.

자바 성능 프로그래밍 : 전체 시리즈 읽기!

  • Part 1. 객체 생성 및 가비지 수집을 제어하여 프로그램 오버 헤드를 줄이고 성능을 향상시키는 방법을 배웁니다.
  • Part 2. 형식 안전 코드를 통해 오버 헤드 및 실행 오류 감소
  • Part 3. collectionsalternatives가 성능을 어떻게 측정하는지 확인하고 각 유형을 최대한 활용하는 방법 알아보기

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와 동일한 유형의 객체 또는 모든 하위 클래스의 객체 (즉, 하위 클래스 from) 참조의 유형. 이 특별한 경우에는 java.awt.Component은 추상 클래스이므로 참조와 동일한 유형의 객체는있을 수 없지만 해당 참조 유형의 하위 클래스의 객체는 확실히있을 수 있습니다.

다형성 및 캐스팅

참조 유형은 참조 된 객체 , 즉 참조 값인 객체를 사용할 수있는 방법을 결정합니다. 예를 들어, 위의 예제에서 using 코드 는 참조 된 객체에서 클래스 또는 수퍼 myComponent클래스에 의해 정의 된 모든 메서드를 호출 할 수 java.awt.Component있습니다.

그러나 실제로 호출에 의해 실행되는 메서드는 참조 자체의 유형이 아니라 참조 된 개체의 유형에 의해 결정됩니다. 이것이 다형성 의 기본 원칙입니다. 하위 클래스는 다른 동작을 구현하기 위해 부모 클래스에 정의 된 메서드를 재정의 할 수 있습니다. 예제 변수의 경우 참조 된 객체가 실제로의 인스턴스 인 java.awt.Button경우 setLabel("Push Me")호출로 인한 상태 변경 은 참조 된 객체가의 인스턴스 인 경우의 결과와 다릅니다 java.awt.Label.

클래스 정의 외에도 Java 프로그램은 인터페이스 정의도 사용합니다. 인터페이스와 클래스의 차이점은 인터페이스는 동작 집합 (및 경우에 따라 상수) 만 지정하고 클래스는 구현을 정의한다는 것입니다. 인터페이스는 구현을 정의하지 않기 때문에 객체는 인터페이스의 인스턴스가 될 수 없습니다. 그러나 인터페이스를 구현하는 클래스의 인스턴스 일 수 있습니다. 참조 인터페이스 유형일 있으며,이 경우 참조 된 객체는 인터페이스를 구현하는 모든 클래스의 인스턴스 일 수 있습니다 (직접 또는 일부 상위 클래스를 통해).

캐스팅 은 유형 간, 특히 여기서 관심이있는 캐스팅 작업 유형에 대한 참조 유형 간 변환에 사용됩니다. 업 캐스트 작업 ( Java 언어 사양에서 확장 변환 이라고도 함 )은 하위 클래스 참조를 상위 클래스 참조로 변환합니다. 이 캐스팅 작업은 항상 안전하고 컴파일러에서 직접 구현할 수 있기 때문에 일반적으로 자동입니다.

다운 캐스트 작업 ( Java 언어 사양에서 축소 변환 이라고도 함)은 상위 클래스 참조를 하위 클래스 참조로 변환합니다. 이 캐스팅 작업은 실행 오버 헤드를 발생시킵니다. Java는 캐스트가 유효한지 확인하기 위해 런타임시 확인해야하기 때문입니다. 참조 된 객체가 캐스트의 대상 유형 또는 해당 유형의 하위 클래스의 인스턴스가 아닌 경우 시도 된 캐스트는 허용되지 않으며 java.lang.ClassCastException.

instanceofJava 의 연산자를 사용하면 실제로 작업을 시도하지 않고도 특정 캐스팅 작업이 허용되는지 여부를 결정할 수 있습니다. 검사의 성능 비용은 허용되지 않은 캐스트 시도에 의해 생성 된 예외보다 훨씬 적기 때문에 일반적으로 instanceof참조 유형이 원하는 유형인지 확실하지 않을 때마다 테스트 를 사용하는 것이 좋습니다 . . 그러나 그렇게하기 전에 원치 않는 유형의 참조를 처리하는 합리적인 방법이 있는지 확인해야합니다. 그렇지 않으면 예외를 throw하고 코드에서 더 높은 수준에서 처리 할 수 ​​있습니다.

바람에주의를 기울임

캐스팅을 통해 Java에서 일반 프로그래밍을 사용할 수 있습니다. 여기서 코드는 일부 기본 클래스 (일반적 java.lang.Object으로 유틸리티 클래스의 경우) 에서 파생 된 모든 클래스 개체와 함께 작동하도록 작성됩니다 . 그러나 캐스팅을 사용하면 고유 한 문제가 발생합니다. 다음 섹션에서는 성능에 미치는 영향을 살펴 보 겠지만 먼저 코드 자체에 미치는 영향을 살펴 보겠습니다. 다음은 일반 java.lang.Vector컬렉션 클래스를 사용하는 샘플입니다 .

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

이 코드는 명확성과 유지 보수 측면에서 잠재적 인 문제를 제시합니다. 원래 개발자가 아닌 다른 사람이 어떤 시점에서 코드를 수정해야한다면이 코드 는의 하위 클래스이므로 컬렉션에 java.lang.Double를 추가 할 수 있다고 합리적으로 생각할 수 있습니다 . 그가 이것을 시도하면 모든 것이 잘 컴파일되지만 실행의 불확실한 지점에서 그는 부가 가치를 위해 시도 된 캐스트 가 실행 되었을 때 던질 가능성이 있습니다.someNumbersjava.lang.Numberjava.lang.ClassCastExceptionjava.lang.Integer

여기서 문제는 캐스팅을 사용하면 Java 컴파일러에 내장 된 안전 검사를 우회한다는 것입니다. 컴파일러가 오류를 포착하지 못하기 때문에 프로그래머는 실행 중에 오류를 찾기 시작합니다. 이것은 그 자체로 재앙은 아니지만, 이러한 유형의 사용 오류는 종종 코드를 테스트하는 동안 매우 영리하게 숨겨져 프로그램이 프로덕션에 들어갈 때만 드러납니다.

당연히 컴파일러가 이러한 유형의 사용 오류를 감지 할 수있는 기술에 대한 지원은 Java에 대해 가장 많이 요청 된 개선 사항 중 하나입니다. 이 지원 만 추가하는 것을 조사중인 Java 커뮤니티 프로세스에서 현재 진행중인 프로젝트가 있습니다. 프로젝트 번호 JSR-000014, Java 프로그래밍 언어에 일반 유형 추가 (자세한 내용은 아래 리소스 섹션 참조)이 기사의 계속에서, 다음 달에 우리는이 프로젝트를 더 자세히 살펴보고 그것이 어떻게 도움이 될 수 있는지, 그리고 우리가 더 많은 것을 원하게 될 부분에 대해 논의 할 것입니다.

성능 문제

캐스팅은 Java의 성능에 해로울 수 있으며 많이 사용되는 코드에서 캐스팅을 최소화하여 성능을 향상시킬 수 있다는 사실은 오랫동안 인식되어 왔습니다. 메서드 호출, 특히 인터페이스를 통한 호출도 종종 잠재적 인 성능 병목 현상으로 언급됩니다. 하지만 현재 세대의 JVM은 이전 버전과 크게 다르며 이러한 원칙이 오늘날 얼마나 잘 유지되는지 확인하는 것이 좋습니다.

이 기사에서는 이러한 요소가 현재 JVM의 성능에 얼마나 중요한지 확인하기 위해 일련의 테스트를 개발했습니다. 테스트 결과는 사이드 바에 메서드 호출 오버 헤드를 보여주는 표 1과 캐스팅 오버 헤드를 보여주는 표 2로 요약되어 있습니다. 테스트 프로그램의 전체 소스 코드는 온라인에서도 사용할 수 있습니다 (자세한 내용은 아래 리소스 섹션 참조).

표의 세부 사항을 검토하고 싶지 않은 독자를 위해 이러한 결론을 요약하면 특정 유형의 메서드 호출 및 캐스트는 여전히 상당히 비싸며, 어떤 경우에는 간단한 개체 할당만큼 오래 걸리는 경우도 있습니다. 가능한 경우 성능을 최적화해야하는 코드에서는 이러한 유형의 작업을 피해야합니다.

특히 재정의 된 메서드 (객체의 실제 클래스뿐 아니라로드 된 모든 클래스에서 재정의되는 메서드)에 대한 호출과 인터페이스를 통한 호출은 단순한 메서드 호출보다 훨씬 더 많은 비용이 듭니다. 테스트에 사용 된 HotSpot Server JVM 2.0 베타는 많은 간단한 메서드 호출을 인라인 코드로 변환하여 이러한 작업에 대한 오버 헤드를 방지합니다. 그러나 HotSpot은 오버라이드 된 메소드 및 인터페이스를 통한 호출에 대해 테스트 된 JVM 중 최악의 성능을 보여줍니다.

캐스팅 (물론 다운 캐스팅)의 경우 테스트 된 JVM은 일반적으로 성능 저하를 합리적인 수준으로 유지합니다. HotSpot은 대부분의 벤치 마크 테스트에서 예외적 인 작업을 수행하며 메서드 호출과 마찬가지로 많은 간단한 경우 캐스팅의 오버 헤드를 거의 완전히 제거 할 수 있습니다. 캐스트와 재정의 된 메소드 호출과 같은 더 복잡한 상황의 경우 테스트 된 모든 JVM이 눈에 띄는 성능 저하를 보여줍니다.

테스트 된 HotSpot 버전은 객체가 연속적으로 다른 참조 유형으로 캐스트 될 때 (항상 동일한 대상 유형으로 캐스트되는 대신) 극도로 저하 된 성능을 보여주었습니다. 이러한 상황은 클래스의 깊은 계층 구조를 사용하는 Swing과 같은 라이브러리에서 정기적으로 발생합니다.

대부분의 경우 메서드 호출과 캐스팅의 오버 헤드는 지난 달 기사에서 살펴본 객체 할당 시간에 비해 적습니다. 그러나 이러한 작업은 종종 개체 할당보다 훨씬 더 자주 사용되므로 여전히 성능 문제의 중요한 원인이 될 수 있습니다.

이 기사의 나머지 부분에서는 코드 캐스팅의 필요성을 줄이기위한 몇 가지 특정 기술에 대해 설명합니다. 특히, 서브 클래스가 기본 클래스와 상호 작용하는 방식에서 캐스팅이 얼마나 자주 발생하는지 살펴보고 이러한 유형의 캐스팅을 제거하기위한 몇 가지 기술을 살펴 보겠습니다. 다음 달, 캐스팅에 대한 두 번째 부분에서는 캐스팅의 또 다른 일반적인 원인 인 일반 컬렉션의 사용을 고려할 것입니다.

기본 클래스 및 캐스팅

Java 프로그램에는 몇 가지 일반적인 캐스팅 사용이 있습니다. 예를 들어, 캐스팅은 여러 하위 클래스에 의해 확장 될 수있는 기본 클래스의 일부 기능을 일반 처리하는 데 자주 사용됩니다. 다음 코드는이 사용법에 대한 다소 인위적인 그림을 보여줍니다.

// 하위 클래스가있는 단순 기본 클래스 public abstract class BaseWidget {...} public class SubWidget extends BaseWidget {... public void doSubWidgetSomething () {...}} ... // 이전 집합을 사용하여 하위 클래스가있는 기본 클래스 of classes public abstract class BaseGorph {//이 Gorph와 관련된 위젯 private BaseWidget myWidget; ... //이 Gorph와 관련된 위젯 설정 (하위 클래스에만 허용됨) protected void setWidget (BaseWidget widget) {myWidget = widget; } //이 Gorph와 관련된 위젯을 가져옵니다. public BaseWidget getWidget () {return myWidget; } ... //이 Gorph와 어떤 관계가있는 Gorph를 반환합니다. // 이것은 항상 호출 된 것과 동일한 유형이지만 // 기본 클래스의 인스턴스 만 반환 할 수 있습니다. public abstract BaseGorph otherGorph () {. ..}} // 위젯 하위 클래스를 사용하는 Gorph 하위 클래스 public class SubGorph extends BaseGorph {//이 Gorph와 관련된 Gorph를 반환 public BaseGorph otherGorph () {...} ... public void anyMethod () {... / / 우리가 사용하고있는 위젯 설정 SubWidget widget = ... setWidget (widget); ... // 위젯 사용 ((SubWidget) getWidget ()). doSubWidgetSomething (); ... // otherGorph SubGorph를 사용합니다. other = (SubGorph) otherGorph (); ...}}