devcken.io

Thoughts, stories and ideas.

Java Performance and Optimization

이 포스트는 Ravi Thapa라는 분이 작성한 Performance and Optimization라는 글을 번역한 것입니다. 오역이나 손발이 오그라드는 직역이 있을 수도 있습니다.

변수 초기화를 적게 하라

이것은 메서드 변수를 루프 내에서 반복적으로 접근할 때 특히 유용합니다. 예를 들어, 일반적인 루프는 다음과 같이 만들어집니다:

for (int i = 0; ++i <= limit; )  

이 코드는 다음 코드처럼 다시 작성하면 25 퍼센트(JIT 컴파일를 사용하면 5퍼센트) 향상시킬 수 있습니다:

for (int i = limit; --i >= 0; )  

이렇게 하면 변수에 접근하는 횟수를 limit 변수만큼 줄일 수 있습니다.

클래스 계층을 줄여라

일반적으로, 객체 생성은 클래스 계층이 깊어지면 질수록 더 느려집니다. 게다가, 깊이가 깊은 클래스 계층은 애플릿의 로드 시간을 더 길게 만드는데 네트워크 상으로 부가적인 클래스들이 전송되어야 하기 때문입니다. 가능하다면, 상태 변수로 표현될 수 있는 하위 변형을 분화시키지 말아야 합니다. 하지만, 애플리케이션의 객체 지향 설계를 희생시키지 않는지 주의를 기울여야 합니다.

명시적인 가비지 컬렉터 호출을 피하라

응답성을 기대하는 경우 가비지 컬렉션을 호출하면, (예를 들어, 마우스 버튼 이벤트 처리 시) 사용자는 빠른 처리를 기대하지만 프로그램은 일시적으로 느려질 수 있습니다. 대부분의 환경에서, System.gc()의 명시적인 호출은 필요치 않습니다. System.gc() 호출은 즉시 가비지 컬렉터를 실행하지 않으며, 시스템이 가비지 컬렉터를 실행시키는 시점에 실행됩니다.

동기화를 피하라

JVM은 클래스 참조 분석 시 클래스 잠금을 사용합니다. 클래스 참조는 (클래스 로드 시에 분석되기 보다는) 사용 기반으로 분석되므로, JVM이 클래스 잠금을 원하는 시점을 예측하기 어렵습니다. 그러니, 클래스에서 동기화를 하지 마십시오.

그에 대한 대안은 특별한 목적의 잠금 객체를 만드는 것입니다. 예를 들어:

private static final Object lock = new Object();  

하지만, 동기화 동작은 상당한 시스템 자원을 소모하며 Java 성능에 영향을 줄 수 있습니다. 이는 잠금 가용성을 위한 상수 검사 때문에, 하나의 스레드에서 실행된다고 할지라도 해당될 수 있습니다. 결과적으로, 동기화는 가능한 많이 줄여야 합니다.

StringBuffer 혹은 StringBuilder를 사용하라

문자열은 불변이기 때문에, 문자열의 모든 변경은 적어도 한 개 이상의 문자열 객체를 생성합니다. 이로 인해 성능이 떨어지고 이후에 가비지 컬렉션해야 하는 객체들이 불필요하게 생성됩니다. 하지만, StringBuffer는 수정 가능하여, 임시 문자열 객체의 생성을 피하는데 사용 가능합니다.

StringBuilder는 StringBuffer를 토대로 작성된 좀 더 고급 API로 동일한 목적으로 사용할 수 있습니다.

자원을 명시적으로 닫아라

FileInputStream 메서드를 사용할 때, 객체 finalizer에 의존해 파일을 닫지 마십시오. 작업이 끝나면 파일을 명시적으로 닫아야 합니다. 파일을 finalizer가 닫도록 하는 경우 finalizer가 실제로 실행되기 전까지 상당히 긴 지연이 있습니다. 이로 인해 finalizer가 실행되는 시간까지 운영체제 파일 핸들을 사용 중인 것으로 유지되며 운영체제 파일 핸들의 풀(pool)이 고갈되어버리는 상황을 만들 수도 있습니다.

스레드의 개수를 제한하라

