IBM Korea Skip to main content
       IBM 홈    |  제품 & 서비스  |  고객지원 & 다운로드  |  회원가입  

자바 원시 컴파일의 무게 재기
목 차:
코드 컴파일의 기초
테스트 설정에 관해
테스트 1: Prime.java
테스트 2: SciMark 2
장단점
결론
참고 자료
필자 소개
기사에 대한 평가
관련 dW 링크:
COM과의 차이 메꾸기
통합된 자바와 C/C++ 코드 디버깅하기
US 원문 읽기
자바 소스에서 원시 코드를 생성할 때의 장단점


Martyn Honeyford
소프트웨어 엔지니어, IBM UK Labs
2002년 1월

자바 원시 컴파일이 처음 소개되었을 때 자바 플랫폼의 대항하기 어려운 플랫폼 독립성을 고려하더라도 분명히 JVM을 앞설 것처럼 보였다. 그러나 인기가 높아지고 점점 더 많은 수의 원시 컴파일러가 시장에 나오고 있지만, 원시 컴파일이 자바 코드의 이식성에 실질적인 위협을 가하려면 갈 길이 멀다. 그리고 불행히도 현재 우리 중 많은 사람이 고군분투하고 있는 자바 성능 문제를 해결할만큼 기술이 성숙하려면 또 어느 정도의 시간이 걸릴 것이다.

많은 면에서 높은 점수를 받음에도 불구하고 자바 언어를 주요 프로젝트에서 제외되도록 하는 몇 가지 문제들이 여전히 존재한다. 여기에는 실행 속도, 메모리 용량, 디스크 용량 및 JVM의 가용성이 포함된다. JIT 컴파일러는 플랫폼의 실행 속도 향상에 크게 기여했고 J2ME는 자바 플랫폼의 메모리 필요량을 줄였지만, 많은 영역에서 자바 애플리케이션은 자신의 원시 (보통 C/C++) 대응자들과는 경쟁이 안 된다. 이러한 문제들을 해결하기 위해, 많은 개발자들이 자바 언어로 애플리케이션을 작성한 후 원시 실행 파일로 컴파일하도록 해주는 자바 원시 컴파일러를 사용해 왔다. 이 솔루션은 플랫폼 독립성이라는 면은 희생시키겠지만, 오늘날 많은 애플리케이션에서 필수적인 보다 신속한 실행과 보다 적은 용량 요구를 가져올 수 있다.

여러분이 자바 원시 컴파일 기술에 관해 빨리 이해하도록 하기 위해, 먼저 코드 컴파일의 기초 사항들을 살펴 보겠다. 여기에서 왜 많은 개발자들이 자신의 애플리케이션에 자바 원시 컴파일러를 채택하고 있는지도 간단히 설명하겠다. 다음에 우리는 무료 소프트웨어 컴파일러와 두 개의 다른 애플리케이션 (하나는 간단하고, 다른 하나는 더 복잡함)을 이용하여 자바 원시 컴파일의 결과를 테스트할 것이다. 이 예제들과 결과로 나오는 도표는 최신 자바 원시 컴파일러가 JVM과 어떻게 비교되는지를 직접 보여줄 것이다.

코드 컴파일의 기초

이 글의 설명을 따라가기 위해 여러분은 다음 세 개의 가장 일반적인 코드 컴파일 방법에 친숙해야 한다. :

  • javac와 같은 자바 컴파일러로 자바 코드 컴파일하기

  • 특정 하드웨어/운영 체제 (OS) 플랫폼을 목표로 한 C/C++ 과 같은 원시 코드 컴파일하기

  • 특정 하드웨어/운영 체제 (OS) 플랫폼을 목표로 한 자바 원시 컴파일러로 자바 코드 컴파일하기

자바 컴파일러를 사용해 자바 코드를 컴파일하는 것은 쉽다. 간단하게 자바 언어로 소스 코드를 작성한 후 자바 컴파일러를 사용해 소스를 자바 바이트코드로 컴파일하고 그 결과를 JVM이 설치되어 있는 모든 하드웨어/OS 플랫폼에서 실행시키면 된다. 상징적인 "한 번 작성하여 어디서든 실행된다"는 이식성을 위해 자바가 JVM에 의존하는 것은 자바의 단점이다. 여러분이 자바 애플리케이션을 실행시키고자 하는 어떤 플랫폼에서도 JVM이 사용 가능해야 할 뿐 아니라, 그 JVM을 지원하려면 상당한 시스템 자원 (메모리 및 디스크 공간)이 있어야 한다. 그 결과 많은 개발자들이 C/C++과 같이 유연성은 낮지만 더 구체적인 타겟이 정해진 언어에 계속해서 의존하고 있다.

