자바 (Java) 퍼포먼스에 대한 미신들

Java Theory and Practice 시리즈 중 Urban Performance Legends, Urban Performance Legends, Revisited 그리고 제임스 고슬링의 언급을 요약했습니다. 아직도(!) 누군가 자바의 성능 문제를 이야기한다면, 이정도 지식과 퍼포먼스 프로파일링에 대한 기본적인 지식으로 충분하지 않을까 싶습니다.

Java 는 배열 인덱스 검사를 하기 때문에 느리다.

요즘 JVM 은 최적화를 통해 필요 없는 경우 인덱스 검사를 전혀 하지 않는다.

synchronized는 사용하지 않았을 때보다 50배 느리다.

JDK 1.0 시절에는 그랬겠지만 지금은 그렇지 않다. 어떤 컴파일러는 코드를 분석해 동기화가 필요 없을 경우 자동으로 동기화 코드를 제거하기도 한다. 그렇지 않다 하더라도 경쟁 상태에 있지 않은 경우 성능 저하는 크지 않다. 괜히 이 미신을 믿고 thread-safety 를 깨지 마라.

클래스나 메소드를 final 로 선언하면 성능이 향상된다.

전혀 근거 없다. 소프트웨어의 디자인만 희생시킨다.

Immutable 개체는 성능에 악영향을 준다.

Immutable 개채와 Mutable 개체의 성능 비교는 매우 어렵다. 개체에 전달될 데이터가 준비될 때까지의 과정이 큰 영향을 미치기 때문이다. 따라서 단정할 수 없고, 또 성능을 위해 소프트웨어 디자인을 희생시켜야 할 정도도 못된다.

자바 메모리 할당·해제는 C/C++ 보다 느리다.

정확히 그 반대다. Sun JVM은 new Object() 를 한 번 수행하는데 10개의 머신 인스트럭션이면 충분하지만 malloc 는 호출당 60에서 100 개의 인스트럭션이 필요하다. Perl 이나 GhostScript 같은 어플리케이션은 malloc 호출에 실행 시간의 20~30% 를 소모할 정도다. 잘 작성된 자바 어플리케이션의 할당·GC 시간을 다 합친 것이 더 효율적인 것으로 알려져 있다. 이는 다른 언어에 가비지 콜렉터 (Garbage Collector) 를 적용했을 때도 마찬가지다.

이는 malloc 의 경우 한 번에 필요한 양만큼의 메모리를 매 번 할당하는 반면, 가베지 콜렉터의 경우 한 번에 대량의 메모리를 할당하여 자유롭게 활용하기 때문이다. 일반적인 가비지 콜렉터는 힙 영역을 ‘young’ 과 ‘old’ 로 나누어 활용한다. 최초의 메모리 할당은 ‘young’ 세대 (generation) 에서 일어나고, ‘young’ 세대를 일정 시간 동안 살아남은 개체는 ‘old’ 세대 영역으로 넘어가게 된다. 그런데 가베지 콜렉터는 ‘young’ 세대 힙 영역의 절반만 사용하고, 나머지 절반은 사용하지 않는다. 대신 메모리 할당 코드는 최대한 단순하게 유지한다:

void *malloc(int n) {
  if (heapTop - heapStart < n)
    doGarbageCollection();

  void *wasStart = heapStart;
  heapStart += n;

  return wasStart;
}

마치 환형 큐 (Circular Queue) 의 동작 원리를 보는 듯 하다. 다만 사용하는 힙 영역이 힙 영역의 끝자락에 다다랐을 때에는 GC 가 일어나 성능 저하를 유발하게 된다. 대신 대부분의 상황에서 malloc 보다 월등한 성능을 보장한다. 또한 ‘young’ 세대에서의 해제 (deallocation) 비용은 ‘0′ 다. 마치 환형 큐에서 이미 꺼낸 데이터의 참조를 지우지 않았다가 나중에 단순히 덮어 씌우는 것에 비유할 수 있다.

대부분의 개체는 중간에 잠시 동안 사용되고 버려지고, 일부만이 ‘old’ 세대로 이동된다는 가정을 우리는 ‘세대 가설’ (generational hypothesis) 이라 부른다. 이 가설은 경험적으로 대부분의 개체 지향 언어에서 사실임이 알려져 있고, 따라서 가베지 컬렉터는 대부분의 개체에 대해 할당이 빠를 뿐 아니라 해제 비용도 거의 없다.

제임스 고슬링의 일화

번역하기 귀찮아서 원문으로 싣습니다. 간단히, -server 옵션만으로도 GCC 로 충분히 최적화된 C/C++ 어플리케이션에 필적한 성능이 나온다는 이야기입니다.

There was a funny incident at a recent developer event where some folks had a booth where they where demo-ing a high end industrial strength C compiler and had a benchmark that they had transliterated into Java. They were comparing their compiler to GCC and Java. GCC was running at about 2/3 the performance of this high end compiler; the Java version was running at about 2/3 the performance of the GCC version. Folks were gathered around the booth and someone noticed that the script they were using to run the Java version didn’t have optimisation turned on. A few seconds with vi to add the “-server” switch and Java’s performance jumped up to match the fancy C compiler. This got the pro-GCC crowd all excited, so a bunch of them started fiddling with its command line switches. They got a bit of improvement, but not much (the original selection had been pretty good).