생성되는 모든 Java 스레드는 그것의 네이티브 스택 프레임을 위한 전용 메모리를 요구합니다. 많은 시스템에서, 네이티브 스택 프레임의 크기는 --ss Java 명령줄 파라메터에 의해 제어되며 생성된 모든 Java 스레드에 대해 동일합니다. 일부 플랫폼의 디폴트 스택 크기는 32킬로바이트 정도입니다. 20개의 스레드를 지닌 애플리케이션의 경우, 이는 32KB * 20 혹은 640KB를 나타냅니다. 애플리케이션 내 스레드 개수 제한은 시스템 메모리 요구사항을 낮추는데 도움이 됩니다. 또한 실제 작업을 수행하는 스레드에 더 많은 CPU 시간을 제공하여 성능을 향상시키는 결과를 낳기도 합니다.

JDBC 데이터 액세스

JDBC의 ResultSet.getXXX 메서드를 사용하는 경우 사용하고 있는 get이 데이터베이스에 데이터가 저장된 방식과 동일한 타입인지를 확인하세요. 데이터베이스 내 정수가 있다면, 결과셋에서 그것을 읽어올 때 ResultSet.getInt() 메서드를 사용하시기 바랍니다.

JNI 사용

JNI를 사용 중이고 프리미티브 배열 객체의 멤버를 가져와야 할 경우(C에서 Java로), GetArray 호출 대신 GetArrayRegion 호출을 사용하세요.

네이티브 연산을 모아서 JNI 호출 횟수를 줄이세요.

트랜잭션이나 연산을 통합하여 어떤 작업을 수행하는데 필요로 하는 JNI 호출 횟수를 줄이세요. JNI 오버헤드가 사용되는 횟수를 줄이면 성능이 향상됩니다.

Java 콘솔에 과도한 쓰기를 피하라

Java 콘솔에 정보를 쓰면 시간과 자원이 들며, 필요하지 않다면 해서는 안됩니다. 디버깅에 도움이 될지라도, 콘솔 쓰기는 보통 문자열 처리, 텍스트 포맷팅, 그리고 출력을 수반합니다. 이들은 전형적으로 느린 연산입니다. 콘솔이 출력되지 않는 경우 조차도, 성능에는 불리하게 작용합니다.

데이터 타입

프리미티브 타입은 타입을 캡슐화한 클래스보다 빠릅니다. 신중하게 변수에 대해 프리미티브 타입을 사용하여 객체 생성과 조작의 비용을 줄이세요. 메모리가 줄어들 수 있으며 변수 접근 시간이 향상될 수 있습니다.

다음 예제에서, 두번째 선언이 더 작고 빠릅니다:

Currency {  
public double amount;  
}
double currency_amount;  

C++처럼, Java의 캐스팅은 컴파일 시점에 이루어지지 않습니다. 런타임 비용이 있으니, 불필요한 변수 리캐스팅을 피하세요.

32비트 시스템에서 가능하면 long 보다는 int를 사용하십시오.

long은 64비트이며 int는 32비트 데이터 타입입니다. 32비트 시스템에서 32비트 연산이 64비트 연산보다 빠르게 실행됩니다. 예제 1이 예제 2의 약 절반의 시간이 걸립니다. 예제 2가 64비트 주소 지정을 사용하는 시스템에서 더 빠르게 실행된다는 점에 유의하십시오.

상수 생성 시 static final을 사용하세요. 데이터가 불변이라면, static과 final하게 선언하십시오. 변수가 초기화되는데 필요한 횟수를 줄이고 JVM에 더 나은 최적화 정보를 제공하게 되어, 성능이 개선됩니다.

가능하다면, 메서드를 final로 선언하라

가능하다면 메서드를 final로 선언하세요. final 메서드는 JVM에 의해 더 좋게 처리되어, 향상된 성능을 이끌어 냅니다. 다음 예에서 두번째 메서드는 첫번째보다 더 빠르게 실행됩니다.

void doThing() {  
  for (int i=0; i<100; ++i) {
    dosomething;
  }
}

final void doThingfinal() {  
  for (int i=0; i<100; ++i) {
    dosomething;
  }
}

배열이 벡터보다 빠릅니다

배열이면 충분한 경우 벡터를 피해, 애플리케이션 성능을 향상시킬 수 있습니다. 벡터를 사용하는 경우, 벡터의 끝에서 아이템을 추가하거나 삭제하는 것이 더 빠르다는 점을 기억하세요.