C/C++로 된 소스를 컴파일하는 것은 자바에서와 비슷하다. 일단 코드가 작성되면 특정 하드웨어/OS 플랫폼을 목표로 한 컴파일러와 링커(linker)를 통해 이를 실행시킨다. 그 결과 나오는 애플리케이션은 목표로 한 플랫폼에서만 실행되지만, JVM이 설치되어 있지 않아도 된다 (사용되는 언어에 따라, 몇몇 공유 라이브러리의 지원을 필요로 할 수도 있겠지만). 가장 간단한 애플리케이션을 제외하고는 이 기법을 사용해 개발된 모든 애플리케이션은 여러분이 실행시키고자 하는 하드웨어/OS 플랫폼 각각에 맞게 개별적으로 수정되어야 한다.

세번째 방법은 개발자들이 애플리케이션을 자바 언어로 작성한 후 이를 원시 실행 파일로 컴파일하도록 함으로써 위 솔루션들의 장점을 결합시키려고 시도한다. 일단 자바 코드가 작성되면 자바 컴파일러를 통해 실행되어 자바 바이트코드를 만들고, 이 바이트코드가 원시 코드로 컴파일된다. 혹은 자바 원시 컴파일러로 바로 실행될 수도 있다. 관련된 단계의 수는 사용하는 컴파일러의 요구 사항에 달려 있다.

이 방식의 장점은 결과로 나오는 코드가 목표 플랫폼에서 JVM 없이도 실행될 수 있다는 것이다. 이 방식은 실행 속도는 많이 향상되고 실행에 필요한 디스크 공간과 메모리는 상당히 줄어든 자바 애플리케이션 개발을 지향한다 (자바 원시 컴파일러에 대한 지원 라이브러리들을 제공해야 할 수도 있지만).

컴파일러는 목표로 하는 플랫폼, 자바 지원 수준 및 사용하는 시스템 자원의 양에 따라 다양하다. 참고 자료에서 현재 사용 가능한 원시 컴파일러 일부의 목록을 볼 수 있다.

테스트 설정에 관해

시장에 나와 있는 모든 원시 컴파일러의 기능과 성능을 비교하는 것은 이 글의 범위를 벗어난다. 대신 나는 원시 컴파일의 과정과 결과를 상세히 설명하기 위한 예제로 GNU Compiler for the Java Programming Language (GCJ)를 사용하였다. GCJ는 GNU 프로젝트의 일부인 GNU Compiler Collection (GCC)을 위해 개발된 컴파일러 중 하나이다. GNU 프로젝트에서 나온 모든 소프트웨어와 마찬가지로 GCJ는 무료 소프트웨어이고 따라서 쉽게 얻을 수 있다 (참고 자료). 여러분의 제품에 원시 컴파일 방식을 심각하게 고려하고 있다면 가능한 한 많은 컴파일러를 평가해 보아야 하는데, 아마도 이 글에서 규정한 기준을 사용할 수 있을 것이다.

나의 테스트용 시스템 하드웨어는 450Mz로 작동하는 Pentium II 프로세서를 장착하고 320 MB의 메모리를 가진 한 대의 PC로 구성되어 있다. OS는 최근 설치한 Mandrake 8.1 리눅스 배포판이다. 이 배포판은 GCC 3.0.1에 포함되어 있고 8.1 Mandrake 배포판의 일부로 출하되는 GCJ 3.0.1과 함께 제공된다.

나는 두개의 개별적인 애플리케이션을 실행시키는데, 하나는 매우 간단한 것이고 나머지 하나는 좀 더 복잡하다. 자바 플랫폼의 성능에 대비해 시스템 성능을 비교하기 위해 나는 애플리케이션들을 자바 바이트 코드로 컴파일하였다. Sun JDK version 1.3.1.02 for Linux를 사용해 자바 코드를 컴파일한 후 그 결과 나온 클래스를 다음의 JVM들에서 테스트하였다. :

  • Kaffe 1.0.6

  • Sun JVM 1.3.1_02

  • IBM JRE 1.3.1

이 글의 목적상 나는 실행 속도, 메모리 부하와 디스크 공간을 측정하였다.

테스트 1: Prime.java

첫번째 테스트 애플리케이션은 prime.java라는 하나의 클래스로 구성된 매우 간단한 것이다. 이 애플리케이션은 소수를 검색하는 매우 기본적인 알고리즘을 구현한다. Listing 1은 prime.java의 소스 코드이다.

