Sungwon Chung

Sungwon Chung@sungpi

Showing all posts tagged "C"

왜 항상 자바Java는 C++보다 느린가?

이 글은 Dejan Jelovic님의 글을 번역 한 글입니다. 수 많은 오역이 있을 수 있습니다. 피드백 부탁드립니다!

“자바Java는 높은 능률Performance을 가졌다. 그럭저럭 높은 능률. 그리고 그럭저럭 이란 느림을 뜻한다."
Mr. Bunny( http://www.mrbunny.com/ )

자바Java 프로그래밍을 해본 사람이라면, 자바Java로 짜여진 프로그램들은 C++로 짜여진 프로그램보다 느린 걸 안다. 이것은 자바Java를 사용하는 사람들이 받아들여야 하는 인생의 진리이다.

그러나 많은 프로그래머들은 이 것이 임시적인 상황일 것이라고 자위하고, 남들을 설득하곤 한다. 그들은 자바Java가 느리게 만들어진 것이 아니라고 한다. 대신에 오늘날의 JIT는 상대적으로 나온지 얼마 안되었고, 최적화가 잘 안되었기 때문이라고.

그 말은 틀렸다. JIT가 얼마나 좋아지건 간에, Java는 항상 C++보다 느릴 것이다.

The Idea

자바가 C++만큼 빠르거나 더 빠르다고 하는 사람들의 생각은 이렇다. 더 통제되는 프로그래밍 언어는 컴파일러가 최적화할 가능성을 더 열어준다. 그래서 모든 코드를 손으로 최적화 할 것이 아니라면, 전체적으로 컴파일러는 더 좋은 성능을 보여줄 것이다.

이 말은 옳다. Fortran은 더 통제되는 언어이기 때문에 계산에 있어서 C++을 완전히 발라버린다. Pointer Aliasing의 걱정 없이 컴파일러는 최적화를 더 잘 해준다. C++이 Fortran의 속도를 흉내낼 수 있는 방법은 영리하게 잘 만들어진 Blitz++같은 라이브러리를 이용하는 것 뿐이다.

그러나 이런 결과를 가져다 주기 위해서 애초에 프로그래밍 언어는 컴파일러에게 최적화의 가능성을 열어 줄 수있게 만들어 졌어야 한다. 공교롭게도 자바Java는 이런 방법으로 만들어지지 않았다. 그래서 컴파일러가 얼마나 똑똑해 지건 간에 자바Java는 C++의 속도를 절대 따라잡지 못할 것이다.

The Benchmarks

거꾸로, 자바Java가 C++ 만큼은 빠를 수 있는 분야는 전형적인 벤치마크를 할 때이다. 만약 N번째 피보나치 넘버를 계산하거나 Linpack(벤치마크 방법)을 돌려야 한다면, 자바가 C++보다 느릴 이유가 전혀 없다. 모든 계산이 한 클래스 안에서 이루어지고, int나 double같은 기본형Primitive 데이터만 다룬다면 자바는 C++의 발자국들을 따라 잡을 수 있다.

The Real World

프로그램에 오브젝트를 사용하는 순간부터 자바는 최적화를 할 가능성을 잃어버린다. 다음에서 그 이유들을 다룰 것이다.

1. 모든 오브젝트는 힙Heap에 할당된다.

자바는 int나 double같은 기본Primitive 자료형만 스택Stack에 할당한다. 모든 오브젝트는 힙Heap에 할당된다.

Identity semantic을 가지는 큰 오브젝트들의 경우에는 문제가 안된다. C++ 프로그래머들도 이런 오브젝트는 힙에 할당한다. 그러나 성능을 저하시키는 주요 이유는 단순 값만 들고 있는 (Value semantics) 작은 오브젝트들의 경우이다.

이런 작은 오브젝트가 무엇인가? 나에게 이것들은 이터레이터들이다. 나는 그들을 내 디자인(코드)에 많이 쓴다. 어떤 사람들은 복잡한 숫자를 쓸 수도 있다. 예로 3D 프로그래머들은 벡터Vector나 포인트Point 클래스를 쓴다. 시간에 연계된 자료를 다루는 사람들은 Time클래스를 쓴다. 이런 자료형들을 쓰는 사람들은 비용 0의 스택할당 보다 상수 시간이 걸리는 힙 할당을 쓴다. 이 것들로 루프를 돌리면 O(n)과 상수 0의 차이가 난다. 다른 루프를 추가하면 O($n^2$)과 상수 0의 차이가 난다.

2. 엄청 많은 형변환Cast.

템플릿Template의 등장으로, 뛰어난 C++ 프로그래머들은 high-level 프로그램에도 형변환Cast를 최대한 피할 수 있었다. 공교롭게도 자바는 템플릿Template이 없고, 자바 코드는 형변환Cast으로 넘쳐난다.

이 것은 성능에 무슨 영향을 끼칠까? 자바의 모든 형변환은 동적Dynamic 형변환이고, 비싼 비용을 지불해야 한다. 얼마나 비싼지 동적 형변환을 예로 들어 보겠다.

가장 쉬운 예는 클래스에 번호를 할당하고, 두 클래스가 연관되었는지를 판단하는 행렬을 만들고, 만약 그렇다면 offset은 형변환을 하기 위해 포인터에 추가되어야 될 것이다. 그 경우에, 형변환을 위한 수도코드는 다음과 같다 :


DestinationClass makeCast (Object o, Class destinationClass)
{
     Class sourceClass = o.getClass(); // JIT compile-time
     int sourceClassId = sourceClass.getId(); // JIT compile-time

     int destinationId = destinationClass.getId();

     int offset = ourTable [sourceClassId][destinationClassId];
     if (offset != ILLEGAL_OFFSET_VALUE)
     {
          return ;
     }else throw new IllegalCastException();
}

작은 형변환을 위해 너무 많은 코드를 쓴다. 그리고 여기에 장밋빛 그림이 있다 - 클래스의 관계를 나타내기 위해 행렬을 쓰는 것은 너무 많은 메모리를 잡아먹고 미치지 않은 컴파일러라면 이 짓을 할리가 없다. 대신에 그들은 Map이나 상속 계층을 뒤질 것이다. - 이 두 경우에는 계산이 더 느리다.

3. 메모리 사용량의 증가

자바 프로그램들은 C++ 프로그램들이 데이터를 저장하는 것의 거의 2배를 사용한다. 이는 다음과 같은 세 가지 이유가 있다.

    1. GC를 켜놓은 프로그램들은 직접 손으로 메모리 관리를 하는 프로그램보다 메모리를 50% 정도 더 쓴다.
    1. C++에선 많은 오브젝트들이 스택에 할당 될 때, 자바에서는 힙에 할당 된다.
    1. 자바의 오브젝트는 더 큰데, 모든 오브젝트가 virtual table을 가지는 데다가 synchronization primitive를 지원하기 때문이다.

큰 메모리를 사용하는 프로그램들은 프로그램이 디스크에서 스왑 아웃될 확률을 증가시킨다. 그리고 스왑 아웃이 된다면, 속도는 말할 것도 없다.

4. 디테일을 다루는 컨트롤의 부재

자바는 의도적으로 심플한 언어로 설계되었다. C++은 프로그래머에게 디테일을 다룰 수 있는 많은 기능들을 제공한다. 이 기능들은 자바에서 의도적으로 삭제되었다.

예를 들어 C++은 Locality of reference를 향상시킬 수 있는 기능을 제공한다. 많은 오브젝트들을 한 번에 할당하고, 해제하는 기능도 제공한다. 포인터로 멤버를 빠르게 접근할 수 있는 트릭도 제공한다...

하지만 이런 기능들을 자바에 없다.

5. High-level 최적화의 부재

프로그래머들은 high-level 개념들을 다룬다. 반대로 컴파일러는 low-level만을 다룬다. 프로그래머에게 Matrix라고 이름지어진 클래스는 Vector라고 이름 지어진 클래스와 다른 high-level 개념이다. 컴파일러에게 이들은 심볼 테이블Symbol Table의 일부일 뿐이다. 컴파일러가 신경쓰는 것은 그 클래스가 가지는 함수들과, 그 함수들 안의 구문들 뿐이다.

이제 이걸 생각해보자 : $x^y$를 반환하는 exp(double x, double y)라는 함수를 사용한다고 생각해보자. 컴파일러는 이 구문들을 들여다 보는 것만으로 exp(exp(x,2), 0.5)가 x로 최적화 될 수 있는 걸 알 수 있을까? 당연히 아니다.

컴파일러가 할 수 있는 모든 최적화는 구문 단계에서 이루어지며, 그들은 컴파일러 안에 쓰여져 있다. 위의 예제에서 가 exp(exp(x,2), 0.5)가 x와 같다는 것을 알거나 코드 안에서 함수를 부르는 순서가 잘못되었다는 걸 프로그래머는 안다. 하지만 컴파일러는 이걸 구문 레벨에서 뜯어낼 수 없기 때문에, (컴파일러가 구문 단계에서 이걸 뜯어 낼 수 있게 되기 전까지는) 최적화는 되지 않을 것이다.

그래서 만약 high-level 최적화가 되길 바란다면, 프로그래머가 컴파일러에게 high-level 최적화 조건들을 알려줄 수 있는 방법이 있어야 한다.

유명한 프로그래밍 언어와 시스템 중 이런 방법을 가진 것은 없다. 그러나 C++에서는 템플릿 메타프로그래밍으로 high-level 오브젝트들의 최적화를 해낼 수 있다. Temporary elimination, partial evaluation, symmetric function call removal 그리고 더 많은 다른 최적화들은 템플릿을 이용함으로써 구현할 수 있다. 당연히 모든 high-level최적화가 이런 방식으로 되는 것은 아니다. 그리고 이런 것들을 사용하는 것은 어렵고 귀찮다. 그러나 상당히 많은 수가 해결될 수 있으며, 사람들을 이미 이런 기술을 이용해 snazzy libraries를 구현 했다.

다시 한 번 공교롭게도, 자바에는 메타프로그래밍 설비가 되어있지 않다. 그러므로 (이를 이용한) high-level 최적화는 자바에서 불가능하다.

So...

지금까지의 기능을 볼 때 자바는 결코 C++처럼 빠를 수 없다. 이 것은 곧 자바가 고성능 소프트웨어나 COTS arena에 적당하지 않다는 것을 의미한다. 그러나 자바는 배우고 쉽고, 관대하고, 이용하기 쉬운 방대한 라이브러리가 있어서 작거나 중간 정도의 크기의 프로그램을 만드는 데 적당하다.

역주

정확한 번역보다 읽기 쉽게 의역하려고 노력 했습니다.

번역 : 정성원(sungpia@me.com)

Citation

  • Jevolic, D. (n.d.). Why Java Will Always Be Slower than C. Retrieved April 15, 2015, from http://www.jelovic.com/articles/why_java_is_slow.htm

Type Lattice

C++ 에서의 자료형type 을 크게 네 가지로 생각해 보면 다음과 같다:

  • bool
  • char
  • int
  • double

이 네 가지 자료형Type 에는 우선 순위가 있다. 이를 Type Lattice라고 한다.
자료형 변환Cast을 할 때나, 연산을 할 때 이 우선 순위가 적용된다:

bool to char
bool to int
bool to double
char to int
char to double
int to double
위와 같은 경우는 자료형을 변환할 때 안전하다고 한다. (Safe conversion)
이 외의 경우에는 안전하지 않다고 한다. 이는 자료형Type안의 값Value가 파괴될 수 있기 때문이다. C++에서는 변수를 선언할 때나 변환할 때, { } 를 사용함으로써 컴파일 단계에서 이 안전하지 않은 변환에서 에러를 낼 수 있다. 그리고 이 에러를 Narrowing이라고 한다.


int a{4.2};
int b = 4.2;
1 + 2.5;

이 코드는 int a에 double 값인 4.2가 들어감으로써 narrow error를 발생시킨다.
반면, int b = 4.2는 b에 4를 넣어주는 강제 형변환이 된다.

연산

연산 시에는 Type Lattice에 따라, 안전하게 변환이 되게 컴파일러가 계산을 한다.
예를 들어 1 + 2.5 는 int와 double의 +연산이다. 이 경우에 int + int로 계산하거나, double + double로 계산하는 두 가지 경우가 있다. 전자는 int + double->int의 연산이고, 후자는 int->double + double의 연산이다. 전자는 double->int가 unsafe conversion이고, 후자는 int->double이 safe conversion이므로 컴파일러는 자동으로 후자를 택해서 연산을 하게 된다.