Java를 빠르게 : 최적화하십시오!

선구적인 컴퓨터 과학자 인 Donald Knuth에 따르면, "조기 최적화는 모든 악의 근원입니다." 최적화에 대한 모든 기사는 일반적 으로 최적화보다 최적화 하지 않는 이유가 더 많다는 점을 지적하는 것으로 시작해야합니다 .

  • 코드가 이미 작동하는 경우 최적화는 새롭고 미묘한 버그를 도입하는 확실한 방법입니다.

  • 최적화는 코드를 이해하고 유지하기 어렵게 만드는 경향이 있습니다.

  • 여기에 제시된 기술 중 일부는 코드의 확장 성을 줄여 속도를 높입니다.

  • 한 플랫폼에 대한 코드를 최적화하면 실제로 다른 플랫폼에서 더 나빠질 수 있습니다.

  • 최적화에 많은 시간이 소요될 수 있지만 성능은 거의 향상되지 않으며 코드가 난독 화 될 수 있습니다.

  • 코드 최적화에 지나치게 집착하면 사람들은 당신을 등 뒤에서 괴짜라고 부를 것입니다.

최적화하기 전에 최적화가 필요한지 여부를 신중하게 고려해야합니다. Java의 최적화는 실행 환경이 다양하기 때문에 어려운 대상이 될 수 있습니다. 더 나은 알고리즘을 사용하면 낮은 수준의 최적화보다 더 큰 성능 향상을 얻을 수 있으며 모든 실행 조건에서 개선을 제공 할 가능성이 높습니다. 일반적으로 낮은 수준의 최적화를 수행하기 전에 높은 수준의 최적화를 고려해야합니다.

그렇다면 왜 최적화할까요?

그렇게 나쁜 생각이라면 왜 최적화할까요? 글쎄, 이상적인 세상에서는 그렇게하지 않을 것입니다. 그러나 실제로는 프로그램의 가장 큰 문제는 단순히 너무 많은 리소스가 필요하고 이러한 리소스 (메모리, CPU주기, 네트워크 대역폭 또는 조합)가 제한 될 수 있다는 것입니다. 프로그램 전체에서 여러 번 발생하는 코드 조각은 크기에 민감 할 수 있지만 실행 반복이 많은 코드는 속도에 민감 할 수 있습니다.

Java를 빠르게 만드십시오!

간결한 바이트 코드, 속도 또는 부족한 해석 언어는 Java에서 가장 자주 발생하는 문제입니다. 우리는 주로 Java를 더 작은 공간에 맞추기보다 더 빠르게 실행하는 방법을 살펴볼 것입니다. 그러나 이러한 접근 방식이 메모리 또는 네트워크 대역폭에 영향을 미치는 위치와 방식을 지적 할 것입니다. 초점은 Java API보다는 핵심 언어에 있습니다.

그런데 여기서 논의 하지 않을 한 가지는 C 또는 어셈블리로 작성된 네이티브 메서드를 사용하는 것입니다. 네이티브 메서드를 사용하면 궁극적 인 성능 향상을 얻을 수 있지만 Java의 플랫폼 독립성을 희생해야합니다. 선택한 플랫폼에 대해 Java 버전의 메소드와 기본 버전을 모두 작성할 수 있습니다. 이로 인해 모든 플랫폼에서 실행되는 기능을 포기하지 않고 일부 플랫폼에서 성능이 향상됩니다. 그러나 이것이 Java를 C 코드로 대체하는 주제에 대해 제가 말하고자하는 전부입니다. (이 주제에 대한 자세한 정보는 Java 팁, "기본 메소드 작성"을 참조하십시오.)이 기사에서는 Java를 빠르게 만드는 방법에 중점을 둡니다.

90/10, 80/20, 헛, 헛, 하이킹!

