본문 바로가기

JAVA

[JAVA] 자바 코드 튜닝 가이드

< 자바 코드 튜닝 가이드 I >

1. 메모리 낭비하는 거 없애기

2. 루프에서 똑같은 결과를 얻어내는 함수를 호출하거나 불필요한 new 하면 절대 안됨

3. 쓸데없이 array 쓰는 경우 없애기

4. Buffering은 아주 가끔 오래 끄는 메소드에서만 사용할 것. 그렇지 않으면 버퍼를 할당받는데 걸리는 시간이 더 걸림

5. synchronized 블럭에서 함수 호출할 때 주의할 것. (데드락)

6. String immutable . 이걸 모르고 그냥 넘기고, 연산하고 그러는 사람들 있는데 잊지 말 것

7. java.text.DateFormat 시리즈 함수들은 call stack을 보면 알겠지만 메모리 잡아먹는데 Best of Best. 조심.

8. java.util.Date도 자주 생성하면 안 좋음.

9. 개발 중에는 사용하지 말도록 해야겠지만, 튜닝시에는 2의 거듭승 들의 * / 연산은 모두 비트 연산하도록 변경 (하지만, 잦은 호출이 아니면 튜닝시에도 할 필요 없음).

10. 서버에서 Vector Hashtable method자체가 모두 synchronized로 선언되어있음. MT에 문제가 없는 사용이라면 ArrayList HashMap을 사용할 것.

11. 아무리 튜닝이라고 하지만, 지금 튜닝하는 코드는 내일 당장 상관이나 사장에 의하여 방향성의 변경이 생길 수 있음을 숙지하고, Refactoring을 고려. (반드시 그래야함)

12. 서버에서 Vector같이 사용하는 것이 있고, 이에 접근하는 빈도가 아주 크다면 List interface를 구현하지 말고, 직접 구현할 것. Object return되는 것들에 대한 Casting에 대한 귀찮은 일이 사라지고 Casting에 의한 오버헤드가 줄어듬. (몸으로 느낀 적은 없으나 책에서는 Casting도 안 좋다고 함)

13. 튜닝이 완료되었다고 생각하면, 일단 기뻐하고 그 후에는 Stress test를 반드시 한다. 내가 보통 하는 테스트는 해당 Unit에 대해서 100만번 루프를 돌린다.


< 자바 코딩 튜닝 II >

프로그램의 최적화 원칙

l 80/20 : 이것은 프로그램 수행 시간의 80%를 실제 프로그램 코드의 20%가 잡아먹는다는 것을 의미합니다. 때문에 실제 프로그램 코드 중 수행 시간의 80%를 차지하는 그 일부 코드를 찾는 일이 중요하다는 것입니다.

l 빠른 알고리즘: 같은 프로그램이라도 열 번 수행하는 프로그램을 한 번만 수행해도 되도록 코딩 한다면 훨씬 빠르겠죠. 평소에 다양한 로직으로 프로그램을 짜 보고, 어떻게 하면 더 간결하고 빠르게 돌아가는 코드를 만들 수 있을지를 많이 궁리해야 하겠습니다.

l 가벼운 데이터 구조: 리소스를 적게 사용하는 것이 훨씬 빠르게 작동한다는 것은 당연한 이치일 것입니다. 불필요하게 메모리를 많이 쓰는 코딩은 지양합니다.

l 가독성과 최적화: 프로그램의 최적화를 더 중요시 할지, 가독성을 더 중요시 할지를 잘 판단해야 합니다. 다음 예제 1은 가독성과 최적화의 딜레마라 볼 수 있습니다.

예제 1

x >> 2 또는 x / 4, x << 1 또는 x * 2

일반적 최적화 기법

l Strength reduction : 앞의 예제 1처럼 동일한 값이 나오지만 더 빠르게 작동하는 수식을 사용하는 것을 말합니다. Shift 연산은 비트 단위로 값을 이동하는 것이지만 좌로 1비트 이동하면 곱하기 2, 우로 1비트 이동하면 나누기 2가 됩니다. 이때 곱하기나 나누기보다 Shift 연산이 훨씬 빠릅니다.

l Common sub expression elimination : 불필요한 수식을 제거하는 것을 말합니다. 다음의 예제 2를 보면 d * (lim/max)의 계산이 2번 수행되지만, 예제 3 d * (lim/max)이 한 번 수행됩니다. 만약 이러한 계산이 복잡하고 오래 걸리는 것이라면 그 차이는 더욱 커지겠죠?

