본문 바로가기

Architecture for Software/Java

[Java의 이해] 핫스팟VM의 메소드 인라이닝

Java의 메소드 인라인(Method Inline)에 관한 좋은 글이 있어서 공유하고자 올립니다.
기회가 있을때 마다 Java의 이해라는 글을 올리고자 합니다.
이 글은 The Java HotSpot Performance Engine: Method Inlining Example 을 바탕으로 작성하였습니다.

 

Java에서는 성능 향상을 위하여 Java HotSpot Engine(VM)이 다음과 같은 경우 메소드 인라이닝(Method Inlining)을 합니다. 이를 통해서 메소드를 호출할때 실제 메소드를 호출하지 않고 바로 결과값을 돌려주어, JVM(Java Virtual Machine)의 성능을 향상시킵니다.

class A {
  final int foo() { return 3; }
}


A라는 클래스(Class)가 있고 다른 클래스에서 에서 foo()라는 메소드를 호출하면 당연히 3이라는 값이 리턴됩니다. 왜냐하면 이 클래스는 final이라는 키워드 때문에 변경할 수 없는 값을 리턴하기 때문입니다. 특히 final이라는 키워드를 사용하는 경우 int foo()를 오버라이드 할 수 없습니다.

이에따라 Java HotSpot VM은 final int foo() 메소드를 상수 3으로 교체하여 성능을 향상 시킵니다.

HopSpot이 이와 같이 메소드를 상수로 교체하는 것을 메소드 인라이닝(Method Inlining)이라고 부릅니다.
메소드 인라이닝은 다음과 같은 효과가 있습니다.

  1. 메소드 호출(Method call)을 하지 않습니다.
  2. 동적인 디스패치(Dynamic dispatch)를 하지 않습니다.
  3. 값에 대한 상수 연산(constant-fold; 상수처럼 인식되어 바로 연산할 수 있도록하기 때문에 상수연산으로 번역합니다.)이 가능합니다. 예를 들어 “a.foo()+2” 의 결과값은 “5” 인데, 런타임(Runtime)시 a.foo() 메소드를 실행하지 않고 바로 “3 + 2” 연산으로 처리됩니다.


예전에는 이러한 이유로 프로그래머들이 종종 final이라는 키워드를 사용하였습니다. final이라는 키워드를 사용하여 실행 속도를 높이거나 큰 메소드안에서 작은 메소드를 통합하기 위하여 인라인 기능을 사용하였습니다.

하지만 이런 테크닉을 사용하면 프로그래밍 언어에서 완벽한 모듈화(modularization)를 할 수 없었으며, 재사용성(reusability)이 감소하게 됩니다.


하지만 Java HotSpot VM은 final이란 키워드가 없이도 메소드 인라이닝 테크닉을 사용할 수 있습니다.

class B {
  int foo() { return 3; }
}

이제 final이라는 키워드가 없으니 당연히 오버라이드가 잘 될 것입니다. 하지만 아래의 클래스 C와 같이 B 클래스를 상속하여 int foo() 메소드를 오버라이딩하는 경우는 어떻게 될까요? C 클래스 외에 D 클래스도 B 클래스를 상속받아 int foo() 메소드를 오버라이딩하였다면 어떻게 될까요?

class C extends B {
  int foo() { return 6; }
}


사실 B.foo()라는 인라이닝 메소드를 부르는 어떤 코드만 있다면 큰 문제는 없습니다. 왜냐하면 B 클래스는 다른 어떤 클래스로부터 상속받지 않았으며, int foo() 메소드 역시 오버라이딩 되어있지 않기 때문입니다.

가장 궁금한 경우는 다음과 같을 때입니다.

B bc2 = (B) c;
int numByBC2 = bc2.foo();
System.out.println("bc2.foo() is " + numByBC2);

결과 값이 3일까요 6일까요? 객체지향개념에 맞도록 메소드 인라인이 이루어 졌다면 당연히 6입니다. 실제로 테스트해본 결과 java version 1.6.0_07에서 정상적으로 6이 리턴되었습니다.


하지만 본 문서의 예처럼 단순한 경우만 있는 것이 아닙니다. 메소드 내에서 다른 메소드를 호출하는 등의 영향으로 결과값이 틀려질 때도 있습니다.

기본적으로 Java HotSpot VM은 이러한 문제를 해결하기 위하여 인라인 메소드 선택의 문제가 발생할 만한 부분을 감지하여 코드에 기록(the code makes)하여 놓습니다. 기록시에는 메소드 인라인의 결과값에 대한 가정(assumptions)을 기록합니다.

그리고 각 클래스들이 로딩되어 실행될 때 이러한 가정이 맞는지 확인합니다. 만약 경합이 발생하거나 다른 이유로 컴파일시의 가정이 맞지 않는다면 해당 메소드 인라인의 가정은 무시됩니다. 이를 디옵티마이즈(Deoptimized)되었다고 합니다.


제가 이해를 돕고자 예제를 만들어 보았습니다. 메소드 인라인에 대한 이해와 함께 상속에 대한 이해도 높이실 수 있을 것 같습니다.

메소드 인라인에 대하여 궁금하신 분들은 제가 첨부한 Java 파일을 다운로드하셔서 간단하게 테스트해보시기 바랍니다.



참고로 결과값은 다음과 같습니다.

 

Created A object!
Called foo() method in A object!
a.foo() is 1
-----------------------
Created B object!
Called foo() method in B object!
b.foo() is 3
-----------------------
Created B object!
Created C object!
Called foo() method in C object!
c.foo() is 6
-----------------------
Created B object!
Created C object!
Called foo() method in C object!
bc.foo() is 6
-----------------------
Called foo() method in C object!
bc2.foo() is 6

 

감사합니다. ;-)