일반적으로 프로그램 실행 시간의 90 %는 코드의 10 %를 실행하는 데 사용됩니다. (일부 사람들은 80 % / 20 % 규칙을 사용하지만 지난 15 년 동안 여러 언어로 상용 게임을 작성하고 최적화 한 경험에 따르면 90 % / 10 % 공식은 성능이 부족한 프로그램에서 일반적입니다. 프로그램의 다른 90 % (실행 시간의 10 %가 소요됨)를 최적화하는 것은 성능에 눈에 띄는 영향을 미치지 않습니다. 90 %의 코드를 두 배 빠르게 실행할 수 있다면 프로그램은 5 % 만 더 빨라질 것입니다. 따라서 코드 최적화의 첫 번째 작업은 대부분의 실행 시간을 소비하는 프로그램의 10 % (이보다 적은 경우가 많음)를 식별하는 것입니다. 이것은t 항상 예상되는 곳에.

일반 최적화 기술

사용되는 언어에 관계없이 적용되는 몇 가지 일반적인 최적화 기술이 있습니다. 전역 레지스터 할당과 같은 이러한 기술 중 일부는 시스템 리소스 (예 : CPU 레지스터)를 할당하는 정교한 전략이며 Java 바이트 코드에는 적용되지 않습니다. 우리는 기본적으로 코드를 재구성하고 메서드 내에서 동등한 작업을 대체하는 기술에 초점을 맞출 것입니다.

강도 감소

강도 감소는 작업이 더 빠르게 실행되는 동등한 작업으로 대체 될 때 발생합니다. 강도 저하의 가장 흔한 예는, 예를 들어 2의 제곱으로 나눈 곱하기 정수 시프트 연산자를 사용하고, x >> 2대신에 사용될 수있는 x / 4, 그리고 x << 1대체한다 x * 2.

일반적인 하위 표현 제거

일반적인 하위 표현식 제거는 중복 계산을 제거합니다. 쓰는 대신

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

공통 하위 표현식은 한 번 계산되고 두 계산에 사용됩니다.

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

코드 모션

코드 움직임 동작 코드가 수행하는 동작 또는 그 결과를 변경하지 않는 식을 계산하거나 것을 불변 . 결과가 필요할 때마다 실행하지 않고 결과가 변경 될 때만 실행되도록 코드가 이동됩니다. 이것은 루프에서 가장 일반적이지만 메서드를 호출 할 때마다 반복되는 코드를 포함 할 수도 있습니다. 다음은 루프에서 변하지 않는 코드 모션의 예입니다.

for (int i = 0; i < x.length; i++) x [i] * = Math.PI * Math.cos (y); 

된다

더블 피코시 = Math.PI * Math.cos (y); for (int i = 0; i < x.length; i++)x [i] * = 피코시;

루프 풀기

루프를 풀면 루프를 통해 매번 둘 이상의 작업을 수행하고 결과적으로 더 적은 반복을 실행하여 루프 제어 코드의 오버 헤드를 줄입니다. 이전 예제에서 작업하면서의 길이 x[]가 항상 2의 배수 라는 것을 알고 있다면 루프를 다음과 같이 다시 작성할 수 있습니다.

더블 피코시 = Math.PI * Math.cos (y); for (int i = 0; i < x.length; i += 2) {x [i] * = 피코시; x [i + 1] * = 피코시; }

실제로 이와 같은 언 롤링 루프 (루프 인덱스의 값이 루프 내에서 사용되고 별도로 증분되어야 함)는 바이트 코드에 " +1"를 배열 색인에 추가합니다.

이 도움말의 모든 최적화 도움말은 위에 나열된 일반적인 기술 중 하나 이상을 구현합니다.

컴파일러 작동시키기

최신 C 및 Fortran 컴파일러는 고도로 최적화 된 코드를 생성합니다. C ++ 컴파일러는 일반적으로 덜 효율적인 코드를 생성하지만 여전히 최적의 코드를 생성하는 경로를 따라갑니다. 이 모든 컴파일러는 강력한 시장 경쟁의 영향으로 여러 세대를 거쳐 왔으며 일반 코드의 마지막 성능 저하를 모두 제거 할 수있는 정교하게 연마 된 도구가되었습니다. 그들은 거의 확실하게 위에 제시된 모든 일반 최적화 기술을 사용합니다. 그러나 컴파일러가 효율적인 코드를 생성하도록 만드는 방법은 아직 많이 남아 있습니다.

javac, JIT 및 네이티브 코드 컴파일러

javac이 시점에서 코드를 컴파일 할 때 수행 하는 최적화 수준 은 최소화됩니다. 기본적으로 다음을 수행합니다.

  • 상수 폴딩-컴파일러 i = (10 *10)i = 100.

  • 분기 접기 (대부분)-불필요한 goto바이트 코드를 피합니다.

  • 제한된 데드 코드 제거- if(false) i = 1.

javac가 제공하는 최적화 수준은 언어가 성숙 해지고 컴파일러 공급 업체가 코드 생성을 기반으로 본격적으로 경쟁하기 시작함에 따라 크게 향상 될 것입니다. Java는 이제 2 세대 컴파일러를 사용하고 있습니다.

그런 다음 런타임에 Java 바이트 코드를 원시 코드로 변환하는 JIT (Just-In-Time) 컴파일러가 있습니다. 여러 가지가 이미 사용 가능하며 프로그램의 실행 속도를 크게 높일 수 있지만 최적화가 런타임에 발생하기 때문에 수행 할 수있는 최적화 수준이 제한됩니다. JIT 컴파일러는 가장 빠른 코드를 생성하는 것보다 코드를 빠르게 생성하는 데 더 관심이 있습니다.

Java를 네이티브 코드로 직접 컴파일하는 네이티브 코드 컴파일러는 최고의 성능을 제공해야하지만 플랫폼 독립성을 희생해야합니다. 다행히 여기에 제시된 많은 트릭은 미래의 컴파일러에 의해 달성 될 것이지만 지금은 컴파일러를 최대한 활용하려면 약간의 작업이 필요합니다.

javac사용할 수있는 하나의 성능 옵션을 제공 -O합니다. 컴파일러가 특정 메서드 호출을 인라인 하도록 옵션을 호출합니다.

javac -O MyClass

메서드 호출을 인라인하면 메서드를 호출하는 코드에 메서드에 대한 코드가 직접 삽입됩니다. 이렇게하면 메서드 호출의 오버 헤드가 제거됩니다. 작은 메서드의 경우이 오버 헤드는 실행 시간의 상당한 비율을 나타낼 수 있습니다. 유일한 방법 중 하나로 선언합니다 private, static또는 final오직이 방법은 정적 컴파일러에 의해 해결되기 때문에, 인라인 고려 될 수있다. 또한 synchronized메서드는 인라인되지 않습니다. 컴파일러는 일반적으로 한두 줄의 코드로만 구성된 작은 메서드 만 인라인합니다.

불행히도, javac 컴파일러의 1.0 버전에는 -O옵션이 사용될 때 바이트 코드 검증기를 통과 할 수없는 코드를 생성하는 버그가 있습니다 . 이것은 JDK 1.1에서 수정되었습니다. (바이트 코드 검증기는 Java 규칙을 위반하지 않는지 확인하기 위해 실행이 허용되기 전에 코드를 확인합니다.) 호출 클래스에 액세스 할 수없는 클래스 멤버를 참조하는 메소드를 인라인합니다. 예를 들어 다음 클래스가 -O옵션을 사용하여 함께 컴파일 된 경우

class A {private static int x = 10; public static void getX () {return x; }} 클래스 B {int y = A.getX (); }

클래스 B의 A.getX () 호출은 B가 다음과 같이 작성된 것처럼 클래스 B에서 인라인됩니다.

클래스 B {int y = Ax; }

그러나 이로 인해 바이트 코드 생성이 B의 코드에서 생성 될 비공개 Ax 변수에 액세스합니다. 이 코드는 잘 실행되지만 Java의 액세스 제한을 위반 IllegalAccessError하므로 코드가 처음 실행될 때 검증 자에 의해 플래그가 지정됩니다 .

이 버그가 -O옵션을 쓸모 없게 만드는 것은 아니지만 사용 방법에 대해주의해야합니다. 단일 클래스에서 호출되는 경우 위험없이 클래스 내에서 특정 메서드 호출을 인라인 할 수 있습니다. 잠재적 인 액세스 제한이없는 한 여러 클래스를 함께 인라인 할 수 있습니다. 그리고 일부 코드 (예 : 응용 프로그램)는 바이트 코드 검증의 대상이 아닙니다. 코드가 검증 자없이 만 실행된다는 것을 알고 있다면 버그를 무시할 수 있습니다. 추가 정보는 내 javac-O FAQ를 참조하십시오.

프로파일 러

다행히 JDK에는 프로그램에서 시간이 소비되는 위치를 식별하는 데 도움이되는 프로파일 러가 내장되어 있습니다. 각 루틴에 소요 된 시간을 추적하고 정보를 파일에 기록합니다 java.prof. 프로파일 러를 실행하려면 -profJava 인터프리터를 호출 할 때 옵션을 사용하십시오 .

java -prof myClass

또는 애플릿과 함께 사용하는 경우 :

java -prof sun.applet.AppletViewer myApplet.html

프로파일 러 사용에 대한 몇 가지주의 사항이 있습니다. 프로파일 러 출력은 특히 해독하기 쉽지 않습니다. 또한 JDK 1.0.2에서는 메서드 이름을 30 자로 잘라서 일부 메서드를 구분하지 못할 수도 있습니다. 불행히도 Mac에서는 프로파일 러를 호출 할 방법이 없으므로 Mac 사용자는 운이 좋지 않습니다. 이 외에도 Sun의 Java 문서 페이지 (참고 자료 참조)에는 -prof옵션에 대한 문서가 더 이상 포함되어 있지 않습니다 . 그러나 플랫폼이 -prof옵션을 지원하는 경우 Vladimir Bulatov의 HyperProf 또는 Greg White의 ProfileViewer를 사용하여 결과를 해석 할 수 있습니다 (참고 자료 참조).

코드에 명시적인 타이밍을 삽입하여 코드를 "프로파일"할 수도 있습니다.

long start = System.currentTimeMillis(); // do operation to be timed here long time = System.currentTimeMillis() - start;

System.currentTimeMillis()1/1000 초의 시간을 반환합니다. 그러나 Windows PC와 같은 일부 시스템에는 1/1000 초보다 낮은 (훨씬 적은) 해상도의 시스템 타이머가 있습니다. 1/1000 초도 많은 작업 시간을 정확하게 측정 할 수있을만큼 길지 않습니다. 이러한 경우 또는 저해상도 타이머가있는 시스템에서는 작업을 n 번 반복하는 데 걸리는 시간을 측정 한 다음 총 시간을 n 으로 나누어 실제 시간을 구해야 할 수 있습니다. 프로파일 링을 사용할 수있는 경우에도이 기술은 특정 작업 또는 작업의 타이밍에 유용 할 수 있습니다.

다음은 프로파일 링에 대한 몇 가지 결론입니다.

  • 최소한 테스트 플랫폼에서 변경 사항이 프로그램을 개선했는지 확인하기 위해 변경 전후에 항상 코드 시간을 측정합니다.

  • 동일한 조건에서 각 타이밍 테스트를 시도하십시오.

  • 가능한 경우 사용자 입력에 의존하지 않는 테스트를 시도하십시오. 사용자 응답의 변화로 인해 결과가 변동될 수 있습니다.

벤치 마크 애플릿

Benchmark 애플릿은 작업을 수천 번 (또는 수백만 번) 수행하는 데 걸리는 시간을 측정하고 테스트 이외의 작업 (예 : 루프 오버 헤드)을 수행하는 데 소요 된 시간을 뺀 다음이 정보를 사용하여 각 작업의 시간을 계산합니다. 했다. 약 1 초 동안 각 테스트를 실행합니다. 컴퓨터가 테스트 중에 수행 할 수있는 다른 작업의 임의 지연을 제거하기 위해 각 테스트를 세 번 실행하고 최상의 결과를 사용합니다. 또한 테스트에서 가비지 콜렉션을 제거하려고 시도합니다. 이 때문에 벤치 마크에 사용 가능한 메모리가 많을수록 벤치 마크 결과가 더 정확 해집니다.