인스턴스 변수를 남용하지 말라

로컬 변수를 사용해 성능을 향상시킬 수 있습니다. 예제 1의 코드는 예제 2의 코드보다 빠르게 실행됩니다.

예제 1:

public void loop() {  
     int j = 0;
     for ( int i = 0; i<250000;i++){
     j = j + 1;
     }
}

예제 2:

int i;  
public void loop() {  
    int j = 0;
    for (i = 0; i<250000;i++){
    j = j + 1;
  }
}

불변 객체를 사용해야 할 때를 알아두자

불변 객체를 사용해야 하는 많은 타당한 이유들이 있습니다. 불변 객체의 주요 단점은 성능이 될 수 있으므로, 그것들을 언제, 어디서 사용해야 하는지 알아두는 것이 좋습니다.

Java API는 두 가지 매우 유사한 클래스를 제공하는데, 하나는 불변이고 다른 하나는 가변입니다. String은 불변으로 설계되어 더 간단하고 안전합니다. StringBuffer는 가변으로 설계되어, 성능 상의 엄청난 이점을 지니고 있습니다. 예를 들어, 다음 코드를 보면, 하는 문자열 결합을 사용하고 있고 다른 하나는 StringBuffer의 append 메서드를 사용하고 있습니다(doSomething() 메서드가 오버로드되어 String 혹은 StringBuffer로 문자들을 받는다고 가정하겠습니다).

케이스 1

for (i = 0; i < source.length; ++i) {  
  doSomething(i + ": " + source[i]);
}

케이스 2

StringBuffer temp = new StringBuffer();  
for (i = 0; i < source.length; ++i) {  
temp.setLength(0); // clear previous contents  
temp.append(i).append(": ").append(source[i]);  
doSomething(temp);  
}

뒤에서, 컴파일러는 케이스 1을 최적화하지만, 이터레이션 당 두 객체의 생성을 수반합니다. 반면, 케이스 2에서는 모든 객체 생성을 피하고 있습니다. 이 때문에, 케이스 2는 케이스 1에 비해 1000% 빠릅니다(물론 JIT에 따라 달라집니다).

불필요한 객체 생성을 피하고 항상 지연된 초기화를 사용하라

Java의 객체 생성은 메모리 활용과 성능 영향 측면에서 가장 비싼 연산 중 하나입니다. 그러므로 코드 내에서 필요로 하는 경우에만 객체를 생성하거나 초기화하는 것이 바람직합니다.

public class Countries {  
    private List countries;    
    public List getCountries() {        
        // 필요한 경우에만 초기화한다
        if(null == countries) {
            countries = new ArrayList();
        }
        return countries;
    }
}

클래스의 인스턴스 필드를 절대 public으로 만들지 마라

클래스 필드를 public으로 만드는 것은 프로그램 내에서 많은 이슈를 만들어 낼 수 있습니다. 예를 들어 MyCalender라는 클래스가 있다고 가정하겠습니다. 이 클래스는 문자열로 된 요일 배열을 가지고 있습니다. 이 배열이 항상 요일에 대한 7개의 이름을 가지고 있다고 하겠습니다. 그 배열이 public 임으로써, 누구나 접근 가능합니다. 실수로 누군가 해당 값을 고치고 버그를 만들 수도 있습니다!

public class MyCalender {  
    public String[] weekdays = 
        {"Sun", "Mon", "Tue", "Thu", "Fri", "Sat", "Sun"};

    //some code     
}

많은 이들이 이미 알고 있는대로 가장 좋은 방법은 필드를 항상 private으로 만들고 getter 메서드를 추가해 요소에 접근하는 것입니다.

private String[] weekdays =  
    {"Sun", "Mon", "Tue", "Thu", "Fri", "Sat", "Sun"};

public String[] getWeekdays() {  
    return weekdays;
}

그러나 getter 메서드 작성이 이 문제를 확실히 해결해주지는 않습니다. 해당 배열은 여전히 접근 가능합니다. 그것을 수정 불가능한 상태로 만드는 최고의 방법은 배열 자체보다는 배열의 복제를 반환하는 것입니다. 그래서 getter 메서드는 다음과 같이 변경 가능합니다.

public String[] getWeekdays() {  
    return weekdays.clone();
}

항상 클래스의 변이성을 최소화하려고 노력하라