Listing 1. prime.java의 소스

import java.io.*;
class prime 
{
   private static boolean isPrime(long i)
   {
       for(long test = 2; test < i; test++)
       {
	   if(i%test == 0)
	   {
		return false;
	   }
       }       
       return true;
   }

   public static void main(String[] args) throws IOException 
   {
       long start_time = System.currentTimeMillis();

       long n_loops =  50000;
       long n_primes = 0;

       for(long i = 0; i < n_loops; i++)
       {
	   if(isPrime(i))
           {
	       n_primes++;
           }
       }
   
       long end_time = System.currentTimeMillis();

       System.out.println(n_primes + " primes found");       
       System.out.println("Time taken = " + (end_time - start_time));
   }
}

여러분이 볼 수 있듯이, 코드는 0에서 50000까지 반복한다. 이 코드는 반복하며 만나는 각 수를 그 자신이 될 때까지의 모든 수로 일일이 나누어 나머지가 있는지 확인하고자 한다. (이것은 소수를 찾아내기 위한 억지스러운 기법이지만, 예제로는 충분할 것이다.)

나는 다음의 명령문으로 prime.java를 원시 실행 파일이 되도록 컴파일하였다:

gcj prime.java -O3 --main=prime -o prime

-03 인자는 "속도를 위해 최적화한다"는 의미이다.; --main 인자는 GCJ에게 애플리케이션이 실행될 때 사용되는 메인 메소드를 어떤 클래스가 포함하고 있는지 알려준다.; -o Prime 인자는 결과로 나오는 실행 파일을 명명한다. 명령행에서 쓸 수 있는 모든 인자를 보려면 GCJ 문서를 참조한다.

자바 바이트코드 테스트를 컴파일하기 위해 다음 명령어를 사용하였다:

/usr/java/jdk1.3.1_02/bin/javac -O prime.java

그리고 나서 다음 명령어를 사용하여 우리의 테스트 JVM 각각에 대해 코드를 호출하였다.:

  • Native: ./prime

  • Kaffe: /usr/bin/java prime

  • Sun JDK: /usr/java/jdk1.3.1_02/bin/java prime

  • IBM JRE: /opt/IBMJava2-13/jre/bin/java prime

prime.java의 테스트 결과

앞에서 언급했듯이, 나는 실행 속도, 메모리 사용량 및 디스크 공간 사용량을 테스트하였다. 다음 표는 첫번째 테스트 결과를 상세하게 보여 준다.

표 1. Prime.java: 실행 속도

구현 시간 (단위: msec, 3회 실행 평균, 낮을수록 좋다)
Native 40180
Kaffe 75456
Sun JDK 67315
IBM JRE 18188

표 2. Prime.java: 메모리 사용량

구현 VM 사이즈 (KB) VM RSS (KB)
Native 7024 3528
Kaffe 8888 3564
Sun JDK 169560 6636
IBM JRE 81936 6288

VM 사이즈가 프로세스 이미지의 전체 사이즈와 같다는 점에 주의하다. 여기에는 스와핑된 페이지들을 포함해 모든 코드, 데이터, 프로세스가 사용하는 공유 라이브러리가 포함된다. VM resident set size (RSS)는 공유 라이브러리를 포함해 실제로 RAM에 상주하는 프로세스 부분 (코드와 데이트)의 사이즈와 같다. 이는 한 프로세스가 얼마나 많은 RAM을 사용하고 있는지 적정하게 추정할 수 있게 해준다.

간단히 말해, 한 프로세스가 대량의 메모리를 할당하면 그것이 VM 사이즈에 나타나지만 실제로 사용될 때까지는 (예 : 읽기나 쓰기) VM RSS에는 나타나지 않을 것이다. VM RSS는 실제로 보다 중요한 척도이다. 시스템의 성능 상황을 더 잘 나타내기 때문이다.

표 3. Prime.java: 디스크 공간 사용량

구현 총 컴파일 사이즈 (bytes)
Native 22268
Java classes 962

표 3에 나타난 측정치는 공유 라이브러리와 JVM을 제외하고 실행 파일만 빼내어 측정되었다.

테스트 2: SciMark 2
두번째 테스트를 위해 나는 좀 더 복잡한 자바 애플리케이션인 SciMark 2 자바 벤치마크를 채택하였다. 이 글에서 사용된 명령행 버전을 무료로 얻을 수 있다. (참고 자료) SciMark 2는 아주 복잡한 애플리케이션으로, JVM의 효율을 정확하게 측정하기 위한 많은 수의 벤치마크들을 구현하고있다.