예제 2

double x = d * (lim / max) * sx;

double y = d * (lim / max) * sy;

예제 3

double depth = d * (lim / max);

double x = depth * sx;

double y = depth * sy;

l Code motion: 이는 변하지 않는 값을 가진 코드 부분을 반복 수행되는 곳의 바깥으로 이동시키는 것을 말합니다. 예제 4에서 Math 클래스의 PI, cos가 계속 반복해서 호출되는데, 예제 5에선 이것을 picosy 변수에 담아서 반복문 내부에서 곱해 더합니다. 이는 Math 클래스의 PI, cos가 계속 반복해서 호출 회수를 한 번으로 줄여줍니다.

예제 4

for (int i = 0; i < x.length; i++)

x[i] *= Math.PI * Math.cos(y);

예제 5

double picosy = Math.PI * Math.cos(y);

for (int i = 0; i < x.length; i++)

x[i] *= picosy;

l Unrolling loops: 반복문 내에서 한 번 이상의 연산을 수행함으로써 반복 회수를 줄여 반복 제어의 부하를 줄여 주는 것을 말합니다. 앞서 예제 코드 4에서 x배열은 항상 2의 배수이기 때문에 예제 6처럼 제어변수 i 2씩 증가시키면서 내부에서는 연산을 2번 수행합니다. 이는 결과적으로 연산 회수는 같으나 반복제어 회수가 줄어든다는 이점이 있습니다.

예제 6

double picosy = Math.PI * Math.cos(y);

for (int i = 0; i < x.length; i += 2) {

x[i] *= picosy;

x[i+1] *= picosy;

}

메쏘드 호출 감소

같은 프로그램이라도 메쏘드 호출이 잦으면 더 많은 연산을 수행하게 되며, 이는 실행 시간의 증가를 의미합니다. 게다가 자바에서는 동적인 메쏘드 호출을 지원하기 때문에 잦은 메쏘드 호출은 프로그램의 실행 속도를 더욱 저하시킵니다. 또한 메쏘드를 통해서만 데이터를 사용할 수 있는 데이터 캡슐화는 잦은 메쏘드 호출을 발생시켜 성능의 저하 요인이 됩니다.

이를 위해서는 컴파일시 인라인(inline)을 하는 -O 옵션을 사용합니다. 메쏘드 인라인은 코드 부분을 호출한 메쏘드로 이동시켜서 컴파일함으로써 메쏘드 호출의 부하를 줄여주는 것을 말합니다. 주의할 점은 static, private, final로 선언된 메쏘드만 인라인의 대상이 된다는 것입니다. static, private, final이라는 키워드를 가진 메쏘드는 정적으로 바인딩되어 인라인화될 수 있습니다. 메쏘드 인라인에 의한 메쏘드 호출 감소법을 살펴봅시다.

예제 7

public class InlineMe{

int counter=0;

public void method1(){

for(int i=0;i<1000;i++){

addCount();

System.out.println("counter="+counter);

}

public int addCount(){

counter=counter+1;

return counter;

}

public static void main(String args[]){

InlineMe im=new InlineMe();

im.method1();

}

}

}

예제 7 코드에서 addCount() 메쏘드를 다음과 같이 수정해 볼까요.

public void addCount(){

counter=counter+1;

}

이렇게 수정할 경우 addCount() 메소드는 컴파일시 인라인되어서 실제 메쏘드를 호출하지 않고 같은 결과를 반환한다. method1()이 실제 수행될 때는 예제 8과 같이 수행됩니다.

예제 8

public void method1(){

for(int i=0;i<1000;i++){

counter=counter+1;

System.out.println("counter="+counter);

}

}

객체를 적게 생성하자

임시 객체를 빠른 캐시에 저장하는 C C++와 달리, 자바에서는 메모리의 힙 영역에 객체를 저장합니다. 따라서 임시 객체를 생성할 때마다 힙에 액세스해야 하므로 속도가 느려집니다. 또한 임시 객체에 대한 가비지 컬렉션의 오버헤드가 큽니다. 이를 위해서는 다음과 같이 코딩할 것을 권장합니다.

l 될 수 있는 한 불필요한 임시 객체의 생성을 줄입니다.

l new를 통한 생성자 호출대신 static 메쏘드를 사용합니다. 다음의 예제 9에서는 Integer 클래스를 생성한 다음 string에서 정수 값을 추출해냈습니다. 예제 10에서는 Object Instance가 필요없는 static 메쏘드를 사용했습니다.