클래스를 불변으로 만드는 것을 그것을 수정 불가능한 상태로 만드는 것입니다. 클래스가 유지하는 정보는 클래스의 수명 내내 그대로 유지됩니다. 불변 클래스는 간단하며, 관리하기 쉽습니다. 스레드에 안전합니다. 다른 객체를 위한 굉장한 빌딩 블록을 만듭니다.

하지만 불변 객체를 만드는 것은 앱의 성능에 영향을 줄 수 있습니다. 그러므로 클래스를 불변으로 만들지 아니면 변이로 만들지를 현명하게 선택해야 합니다. 항상 더 적은 필드를 지닌 작은 클래스를 불변하게 만들려고 노력하세요.

클래스를 불변으로 만들기 위해 모든 생성자를 private으로 만든 뒤 public static 메서드를 만들어 객체를 초기화하고 그것을 반환하세요.

public class Employee {

    private String firstName;
    private String lastName;

    //private default constructor
    private Employee(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public static Employee valueOf (String firstName, String lastName) {
        return new Employee(firstName, lastName);
    }
}

추상 클래스 대신 인터페이스를 사용하려고 하라

먼저 Java에서는 다중 클래스 상속이 불가능합니다. 그러나 여러 인터페이스를 구현하는 것은 분명 가능합니다. 기존의 클래스 구현을 바꾸고 클래스의 전체 계층 구조를 바꾸지 않고 한 개 이상의 인터페이스 구현을 추가하는 것은 매우 쉽습니다.

어떤 메서드가 어떤 인터페이스를 가질지 100% 확신한다면, 해당 인터페이스 코딩만 시작하세요. 기존의 구현된 코드를 깨지 않고 기존의 인터페이스에 새로운 메서드를 추가하는 것은 매우 어렵습니다. 반대로 기존 기능을 손상시키지 않고 추상 클래스에 새 메서드를 추가하는 것은 쉽습니다.

항상 지역 변수의 범위를 제한하려고 노력하라

지역 변수는 훌륭합니다. 그러나 때로는 예전 코드를 붙여넣기 하여 버그가 들어갈 수도 있습니다. 로컬 변수의 범위를 최소화하는 것은 코드를 좀 더 읽기 좋게 만들고, 오류 발생을 줄이며, 코드의 유지보수성을 향상시킵니다.

그러니 로컬 변수 사용 직전에 필요로 하는 경우에만 변수를 선언하세요.

항상 선언과 함께 로컬 변수를 초기화하세요. 불가능하다면 적어도 로컬 인스턴스에 null 값을 할당하세요.

자신의 코드를 처음부터 다 작성하는 대신 표준 라이브러리를 사용하려고 하라

코드 작성은 재미있습니다. 하지만 "바퀴를 발명하려고 하지 마세요". 이미 테스트되고, 디버그되어 다른 이들이 사용 중인 기존의 표준 라이브러리를 사용하는 것이 매우 바람직합니다. 이는 프로그래머의 효율성을 향상시킬 뿐만 아니라, 여러분의 코드에 새로운 버그를 넣을 가능성을 줄여줍니다. 또한 표준 라이브러리를 사용하면 코드를 잃기 좋고 유지보수성을 높게 만들어줍니다.

가능하다면 래퍼 클래스 대신 프리미티브 타입을 사용하려고 하라

래퍼 클래스는 훌륭합니다. 그러나 동시에 느립니다. 프리미티브 타입은 그냥 값인 반면, 래퍼 클래스는 전체 클래스에 대한 정보를 저장합니다.

때때로 프로그래머는 래퍼 클래스를 부주의하게 사용하여 코드 내에 버그를 넣을수도 있습니다. 예를 들어, 아래 예제에서:

int x = 10;  
int y = 10;

Integer x1 = new Integer(10);  
Integer y1 = new Integer(10);

System.out.println(x == y);  
System.out.println(x1 == y1);  

첫번째로 출력되는 것은 true, 두번째로 출력되는 것은 false일 겁니다. 문제는 두 래퍼 클래스를 비교할 때 == 연산자를 사용할 수 있다는 것입니다. 그렇게 하면 객체의 실제 값이 아닌 참조를 비교하게 됩니다.

또한 래퍼 클래스 객체를 사용 중이라면 그것을 디폴트 값으로 초기화하는 것을 결코 잊어서는 안됩니다. 기본적으로 모든 래퍼 클래스는 null로 초기화됩니다.

Boolean flag;

if(flag == true) {  
    System.out.println("Flag is set");
} else {
    System.out.println("Flag is not set");
}

위 코드는 true와 비교하기 이전에 상자 안의 값을 알아보려 하고 상자가 null이기 때문에 NullPointerException을 발생시킵니다.

String을 사용할 때 각별히 주의하라

코드 내에서 항상 String을 주의하여 사용하세요. String의 간단한 결합은 프로그램의 성능을 저하시킵니다. 예를 들어 for 루프 내에서 + 연산자를 사용해 String을 결합하는 경우 모두 + 연산자가 사용되며, 그로 인해 새로운 String 객체가 생성됩니다.

또한 String 객체를 인스턴스화할 때마다, 절대 생성자를 사용하지 말고 항상 직접 인스턴스화하십시오. 예를 들어:

// 느린 인스턴스화
String slow = new String("Yet another string object");

// 빠른 인스턴스화
String fast = "Yet another string object";  

null 대신 항상 비어있는 컬렉션과 배열을 반환하라

여러분의 메서드가 컬렉션 요소 혹은 배열을 반환한다면, null이 아니라 비어있는 배열이나 컬렉션을 반환하는지 확인하세요. 이는 null 요소에 대한 많은 if else 검사를 절약해줍니다. 예를 들어, 아래 예제에 직원의 이름을 반환하는 getter 메서드가 있습니다. 이름이 만약 null이라면 간단히 빈 문자열인 ""을 반환하고 있습니다.

public String getEmployeeName() {  
    return (null==employeeName ? "": employeeName);
}

방어적인 복사본은 진리다

방어적인 복사본은 객체의 변이를 피하기 위해 생성된 복제 객체입니다. 예를 들어 아래 코드에서 객체가 생성될 때 초기화되는 private 필드인 생일을 지닌 Student 클래스를 정의하고 있습니다.

public class Student {  
    private Date birthDate;