나는 다음 코드를 사용하여 SciMark 2를 원시 실행 파일로 컴파일하였다:


gcj-3.0.1 -O3 commandline.java Random.java FFT.java SOR.java Stopwatch.java 
  SparseCompRow.java LU.java kernel.java MonteCarlo.java 
    --main=jnt.scimark2.commandline -o scimark

그리고 다음 코드를 사용하여 애플리케이션을 자바 바이트코드로 컴파일하였다:


/usr/java/jdk1.3.1_02/bin/javac -O *.java

SciMark 2 벤치마크는 일반 모드와 대형 모드의 두 모드로 실행될 수 있다. 사용하는 모드는 사용되는 문제 세트의 크기를 결정한다. 나는 두 모드 모두로 테스트를 실행시켰다.

일반 모드에서 코드를 호출하기 위해 다음 명령어를 사용하였다:

  • Native: ./scimark

  • Kaffe: /usr/bin/java jnt.scimark2.commandline

  • Sun JDK: /usr/java/jdk1.3.1_02/bin/java jnt.scimark2.commandline

  • IBM JRE: /opt/IBMJava2-13/jre/bin/java jnt.scimark2.commandline

보다 큰 문제 세트에 대해서는 다음 명령어를 사용하였다:

  • Native: ./scimark -large

  • Kaffe: /usr/bin/java jnt.scimark2.commandline -large

  • Sun JDK: /usr/java/jdk1.3.1_02/bin/java jnt.scimark2.commandline -large

  • IBM JRE: /opt/IBMJava2-13/jre/bin/java jnt.scimark2.commandline -large

SciMark 2의 테스트 결과

다음 표는 SciMark 2를 컴파일한 결과이다. 일반 모드와 대형 모드에서의 결과의 차이에 주목한다.

표 4. SciMark 2, 일반 모드 : 실행 속도

구현 복합 점수 (3회 실행 평균, 높을수록 좋다)
Native 15.22
Kaffe 7.01
Sun JDK 22.86
IBM JRE 25.29

표 5. SciMark 2, 일반 모드 : 메모리 사용량

구현 VM size (KB) VM RSS (KB)
Native 9788 5956
Kaffe 8888 4092
Sun JDK 169692 7428
IBM JRE 81964 7408

표 6. SciMark 2, 대형 모드 : 실행 속도

구현 복합 점수 (3회 실행 평균, 높을수록 좋다)
Native 8.78
Kaffe 5.72
Sun JDK 12.04
IBM JRE 15.04

표 7. SciMark 2, 대형 모드 : 메모리 사용

구현 VM 사이즈 (KB) VM RSS (KB)
Native 62888 59072
Kaffe 58056 56988
Sun JDK 169692 64624
IBM JRE 81964 57704

표8. SciMark 2: 두 모드에서의 디스크 공간 사용량

구현 컴파일 사이즈 Compiled size (bytes)
Native 49588
Java Classes 16318

다시 한번 말하지만, 표 8에 나타난 측정치는 공유 라이브러리와 JVM은 제외하며 실행 파일만 빼내어 측정되었다.

원시 컴파일의 장단점

위의 테스트 결과에서도 분명히 나타나지만, 자바 원시 컴파일이 성공이나 실패냐는 명확하지 않다. 몇 가지 벤치마크는 원시 코드로 컴파일된 실행 파일이 JVM 버전에서보다 빠르다는 것을 보여주었다.; 그러나 다른 것은 늦었다. 마찬가지로 일부 작업의 속도는 다른 JVM간에 차이가 아주 많이 난다."working set" 메모리 테스트를 수행한 결과 실행시의 메모리 사용량에는 큰 차이가 없는 것으로 나타났다. 원시 테스트와 JVM 테스트 양쪽에 서로 다른 가비지 컬렉션 방법을 채택한 테스트가 이 영역을 더 잘 보여줄 수 있다.