예제 9

String string="55";

int theInt=new Integer(string).intValue();

예제 10

String string="55";

int theInt=Integer.parseInt(string);

l 루프문이나 자주 불리워지는 메쏘드에서의 객체 생성을 피합니다. 다음의 예제 11에서는 반복문 내부에서 무려 1000번이나 Date라는 객체를 만들지만 예제 12에서는 한 개의 Date 객체에 계속 값을 할당하고 지우고 하는 식으로 사용합니다.

예제 11

for(i=0;i<1000;i++){

Date a=new Date();

................

}

예제 12

Date a;

for(i=0;i<1000;i++){

a=new Date();

.....

a=null;

}

l 디폴트 값으로 저절로 초기화가 되는 인스턴스 변수들을 프로그램에서 또다시 디폴트 값으로 초기화하여 객체 생성 시간을 더 늘리지 않도록 합니다.

필요할 때만 동기화

동기화를 위해서 쓰레드 모니터는 백그라운드로 계속 관리작업을 수행합니다. 이때 많은 경우 쓰레드들이 동기화된 메쏘드나 블럭에 들어가지 못하고 대기하고 있게 되어 수행시간이 길어지게 됩니다. 따라서 꼭 필요할 때가 아니면 동기화를 사용하지 않습니다. 동기화되지 않은 메쏘드를 호출하는 것이 동기화된 메쏘드 호출보다 10배 정도 빠릅니다.

보다 빠른 연산자의 사용

같은 일을 하더라도 더 빠른 연산자를 사용합니다. 증감연산은 ++ -- +1, -1보다 훨씬 빠르고, shift 연산이 곱셈이나 나눗셈보다 빠릅니다.

캐스팅의 지향

캐스팅을 하면 컴파일 시간에 그 타입이 결정될 수 없기 때문에 실행 시간을 느리게 만듭니다. 인터페이스를 캐스팅할 경우 더욱 많은 실행 시간을 필요로 하게 됩니다. 이를 위해서 같은 객체를 여러 번 캐스팅할 필요가 있을 경우 지역 변수에 저장해서 사용합니다. 캐스팅을 될 수 있는 한 피하고, 가장 빠른 변수 타입은 int이기 때문에 불가피한 경우를 제외하고는 int를 사용하는 것이 좋습니다.

빠른 변수 타입의 사용

변수의 성능은 그것의 범위와 타입에 의해서 결정됩니다. 가장 빠른 변수는 지역 메쏘드 변수이며, 가장 빠른 변수 타입은 int와 참조 변수입니다. 또한 어떻게 배열을 초기화시킬지가 중요한 요소가 됩니다. 다차원 배열로 정의할 경우 매번 생성자를 호출하기 때문에 꼭 필요한 경우가 아니면 다차원 배열로 정의하지 않습니다. 배열이 지역변수일 경우 메쏘드 호출시 매번 초기화를 수행하므로, 배열을 static으로 선언하면 초기화가 반복되는 것을 제거할 수 있습니다.

반복의 최적화

루프의 실행 시간은 얼마나 많이 루프 구문을 반복하느냐와 한 번 반복시 얼마나 많은 양의 일을 하는지에 따라 달라집니다. 루프에서의 인덱스는 로컬 int를 사용하는 것이 좋습니다. 또한 필요없는 메쏘드 호출이나 객체 생성을 루프문 안에서 사용하지 않습니다.

반복문 내에서 배열의 바운드를 체크해야 할 경우, try-catch 구문을 써서 ArrayIndexOutOfBounds Exception이라는 예외 상황(Exception)을 사용하면 비교를 수행할 필요가 없기 때문에 실행 시간이 줄어듭니다. 이는 배열의 바운드 체크가 비용이 드는 동작이기 때문입니다.

또한 반복문 내의 비교 상대값을 메쏘드 호출해서 계산해 오는 것이라면 다음처럼 미리 지역 변수에 그 값을 저장한 후에 그것을 사용하는 것이 좋습니다.

for(int k=0; k< s.length(); k++) 보다는

int limit = s.length(); for(int k=0; k < limit; k++)

스트링보다는 스트링 버퍼를 사용