    public Student(Date birthDate) {
        this.birthDate = birthDate;
    }

    public Date getBirthDate() {
        return this.birthDate;
    }
}

다음은 Student 객체를 사용하는 다른 코드입니다.

public static void main(String []arg) {

    Date birthDate = new Date();
    Student student = new Student(birthDate);

    birthDate.setYear(2019);

    System.out.println(student.getBirthDate());
}

위 코드에서 birthDate의 기본값을 지닌 Student 객체를 생성했습니다. 그러나 그 뒤에 birthDate의 year 값을 변경했습니다. 그래서 생일을 출력해보면, year가 2019로 변했다는 것을 알 수 있습니다.

이런 경우를 피하기 위해, 방어적인 복사본 메커니즘을 사용할 수 있습니다. Student 클래스의 생성자를 다음과 같이 변경합니다.

public Student(birthDate) {  
    this.birthDate = new Date(birthDate);
}

Student 클래스 내에서 사용하는 birthDate의 또 다른 복사본을 사용하고 있다는 것을 보장합니다.

절대 finally 블록 밖으로 예외가 나가지 않도록 하라

finally 블록은 예외를 던지는 코드를 절대 갖지 않도록 만드세요. 항상 finally 절이 예외를 던지지 않도록 확인해야 합니다. 예외를 던지는 finally 블록 내에 어떤 코드가 있다면, 적절히 예외를 로그로 남기고 절대 그것이 밖으로 나가지 않도록 만드세요 :)

절대 "Exception"을 던지지 마세요

결코 직접 java.lang.Exception을 던지지 마세요. 그것은 검사된 Exception의 목적을 무너뜨립니다. 또한 호출자 메서드에서 전달되는 유용한 정보가 존재하지 않습니다.

부동소수점 사용을 피하라

화폐량과 같은 정확한 수량을 표현하기 위해 부동소수점을 사용하는 것은 좋지 않은 생각입니다. 달러 및 센트 계산에 부동소수점을 사용하면 재앙이 닥칠 겁니다. 부동소수점은 시작되는 값이 근본적으로 정확하지 않은, 측정과 같은 값에 사용하는 것이 가장 적합합니다. 화폐량 계산에는 BigDecimal 사용이 더 좋습니다.

