by 잭 시라지, Java Performance Tuning의 저자
자바에 있는 컬렉션을 질의로 만드는 것은 대부분의 애플리케이션에서 실행되는 일이다. 애플리케이션에서 특정 질의가 병목현상이 일어난다면, 질의를 어떤 식으로 최적화하여 속도를 더 빠르게 할 것인가? 이 기사에서는 일반적으로 혼잡함을 일으키는 것을 해결하려면 어떻게 해야 하는지에 대한 예를 제공할 것이다. 목록에 있는 질의의 퍼포먼스를 개선하려면 튜닝을 거쳐야 한다. 필자가 쓴
Java Performance Tuning에서 표준 튜닝 기술에 대해 설명해 놓았으니 참고하기 바란다.
질의(query)
먼저, 문제를 통해 살펴보도록 하자. 문자열 컬렉션을 담기 위해 목차가 있는 컬렉션을 사용하겠다. 질의에서 나는 어떤 특정 문자열이 특정한 하위 문자열 집합을 포함하는지 체크하기 위해 간단한 테스트를 할 것이다. 질의에서는 단순히 얼마나 많은 문자열에서 그러한 하위 문자열을 포함하는지만을 반환할 것이다. 예를 들어서, 다음과 같은 목록이 있다고 하자.
- "code"
- "rode"
- "load"
- "toad"
- "road"
그리고 "목록에서 "od"나 "lo"가 들어있는 것은 몇 개인가"라고 질의했다고 하자(이 예제에서 답은 3이라고 나올 것이다).
| |
|
| 자바에 있는 컬렉션을 질의로 만드는 것은 대부분의 애플리케이션에서 실행되는 일이다. |
|
| |
실제 컬렉션에서 나는 알파벳 소문자를 사용하여 다중 문자 문자열(multicharacter strings)을 생성할 것이다. 예를 들어 4글자짜리 문자열을 만든다면, 26 x 26 x 26 x 26 = 456976가지가 생길 것이다. 나는 "ie", "xy", "pq" 중 어떤 것이든 포함하는 문자열을 세기 위해 이 컬렉션에 질의할 것이다. Vector 객체를 사용하여 테스트를 시작할 때부터 컬렉션을 모아둘 수 있다.
생성되는 데이터를 사용했고, 질의도 직접적으로 하였다. 하지만 지금은 튜닝에 초점을 맞추고 싶다. 여기서 질의는 다양한 타입의 질의를 대표한다.
간단하면서도 직접적인 질의는 다음과 같이 작성한다.
int count = 0;
for(int i = 0; i < collection.size(); i++)
{
if( ( ((String) collection.get(i)).indexOf("ie") != -1 )
| ( ((String) collection.get(i)).indexOf("xy") != -1 )
| ( ((String) collection.get(i)).indexOf("pq") != -1 ) )
count++;
}
return count;
|
이 예제에서는 문제가 몇 가지 있다. 첫 번째는 숏서킷(short-circuit) 부울-OR 연산자인 (||) 을 사용하지 않고, 부적절하게 정규 부울-OR 연산자 (|)를 사용하였다는 것이다. 두 번째는 루프 테스트인 (collection.size())에서 메소드 호출을 불필요하게 반복하였다는 것이다. 그리고 세 번째는 질의에서 String 캐스트가 반복되었다는 것이다. 이러한 것들 모두 루프에서 최적화를 위한 표준 타겟이다(
자바 퍼포먼스 튜닝 7장을 참고하라).
하지만 단지 이러한 것들이 표준 최적화이기 때문에 우리가 효과를 시험해 보지 않고 즉각적으로 그것을 적용할 수 있다는 것은 아니다. 따라서 그것을 테스트해 보기로 하자. 내가 한 테스트에서는 자바 SDK 1.2와 1.3의
Sun VM(Virtual Machine)을 사용하였다. 그리고 두 가지 SDK와 함께 전달되는 HotSpot VM과 함께 테스트할 것이다. 나는 non-JIT VM도 테스트할 것인데, 이번 경우에는 JIT를 꺼놓은 상태인 1.2 VM을 테스트할 것이다. "java.compiler" 특성(property)을 "NONE"으로 설정하기만 하면 된다(예를 들어 java "-Djava.compiler=NONE"라고 설정한다).
부울(Boolean)-OR 최적화를 적용한다
기본적으로, 숏서킷 부울 연산자는 연산자의 왼쪽이 명확한 결과를 제시하면, 연산자의 오른쪽을 계산하지 않아도 되도록 한다. 이러한 연산자에 대해서는
Java Performance Tuning 7장에서 자세히 논의하였으니, 참고하기 바란다.
우선, 부울-OR 연산자를 대체할 것이다. 간단한 최적화를 통해 |를 ||로 대체한다. 다음 예는
if( ( ((String) collection.get(i)).indexOf("ie") != -1 )
| ( ((String) collection.get(i)).indexOf("xy") != -1 )
| ( ((String) collection.get(i)).indexOf("pq") != -1 ) )
|
다음과 같이 된다.
if( ( ((String) collection.get(i)).indexOf("ie") != -1 )
|| ( ((String) collection.get(i)).indexOf("xy") != -1 )
|| ( ((String) collection.get(i)).indexOf("pq") != -1 ) )
|
숏 서킷 부울은 거의 모든 경우에 테스트의 속도를 약간 높인다(
Table and Listings에 있는 test2를 참고하라).
필요없이 반복되는 메소드 호출을 없앤다
루프 테스트에서 메소드 호출이 반복되지 않게 하려면, 다음 코드 대신에
for(int i = 0; i < collection.size(); i++)
|
이렇게 입력하면 된다.
int max = collection.size();
for(int i = 0; i < max; i++)
|
놀랍게도 이렇게 바꾸고 나면 대부분의 VM은 속도가 1~2% 높아지지만, 1.2 JIT VM은 40 퍼센트 느려진다.
Table and Listings에 있는 test3을 보라.
| |
|
| 목록에 있는 질의의 퍼포먼스를 개선하려면 튜닝을 거쳐야 한다. 필자가 쓴 Java Performance Tuning에서 표준 튜닝 기술에 대해 설명해 놓았으니 참고하기 바란다. |
|
| |
이러한 테스트를 통해 대부분의 VM의 속도를 최고로 높일 수 있다. 그리고 숏 서킷 연산자 최적화가 size() 호출을 대체하여 퍼포먼스가 40 퍼센트의 감소한 것을 직접적으로 없앨 수는 없지만, 1.2 VM 퍼포먼스도 전반적으로 조금 줄어들 뿐이다.
캐스트(Cast)를 없앤다
불필요한 String 캐스트를 없애자. 적절한 타입의 변수에 있는 첫 번째로 캐스트된 객체를 유지하기만 하면 된다. 예를 들어서 다음 코드는
for(int i = 0; i < max; i++)
{
if( ( ((String) collection.get(i)).indexOf("ie") != -1 )
|| ( ((String) collection.get(i)).indexOf("xy") != -1 )
|| ( ((String) collection.get(i)).indexOf("pq") != -1 ) )
|
이렇게 바뀐다.
String s;
for(int i = 0; i < max; i++)
{
if( ( (s = (String) collection.get(i)).indexOf("ie") != -1 )
|| ( s.indexOf("xy") != -1 )
|| ( s.indexOf("pq") != -1 ) )
|
이렇게 수정하면 모든 VM의 속도가 빨라진다. 모든 최적화와 테스트의 결과는 모두 test5에 수록하였다. size() 호출을 제거하지 않은 경우도
Table and Listings에 있는 test6에 나와 있다.
test5에서는 모든 VM의 속도가 처음보다 훨씬 빨라져 있을 것이다. 1.2 VM만 test6에서 더 빠르다.
동기화(Synchronization)를 피한다
| |
|
| 숏 서킷 부울은 대부분의 경우 테스트의 속도를 약간 높인다. |
|
| |
앞에서 언급했듯이, 지금까지 컬렉션을 보관하기 위해서 Vector 객체를 사용해 왔다. 대부분의 애플리케이션에서, 병목 질의는 읽기 전용이거나 단일 스레드로 되는 경향이 있었다. 어느 경우든지, 컬렉션을 유지하기 위해 동기화된 객체를 사용할 수 있다. 그렇게 하려면 우리가 처음에 사용했던 ArrayList 객체를 사용하도록 수정해야 한다. 코드에서 다른 부분은 바뀌지 않는다(자바 컬렉션에 대한 자세한 내용은
Java Performance Tuning 11장을 참고하라).
지금까지 최적화 테스트를 한 결과와 ArrayList 컬렉션 객체에 생긴 변화는
Table and Listings의 test7에 나와 있다. test8에서는 size() 호출을 제거하지 않고 테스트하기도 하였다. test7은 모든 1.2 JIT VM을 포함한 모든 VM 중에서 가장 빠르다. 1.2 VM에서는 축적된 변화가 비효율적인 원시 코드 생성 문제를 피할 수 있게 된다.
메소드 접근자를 피한다
또 다른 표준 최적화는 다른 방법을 사용하여 직접적으로 요소에 접근할 수 있다면, 반복적으로 메소드 접근자를 사용하여 컬렉션의 요소에 접근하는 것을 피하는 것이다.
컬렉션 질의에서 메소드 접근자를 피하려면 컬렉션 클래스에 있는 쿼리를 구현하기만 하면 된다. 여기 있는 예에서는 java.util.List 클래스를 구현할 것이며, 그 클래스에 있는 질의를 구현하여 내부 컬렉션에 접근할 수 있다. 하지만 이보다 더 빠른 메소드도 있다. Vector는 protected라고 정의된 내부 요소 컬렉션과 함께 구현되기 때문에, 내부 요소 컬렉션에 접근하기 위해 Vector를 subclass할 수 있고, 다음과 같이 subclass에 질의를 구현할 수 있다.
class TestList
extends Vector
{
public int customQuery()
{
int count = 0;
String s;
for(int i = 0; i < elementCount; i++)
{
if( ( (s = (String) elementData[i]).indexOf("ie") != -1 )
|| ( s.indexOf("xy") != -1 )
|| ( s.indexOf("pq") != -1 ) )
count++;
}
return count;
}
}
|
원본 테스트와 똑같이 하려면 다음과 같이 입력하면 된다.
return collection.customQuery();
|
이 테스트에 대한 결과는
Table and Listings에 있는 test9에 잘 나와 있다. 이것이 모든 VM에서 가장 빠른 테스트이다(HotSpot 1.0이 처음 실행될 때는 예외이다).
비표준 최적화
| |
|
| 최적화 하는 데에 시간을 보내는 것보다 단지 VM만 최적화 하는 것이 훨씬 쉽고 비용도 적게 든다. 하지만 광범위한 수동 최적화가 적용될 수 있을 때에는 평상시의 JIT VM이 VM을 최적화하는 것보다 실행 속도가 더 빠를 수도 있다. |
|
| |
앞의 섹션에서는 이 기사에서 나온 예에 적용하고 싶은 마지막 표준 최적화를 보여준다. 독자에게 비표준 최적화가 필요할 지도 모르기 때문에,
Table and Listings의 test10에서 test14 까지 이 내용을 담았다. 인스턴스 변수를 로컬 변수로 옮기며, 요소(element)를 내재하는 String[] 배열과 함께 컬렉션을 재구현한다(이에 대해서는
Java Performance Tuning 11장을 참고하라). 모든 테스트의 리스트를 보려면,
Table and Listings을 참고하라.
결과
이러한 튜닝 연습을 통해 컬렉션에 질의를 하는 속도를 눈에 띄게 높이는 것이 어렵지 않다는 것을 알 수 있었다.
Table and Listings에서 나타난 결과를 보면, 심지어 HotSpot VM에 최적화를 적용해도, 질의 속도를 높일 수 있다는 것을 알 수 있다.
흥미롭게도 1.2 VM은 수동 최적화가 적용된 후에 가장 빨라질 수 있는 VM이다(test9에서는 40 퍼센트로 실행되는 1.2 VM을 보여준다. 하지만 다른 VM도 45 퍼센트도 수행하지 못했다. 모든 타이밍은 동일한 절대값이었다). 일반적으로 애플리케이션을 최적화하지 않았을 경우, VM을 최적화하면(1.3 VM과 HotSpot VM처럼) 퍼포먼스가 더 좋아질 때가 많다.
최적화 하는 데에 시간을 보내는 것보다 단지 VM만 최적화 하는 것이 훨씬 쉽고 비용도 적게 든다. 하지만 광범위한 수동 최적화가 적용될 수 있을 때에는 평상시의 JIT VM이 VM을 최적화하는 것보다 실행 속도가 더 빠를 수도 있다. VM을 아무리 최적화 해도 사람만큼 똑똑해질 수 는 없기 때문에, 이것은 놀랄 만한 일이 아니다.
테스트 코드 전체 목록
테스트 코드 전체 목록을 보려면,
이곳으로 가면 된다.
잭 시라지는 독립 컨설턴트이다. 그는 자바를 일찍 수용했으며, 지난 몇 년간 자바 퍼포먼스에 초점을 맞춘 재정 부분에서 컨설팅을 하였다. 자바를 사용하기 전에는 스몰토크 애플리케이션을 몇 년간 튜닝하였다.