자바에서 String은 한 번 생성되면 변하지 않는 성질(immutable)을 가지고 있습니다. 따라서 스트링들 사이에 + 연산을 수행하면 새로운 스트링을 생성하고, 양쪽 스트링의 내용을 복사한 후 앞의 스트링을 가비지 컬렉션합니다. 이에 따른 부하가 많아지므로 스트링의 연산이 필요할 경우, String 대신 고정적이지 않은 Stri ngBuffer를 사용하는 것이 좋습니다. 다음의 예제 13 + 스트링 연산의 예이며, 에제 14 StringBuffer를 이용한 append 연산을 통해 코드를 개선한 것입니다.

예제 13

String a="Hello";

a=a+"World";

System.out.println(a);

예제 14

StringBuffer a=new StringBuffer();

a.append("Hello");

a.append("World");

System.out.println(a.toString());

입출력 버퍼렁

즉 데이터를 입출력할 경우 Buffer를 사용해서 많은 양의 데이터를 내부 버퍼에 저장해서 사용하면 더욱 성능을 올릴 수 있습니다. InputStream을 리턴하는 호출을 할 경우, InputStream read()를 호출하고 이것은 하나의 byte나 문자를 리턴하는데, 이것에 대한 BufferedInputStream을 호출하면 이는 Input Stream과 똑같이 수행하지만, 많은 양의 데이터를 내부 버퍼에 저장함으로써 나중에 데이터를 읽어올 때 디스크 등과 같은 느린 소스로부터 데이터를 읽어올 필요가 없게 됩니다. 마찬가지로 BufferedOutputStream은 쓰여질 값을 버퍼에 저장했다가 버퍼가 가득찼을 때나 flush()가 호출되었을 때 실제로 쓰기 작업을 수행함으로써, 매번 직접 쓸 때 발생하는 오버헤드를 줄일 수 있습니다. 예제 15는 버퍼를 사용하지 않은 경우이고 예제 16은 버퍼를 사용한 코딩의 예입니다.

예제 15

InputStream in=null;
OutputStream out=null;

try{

in=new FileInputStream(from);

out=new FileOutputStream(to);

}

예제 16

InputStream in=null;

OutputStream out=null;

try{

in=new BufferedInputStream(new FileInputStream(from));

out=new BufferedOutputStream(new FileOutputStream(to));

}

객체 재사용

객체를 미리 필요한 만큼 준비해두고서 이를 관리하는 클래스를 통해서 얻어오는 것을 말합니다. 실제 프로젝트에서는 커넥션 풀을 많이 사용하는데 이것도 객체 재사용에 해당합니다. 예제 17과 같이 사용할 경우 하나의 인스턴스 변수를 사용하기는 하지만, 두 번의 초기화 과정을 거치게 됩니다. 예제 18을 보면 setLength라는 메쏘드를 통해서 객체를 초기화하지 않고서 계속 사용할 수 있습니다.

예제 17

StringBuffer sb=new StringBuffer();

sb.append("Hello");

out.println(sb.toString());

sb=null;

sb=new StringBuffer();

sb.append("World");

out.println(sb.toString());

예제 18

StringBuffer sb=new StringBuffer();

sb.append("Hello");

out.println(sb.toString());

sb.setLength(0);

sb.append("World");

out.println(sb.toString());

빠르고 세련되게 코딩하자

이번 연재에서는 자바 코드를 더욱 빠르고 세련되게 하기 위한 방법을 살펴보았습니다. 빠르고 세련된 코드는 곧 프로그래머의 습관입니다. 항상 머릿속에 두고서 어떻게 하면 더 간결하고 빠르게 작동할 수 있도록 하는 코딩을 할 것인가를 궁리하면 여러분들의 코드는 점점 나아지리라 확신합니다. 다음 연재에서는 자바 코드를 다시 사용하기 위한 방법, 즉 객체지향적으로 자바를 다루는 방법을 살펴보도록 하겠습니다.

정리 : 이종림 nowhere@sbmedia.co.kr

이 달의 숙제

지금까지 배운 것을 활용하는 프로그램을 만들어 봅시다.

l 여러분이 코딩한 프로그램을 최적화의 수행 시간을 측정해 보세요. 코드의 시작과 끝에 다음의 코드를 넣고 수행해 보세요.

long start = System.currentTimeMillis();

long end = System.currentTimeMillis() - start;

l 다음의 메쏘드를 사용하여 여러분이 코딩한 프로그램의 메모리 사용량을 측정해 보세요.

long freemem () {

System.gc();

return Runtime.getRuntime().freeMemory();

}