Virtual보다는 Static이 더 낫다

객체의 필드에 접근할 필요가 없다면, 메서드를 static으로 만드세요. 호출이 15%~20%는 더 빨라집니다. 또한 좋은 습관인데, 메서드의 시그니처를 통해 해당 메서드의 실행으로 객체의 상태가 변하지 않는다는 것을 암시할 수 있기 때문입니다.

상수에는 Static Final을 사용하라

클래스 제일 위에 다음과 같은 선언이 있다고 가정해보죠:

static int intVal = 42;  
static String strVal = "Hello, world!";  

컴파일러는 이라는 클래스 초기화 메서드를 만드는데, 클래스가 처음 사용될 때 실행됩니다. 해당 메서드는 intVal에 42라는 값을 저장하며, strVal에 대해서는 클래스 파일의 문자열 상수 테이블에서 참조를 추출합니다. 이런 값들이 이후에 참조될 때, 필드 탐색으로 접근됩니다.

이를 "final" 키워드로 향상시킬 수 있습니다.

static final int intVal = 42;  
static final String strVal = "Hello, world!";  

클래스는 더 이상 메서드를 요구하지 않는데, 그 이유는 상수가 dex 파일 내 정적 필드 이니셜라이저로 들어가기 때문입니다. intVal을 참조하는 코드는 직접 42라는 정수 값을 사용하고, strVal에 대한 접근은 필드 탐색 대신 상대적으로 비용이 적게 드는 "문자열 상수" 명령을 사용합니다.

참고: 이러한 최적화는 임의의 참조 타입이 아닌, 프리미티브 타입과 문자열 상수에만 적용됩니다. 그럼에도 불구하고, 가능하다면 상수를 final static 으로 선언하는 것은 좋은 습관입니다.

향상된 for 루프 문법을 사용하라

향상된 for 루프 (또는 "for-each" 루프라고 알려진)는 Iterable 인터페이스를 구현하는 컬렉션과 배열을 대상으로 사용할 수 있습니다. 컬렉션에서, hasNext()next()에 대한 인터페이스 호출로 iterator가 할당됩니다. ArrayList에서는, 손으로 직접 작성한 카운트 루프가 약 3배 더 빠르지만(JIT를 쓰든 안쓰든), 다른 컬렉션의 경우 향상된 for 루프 문법은 명시적인 iterator을 사용하는 것과 동일합니다.

배열을 통해 반복하는 경우를 위한 여러 대안이 있습니다:

static class Foo {  
    int mSplat;
}

Foo[] mArray = ...

public void zero() {  
    int sum = 0;
    for (int i = 0; i < mArray.length; ++i) {
        sum += mArray[i].mSplat;
    }
}

public void one() {  
    int sum = 0;
    Foo[] localArray = mArray;
    int len = localArray.length;

    for (int i = 0; i < len; ++i) {
        sum += localArray[i].mSplat;
    }
}

public void two() {  
    int sum = 0;
    for (Foo a : mArray) {
        sum += a.mSplat;
    }
}

zero()가 가장 느린데, JIT가 루프에 대한 매번의 반복마다 한번 배열의 길이를 가져오는 비용을 최적화하지 못하기 때문입니다.

one()이 더 빠릅니다. 모든 것을 로컬 변수에 놓음으로써, 탐색을 피했습니다. 배열 길이를 변수로 놓은 것만이 성능 상의 이득입니다.

two()가 JIT가 없는 장치의 경우 가장 빠르며, JIT를 갖춘 장치의 경우에는 one()과 별 차이가 없습니다. Java 프로그래밍 언어의 1.5 버전에서 도입된 향상된 for 루프 문법을 사용하고 있기 때문입니다.

그러므로, 기본적으로 향상된 for 루프를 사용해야 하며, 성능이 매우 중요한 ArrayList 반복의 경우 손으로 작성한 카운트 루프를 고려하시기 바랍니다.

Strength reduction

Strength reduction은 한 연산이 더 빠르게 실행되는 동일한 연산으로 대체될 때 일어납니다. Strength reduction의 가장 일반적인
예는 shift 연산자를 사용해 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 ;  

이렇게 작성하면 공통적인 하위 표현식이 한번만 계산되며, 두 계산식에서 사용됩니다.