디스크 공간 면에서는 원시 버전이 JVM 버전보다 확실한 승자였는데, 이 사실은 JVM의 사이즈를 고려할 때만 적용된다. 클래스 자체는 아주 작지만 테스트된 JVM은 컸다. (IBM과 Sun JVM의 jre 하위 디렉토리에서의 순환적인 디렉토리 목록은 JRE만도 50MB가 넘는 디스크 공간을 차지한다는 사실을 보여주었다.) 그러나 보다 작은 JVM을 이용할 수 있고, JVM과 한 애플리케이션과의 결합이 원시 실행 파일과 GCJ 런타임 라이브러리인 libgcj.so (3MB 이하)의 결합보다 훨씬 크지만 원시 버전의 실행 파일 사이즈가 훨씬 크다는 점을 염두에 두기 바란다. 따라서 많은 애플리케이션이 필요한 상황에서는 JVM 버전이 최종적으로 승자가 될 것이다.

다소 불분명한 이 결과에 덧붙여, 자바 원시 컴파일러를 사용하면 다음과 같은 많은 잠재적인 문제가 발생할 수 있다:

  • 플랫폼 독립성의 상실 : 사실 이것은 그렇게 큰 문제는 아니다. 소스가 자바 언어로 작성되었기 때문에, 여러분은 여전히 어디에서든 실행될 수 있는 자바 바이트 코드 버전을 만들어낼 수 있고 필요할 때는 특정 플랫폼에서 원시 컴파일러를 사용할 수 있다.

  • 클래스 지원/컴파일러 성숙도 : 몇몇 컴파일러는 아직 비교적 미성숙한 상태이고 여러분 애플리케이션이 요구하는 모든 자바 클래스를 지원하지 않을 수도 있다. 예를 들어, GCJ는 사양 v.1.1까지 대부분의 자바 언어 생성자를 지원하지만, 보통 JVM과 함께 출하되는 모든 자바 클래스 라이브러리를 지원하지는 않는다. 가장 뚜렷한 것이 AWT에 대한 지원이 거의 없다는 것인데, 따라서 GCJ는 GUI 애플리케이션에서는 부적당하다. 컴파일러마다 지원하는 클래스 라이브러리의 수준이 다르다. Excelsior JET는 AWT와 Swing을 완벽하게 지원한다고 주장하는 한 컴파일러이다.

  • 지원/복잡성 : 이 문제는 비교적 새로운 분야라서 개발자들이 잘 이해하지 못하는 경우가 많다. 진단 툴은 현장에서는 다소 약해서 원시적으로 컴파일된 자바 애플리케이션에서 등장하는 문제들을 진단하는 것이 더욱 어려워질 가능성이 있다. (에러가 자바 바이트코드 버전에서는 나타나지 않을 경우 특히 더 그러하다.)

결론
애플리케이션 개발에 있어 일반적으로 그러하듯이, 자바 원시 컴파일이 여러분의 특정 환경에 대한 해답인지를 실제로 결정하는 유일한 방법은 문제 해결 사이클을 시행해 보는 것이다:

  1. 원시 컴파일러로 정확히 어떤 문제 (혹은 문제들)을 해결하고자 하는지를 결정한다.

  2. 사용 가능한 원시 컴파일러들을 살펴보고 문제를 해결할 수 있을 것으로 보이는 몇 가지를 선택한다.

  3. 선택한 컴파일러 모두를 여러분 애플리케이션으로 시험해 보고 결과를 살펴 본다.

기술이 비교적 미성숙하고 명확한 결과를 보여주지는 못하지만, 자바 원시 컴파일은 자바 언어에서 흥미로운 새 분야이다. 기존의 옵션을 이용하는 최상의 방법은 이들을 직접 조사하고 테스트해 보는 것인데, 아마도 이 글에서 제시한 기법과 기준을 사용할 수 있을 것이다.

원시 컴파일은 많은 사람들이 생각하듯이 JVM 킬러는 아니지만, 일부 애플리케이션과 환경에 적합한 솔루션임이 판명되었다. 원시 컴파일은 자바 언어를 불과 몇 년전까지는 적용될 수 없었던 영역에서 사용되도록 확장시켰다. 이는 자바 언어와 자바 그룹에 전체적으로 좋은 일이라 할 수 있다.

참고 자료

엄선된 자바 원시 컴파일러

필자소개

Martyn Honeyford은 1996년 Nottingham 대학에서 컴퓨터 과학 학사 학위를 취득했고, 이 후 영국 Hursley 소재 IBM UK Labs에서 소프트웨어 엔지니어로 쭉 일해 왔다. 현재 WebSphere MQ Everyplace 개발팀에서 개발자로 일하고 있다.



이 기사에 대하여 어떻게 생각하십니까?

정말 좋다 (5) 좋다 (4) 그저그렇다 (3) 수정보완이 필요하다(2) 형편없다 (1)

  회사소개  |  개인정보 보호정책  |  법률  |  문의