26장: 코드 튜닝 기법

26장: 코드 튜닝 기법

  • 코드 튜닝은 성능 개선이 필요하고 코드 수준의 변경이 적절하다고 판단한 뒤 시도하는 소규모 최적화임.
  • 주된 목표는 실행 속도이며, 코드 크기 감소는 대개 클래스와 데이터의 재설계에서 더 크게 얻어짐.
  • 코드 구조를 개선하는 리팩터링과 달리, 코드 튜닝은 성능을 얻는 대신 가독성과 유지보수성을 떨어뜨릴 수 있음.
  • 모든 기법은 정답이 아니라 시도해 볼 발견법임. 실제 언어, 컴파일러, 라이브러리, 실행 환경에서 전후 성능을 측정해야 함.

26.1 논리

답을 알면 검사를 중단함

  • 단락 평가(short-circuit evaluation)를 사용해 결과가 정해진 뒤에는 나머지 조건을 계산하지 않음.
  • 원하는 값을 찾는 검색 루프도 break, return, 감시 값 등을 사용해 즉시 종료함.
  • 효과는 데이터 수와 원하는 값을 발견할 확률에 따라 달라지므로 실제 입력을 반영해 측정해야 함.
  • 예시

    // Bad: 음수를 찾은 뒤에도 배열 끝까지 검사함
    bool negativeFoundBad = false;
    for (int i = 0; i < count; i++) {
        if (input[i] < 0) negativeFoundBad = true;
    }
    
    // Good: 답을 아는 즉시 종료함
    bool negativeFoundGood = false;
    for (int i = 0; i < count; i++) {
        if (input[i] < 0) {
            negativeFoundGood = true;
            break;
        }
    }
    

빈도에 따라 검사를 정렬함

  • 빠르면서 참일 가능성이 높은 검사를 먼저 배치해 정상적인 경우가 적은 검사만 거치게 함.
  • case 문과 if-then-else 연쇄 모두에 적용할 수 있지만, 언어와 컴파일러의 구현 방식에 따라 효과가 반대로 나타날 수 있음.
  • 예시

    # Bad: 드문 입력부터 검사
    수학 기호 -> 숫자 -> 문장 부호 -> 공백 -> 알파벳
    
    # Good: 워드프로세서에서 자주 들어오는 입력부터 검사
    알파벳 -> 공백 -> 문장 부호 -> 숫자 -> 수학 기호
    

비슷한 논리 구조의 성능을 비교함

  • 같은 판단을 case 문과 if-then-else로 표현할 수 있다면 두 구조를 직접 측정해 더 나은 쪽을 선택함.
  • 같은 문자 분류 예제에서도 C#과 Visual Basic은 case가 빨랐지만 Java는 if-then-else가 약 6배 빨랐음.
  • 문법이 비슷한 언어라도 생성되는 코드가 달라 결과가 정반대일 수 있으므로 직관이나 경험 법칙만으로 결정하지 않음.
  • 예시
    • Bad: case가 항상 더 빠르다고 가정하고 바로 교체함.
    • Good: 같은 문자 분류를 case와 if-then-else로 각각 구현해 목표 언어·컴파일러에서 측정하고, 더 빠른 구현만 선택함.

복잡한 표현식을 테이블 조회로 대체함

  • 여러 조건을 거쳐 값을 분류하는 복잡한 논리는 입력 조합을 인덱스로 사용하는 테이블로 바꿀 수 있음.
  • 테이블 정의에는 충분한 설명이 필요하지만, 긴 조건문보다 수정하기 쉽고 조회 비용도 줄어들 수 있음.
  • 예시

    // Bad: 입력 조합이 늘수록 조건문이 복잡해짐
    if (!isMember && !hasCoupon) discount = 0;
    else if (isMember && !hasCoupon) discount = 10;
    else if (!isMember && hasCoupon) discount = 15;
    else discount = 20;
    
    // Good: 불린 값을 인덱스로 사용함
    static const int discountTable[2][2] = {
        { 0, 15 },
        { 10, 20 }
    };
    discount = discountTable[isMember][hasCoupon];
    

지연 평가를 사용함

  • 값이 실제로 필요할 때까지 계산을 미루고, 한 번 계산한 값은 이후 요청을 위해 저장함.
  • 전체 항목 중 일부만 사용하는 큰 테이블이라면 시작할 때 모두 계산하는 것보다 필요한 항목만 계산하고 캐시하는 편이 효율적일 수 있음.
  • 예시

    // Bad: 시작할 때 사용하지 않을 값까지 모두 계산함
    for (int i = 0; i < 5000; i++) table[i] = BuildValue(i);
    
    // Good: 처음 요청된 항목만 계산하고 보관함
    Value GetValue(int i) {
        if (!isReady[i]) {
            table[i] = BuildValue(i);
            isReady[i] = true;
        }
        return table[i];
    }
    

26.2 반복문

조건을 반복문 밖으로 옮김(unswitching)

  • 반복 중 바뀌지 않는 조건은 반복문 밖에서 한 번만 검사하고, 조건별 반복문을 따로 실행함.
  • 매회 조건을 검사하는 비용은 줄지만 코드가 중복되어 두 반복문을 함께 유지보수해야 하는 위험이 생김.
  • 예시

    // Bad: 같은 조건을 매번 검사함
    for (int i = 0; i < count; i++) {
        if (sumType == NET) netSum += amount[i];
        else grossSum += amount[i];
    }
    
    // Good: 변하지 않는 조건을 한 번만 검사함
    if (sumType == NET) {
        for (int i = 0; i < count; i++) netSum += amount[i];
    } else {
        for (int i = 0; i < count; i++) grossSum += amount[i];
    }
    

반복문을 결합함(jamming)

  • 같은 범위의 요소를 순회하는 여러 반복문을 하나로 합쳐 반복 제어 비용을 줄임.
  • 결합한 작업의 인덱스 범위와 실행 순서가 계속 호환되는지 확인해야 함.
  • 예시

    // Bad: 같은 범위를 두 번 순회함
    for (int i = 0; i < employeeCount; i++) names[i] = "";
    for (int i = 0; i < employeeCount; i++) earnings[i] = 0;
    
    // Good: 한 번의 순회에서 두 작업을 처리함
    for (int i = 0; i < employeeCount; i++) {
        names[i] = "";
        earnings[i] = 0;
    }
    

반복문을 펼침(unrolling)

  • 한 번의 반복에서 둘 이상의 요소를 처리해 조건 검사와 인덱스 증가 횟수를 줄임.
  • 요소 수가 홀수일 때 남는 항목을 별도로 처리해야 하며, 코드가 복잡해지고 off-by-one 오류 가능성이 커짐.
  • 한 번 펼친 실험에서 Java의 실행 시간은 43% 감소했지만 Python은 27% 증가했음. 더 많이 펼친다고 항상 빨라지는 것은 아니므로 가독성 손실과 측정된 이득을 함께 비교함.
  • 예시

    // Bad: 한 번에 한 항목만 처리함
    for (int i = 0; i < count; i++) values[i] = i;
    
    // Good: 한 번에 두 항목을 처리하고 남은 항목을 보정함
    int i = 0;
    for (; i + 1 < count; i += 2) {
        values[i] = i;
        values[i + 1] = i + 1;
    }
    if (i < count) values[i] = i;
    

반복문 안의 작업을 최소화함

  • 반복마다 값이 달라지지 않는 계산, 배열 참조, 포인터 역참조는 반복문 밖에서 한 번 수행함.
  • 복잡한 표현식을 의미 있는 지역 변수로 옮기면 실행 비용뿐 아니라 가독성도 좋아질 수 있음.
  • 예시

    // Bad: 같은 포인터 경로를 반복해서 따라감
    for (int i = 0; i < rateCount; i++) {
        netRate[i] = baseRate[i] * rates->discounts->factors->net;
    }
    
    // Good: 변하지 않는 값을 반복문 밖에 저장함
    const double quantityDiscount = rates->discounts->factors->net;
    for (int i = 0; i < rateCount; i++) {
        netRate[i] = baseRate[i] * quantityDiscount;
    }
    

감시 값을 사용함(sentinel)

  • 선형 검색 범위 바로 뒤에 반드시 검색을 끝내는 값을 두어, 매회 경계와 발견 여부를 함께 검사하지 않게 함.
  • 배열뿐 아니라 연결 리스트에도 적용할 수 있지만, 감시 값을 위한 공간과 충돌하지 않는 값을 신중히 선택해야 함.
  • 예시

    // Bad: 매회 값과 배열 경계를 함께 검사함
    int badIndex = 0;
    while (badIndex < count && items[badIndex] != target) badIndex++;
    bool foundWithBoundsCheck = badIndex < count;
    
    // Good: count 위치를 감시 값용으로 확보함
    const Item saved = items[count];
    items[count] = target;
    int sentinelIndex = 0;
    while (items[sentinelIndex] != target) sentinelIndex++;
    items[count] = saved;
    bool foundWithSentinel = sentinelIndex < count;
    

가장 많이 도는 반복문을 안쪽에 둠

  • 중첩 반복문에서는 반복 횟수가 큰 반복문을 안쪽에 두어 바깥 반복문의 초기화와 조건 검사 횟수를 줄일 수 있음.
  • 메모리 접근 순서와 컴파일러 최적화도 결과에 영향을 주므로 구조를 바꾼 뒤 측정해야 함.
  • 예시

    // Bad: 100회 도는 반복문이 바깥쪽에 있음
    for (int column = 0; column < 100; column++)
        for (int row = 0; row < 5; row++) sum += table[row][column];
    
    // Good: 5회 도는 반복문을 바깥쪽에 둠
    for (int row = 0; row < 5; row++)
        for (int column = 0; column < 100; column++) sum += table[row][column];
    

연산 강도를 낮춤

  • 반복 인덱스에 의존하는 곱셈을 누적 덧셈으로 바꾸는 것처럼 비싼 연산을 더 싼 연산으로 대체함.
  • 반복할 때 변하는 부분을 분리할 수 있어야 하며, 자료형과 실행 환경에 따라 기대한 이득이 없을 수도 있음.
  • 예시

    // Bad: 반복할 때마다 곱셈을 수행함
    for (int i = 0; i < saleCount; i++)
        commission[i] = (i + 1) * revenue * baseCommission * discount;
    
    // Good: 한 번 계산한 증가분을 누적함
    const double step = revenue * baseCommission * discount;
    double cumulative = step;
    for (int i = 0; i < saleCount; i++) {
        commission[i] = cumulative;
        cumulative += step;
    }
    

26.3 데이터 변환

부동소수점 대신 정수를 사용함

  • 필요한 범위와 정밀도를 정수로 표현할 수 있다면 정수 연산이 더 빠를 수 있음.
  • 특히 반복 인덱스처럼 정수가 자연스러운 값에 부동소수점형을 사용하지 않음.
  • 예시

    ' Bad: 반복 인덱스에 부동소수점형을 사용함
    Dim x As Single
    For x = 0 To 99
        values(x) = 0
    Next
    
    ' Good: 인덱스의 의미에 맞는 정수형을 사용함
    Dim i As Integer
    For i = 0 To 99
        values(i) = 0
    Next
    

배열 차원을 최소화함

  • 다차원 배열을 일차원 배열로 펼치면 인덱스 계산과 반복 제어 비용을 줄일 수 있음.
  • 언어별 차이가 크고 가독성과 인덱스 오류 위험이 달라지므로 실제 효과를 확인한 뒤 적용함.
  • 예시

    // Bad: 행과 열을 따로 순회함
    for (int row = 0; row < numRows; row++) {
        for (int column = 0; column < numColumns; column++)
            matrix2D[row][column] = 0;
    }
    
    // Good: 같은 데이터를 일차원으로 표현함
    for (int entry = 0; entry < numRows * numColumns; entry++)
        matrix1D[entry] = 0;
    

배열 참조를 최소화함

  • 안쪽 반복문에서 변하지 않는 배열 요소를 지역 변수에 저장해 반복적인 배열 접근을 피함.
  • 같은 변경으로 실행 시간이 C++에서는 7% 증가하고 Visual Basic에서는 20% 감소했음. 일부 컴파일러는 이미 같은 최적화를 수행하므로 수동 변경이 오히려 느려질 수도 있음.
  • 예시

    // Bad: 안쪽 반복문에서 같은 배열 요소를 계속 조회함
    for (int type = 0; type < typeCount; type++) {
        for (int level = 0; level < levelCount; level++)
            rate[level] *= discount[type];
    }
    
    // Good: 바깥 반복에서 한 번만 조회함
    for (int type = 0; type < typeCount; type++) {
        double thisDiscount = discount[type];
        for (int level = 0; level < levelCount; level++)
            rate[level] *= thisDiscount;
    }
    

보조 인덱스를 사용함

  • 가변 길이 자료에는 길이를 함께 저장해 매번 처음부터 세지 않게 함.
  • 큰 레코드나 디스크 자료는 키와 위치만 담은 작은 병렬 인덱스를 메모리에서 정렬·검색하고, 최종 자료에 한 번만 접근함.
  • 예시

    // Bad: 길이가 필요할 때마다 연결 리스트 전체를 순회함
    int length(Node node) {
        int count = 0;
        while (node != null) {
            count++;
            node = node.next;
        }
        return count;
    }
    
    // Good: 추가·삭제 시 함께 갱신한 길이 인덱스를 조회함
    final class NodeList {
        Node head;
        int length;
    }
    int length(NodeList list) { return list.length; }
    

캐시를 사용함

  • 자주 읽는 디스크 레코드나 계산 비용이 큰 결과를 저장해 같은 요청을 빠르게 처리함.
  • 캐시의 가치는 새 값을 만드는 비용, 캐시 접근·저장 비용, 적중 빈도에 따라 결정됨.
  • 캐시는 공간과 복잡성을 늘리고 오류 가능성도 높이므로 이득이 충분할 때만 사용함.
  • 예시

    // Bad: 같은 입력에도 제곱근을 다시 계산함
    double hypotenuse(double a, double b) {
        return Math.sqrt(a * a + b * b);
    }
    
    // Good: 직전 입력과 같으면 저장한 결과를 반환함
    double cachedHypotenuse(double a, double b) {
        if (hasCache && a == cachedA && b == cachedB) return cachedValue;
        cachedA = a;
        cachedB = b;
        cachedValue = Math.sqrt(a * a + b * b);
        hasCache = true;
        return cachedValue;
    }
    

26.4 표현식

대수 항등식을 활용함

  • 논리적으로 같은 식 중 연산 수가 적은 형태를 선택함. 예를 들어 not a and not b는 not (a or b)로 바꿀 수 있음.
  • sqrt(x) < sqrt(y)를 x < y로 바꾸는 것처럼 결과에 필요 없는 비싼 계산을 제거하면 큰 개선을 얻을 수 있음.
  • 예시

    // x와 y는 0 이상이라고 가정함
    
    // Bad: 비교를 위해 제곱근 두 번을 계산함
    boolean xIsSmallerWithSqrt = Math.sqrt(x) < Math.sqrt(y);
    
    // Good: 같은 결과를 직접 비교함
    boolean xIsSmallerDirectly = x < y;
    

연산 강도를 낮춤

  • 곱셈은 덧셈으로, 거듭제곱은 곱셈으로, 삼각 함수는 항등식으로 대체할 수 있음.
  • 부동소수점은 고정소수점이나 정수로, 배정밀도는 단정밀도로, 2의 곱셈·나눗셈은 시프트로 바꿀 수 있음.
  • 이론상 연산이 더 적어도 실제 결과가 느릴 수 있으므로 각 대안을 측정함.
  • 예시

    // Bad: 반복할 때마다 x의 거듭제곱을 다시 계산함
    double valueWithPow = coefficient[0];
    for (int power = 1; power <= order; power++)
        valueWithPow += coefficient[power] * Math.pow(x, power);
    
    // Good: 이전 거듭제곱에 x를 곱해 다음 값을 만듦
    double valueWithMultiplication = coefficient[0];
    double powerOfX = x;
    for (int power = 1; power <= order; power++) {
        valueWithMultiplication += coefficient[power] * powerOfX;
        powerOfX *= x;
    }
    

컴파일 시점에 초기화함

  • 변하지 않는 식은 실행할 때 반복 계산하지 말고 미리 계산한 이름 있는 상수로 둠.
  • 상수는 의미를 설명하는 이름을 사용하고 필요한 정밀도를 유지함.
  • 예시

    // Bad: 호출할 때마다 log(2)를 계산함
    unsigned int Log2WithRepeatedConstant(unsigned int x) {
        return static_cast<unsigned int>(log(x) / log(2.0));
    }
    
    // Good: 변하지 않는 값을 미리 계산함
    constexpr double LN_2 = 0.6931471805599453;
    unsigned int Log2WithPrecomputedConstant(unsigned int x) {
        return static_cast<unsigned int>(log(x) / LN_2);
    }
    

시스템 루틴을 주의함

  • 범용 수학 루틴은 최악의 경우와 높은 정밀도를 지원하므로 필요한 결과보다 비용이 클 수 있음.
  • 정수 결과만 필요하다면 부동소수점 로그 함수 대신 정수 비교나 비트 연산처럼 문제에 맞춘 계산을 검토함.
  • 한 번의 성공적인 최적화에 멈추지 말고 더 단순하고 빠른 대안이 있는지 계속 측정함.
  • 예시

    // Bad: 정수 결과를 얻으려고 부동소수점 시스템 루틴을 사용함
    unsigned int Log2WithSystemRoutine(unsigned int x) {
        return static_cast<unsigned int>(log(x) / log(2.0));
    }
    
    // Good: 양의 정수에 필요한 연산만 수행함
    unsigned int Log2WithIntegerOperations(unsigned int x) {
        unsigned int result = 0;
        while (x >= 2) {
            x >>= 1;
            result++;
        }
        return result;
    }
    

올바른 형의 상수를 사용함

  • 상수와 대입 대상 변수의 형을 맞춰 불필요한 실행 시점 형 변환을 피함.
  • 최적화 컴파일러는 변환을 미리 처리할 수 있지만 인터프리터나 단순한 컴파일러는 그렇지 않을 수 있음.
  • 예시

    // Bad: 대상 변수와 다른 형의 상수를 사용함
    double convertedRate = 5;
    int convertedCount = 3.0;
    
    // Good: 대상 변수와 같은 형의 상수를 사용함
    double exactRate = 5.0;
    int exactCount = 3;
    

결과를 미리 계산함

  • 반복해서 쓰는 비싼 결과는 한 번 계산한 뒤 조회하는 편이 나을 수 있음.
  • 대출 계산의 일부를 테이블로 만든 예제는 실행 시간이 Java에서 92% 감소했지만 Python에서는 20% 증가했고, 같은 계산을 반복문 밖으로 옮기자 두 언어 모두 실행 시간이 감소했음.
  • 미리 계산하는 방법은 다음과 같음.
    • 실행 전에 계산해 컴파일 시점 상수나 실행 시점 변수에 넣음.
    • 실행 전에 계산한 결과를 파일에 저장하고 시작할 때 읽음.
    • 프로그램 시작 시 한 번 계산한 뒤 계속 참조함.
    • 반복문 밖에서 가능한 부분을 먼저 계산해 안쪽 작업을 줄임.
    • 처음 필요할 때 계산한 뒤 캐시해 다시 사용함.
  • 예시

    // Bad: 모든 대출 금액에서 같은 분모를 다시 계산함
    for (long loan = MIN_LOAN; loan <= MAX_LOAN; loan++) {
        double paymentWithRepeatedDivisor = loan / ((1.0 - Math.pow(
            1.0 + interestRate / 12.0, -months)) / (interestRate / 12.0));
        process(paymentWithRepeatedDivisor);
    }
    
    // Good: 반복문 밖에서 공통 분모를 한 번 계산함
    double monthlyRate = interestRate / 12.0;
    double divisor =
        (1.0 - Math.pow(1.0 + monthlyRate, -months)) / monthlyRate;
    for (long loan = MIN_LOAN; loan <= MAX_LOAN; loan++) {
        double paymentWithPrecomputedDivisor = loan / divisor;
        process(paymentWithPrecomputedDivisor);
    }
    

공통 부분 표현식을 제거함

  • 같은 표현식이 여러 번 나타나면 의미 있는 변수에 한 번 계산해 저장함.
  • 컴파일러가 이미 제거했거나 다른 비싼 연산이 전체 비용을 지배하면 효과가 작을 수 있음.
  • 예시

    // Bad: interestRate / 12.0을 두 번 계산함
    double paymentWithRepeatedExpression = loanAmount /
        ((1.0 - Math.pow(1.0 + interestRate / 12.0, -months)) /
        (interestRate / 12.0));
    
    // Good: 공통 부분 표현식에 이름을 붙여 한 번만 계산함
    double monthlyInterest = interestRate / 12.0;
    double paymentWithNamedExpression = loanAmount /
        ((1.0 - Math.pow(1.0 + monthlyInterest, -months)) /
        monthlyInterest);
    

26.5 루틴

  • 작고 역할이 분명한 루틴은 중복 코드를 줄이고, 한 곳의 최적화가 모든 호출부에 적용되게 함.
  • 필요한 경우 저수준 언어로 다시 작성하기도 쉬우므로 좋은 루틴 분해 자체가 강력한 튜닝 기반임.
  • 예시
    • Bad: screenTotal()과 reportTotal()에 같은 세금 계산을 복사하면 최적화와 수정도 두 곳에서 반복해야 함.
    • Good: 공통 계산을 CalculateTotal()로 분리하면 캐시나 더 빠른 알고리즘을 한 번 적용해 모든 호출부가 함께 이득을 얻음.

루틴을 인라인으로 다시 작성함

  • 현대 환경에서는 루틴 호출 비용이 매우 작아 인라인화가 성능을 높인다고 단정할 수 없음.
  • 언어의 inline 기능이나 매크로를 사용할 수 있지만, 코드 중복과 크기 증가가 생기고 실제로 더 느려질 수도 있음.
  • 예시

    // Bad: 측정 없이 루틴 본문을 모든 호출부에 복사함
    checksum += bytes[i] & 0xff;
    
    // Good: 호출 비용이 병목으로 확인된 경우에만 한 정의를 인라인화함
    inline int ByteValue(const unsigned char* bytes, int i) {
        return bytes[i] & 0xff;
    }
    checksum += ByteValue(bytes, i);
    

26.6 저수준 언어로 다시 작성함

  1. 응용 프로그램 전체를 고급 언어로 작성함.
  2. 충분히 테스트해 정확성을 확인함.
  3. 성능 개선이 필요하면 프로파일링으로 과열지점을 찾음.
  4. 실행 시간을 지배하는 작은 부분만 어셈블리어나 C 같은 저수준 언어로 다시 작성함.
  • 예시

    # Bad: 바이트마다 저수준 루틴을 호출해 언어 경계를 반복해서 넘음
    for i in range(len(data)):
        data[i] = native_xor_byte(data[i], key)
    
    # Good: 측정된 과열 반복문 전체를 저수준 루틴 하나로 옮김
    native_xor_buffer(data, key)
    
  • 비트 조작처럼 저수준 언어가 잘하는 작업은 속도와 코드 크기를 함께 개선할 수 있음.
  • 컴파일러가 생성한 어셈블리 목록을 출발점으로 삼아 작은 단계로 손보고, 단계마다 정확성과 성능을 확인함.
  • 비트 조작 예제에서는 어셈블리어로 옮긴 뒤 고급 언어판 대비 실행 시간이 C++에서 29%, Delphi에서 41% 감소했음.
  • 얻는 이득이 언어 간 경계, 이식성, 디버깅과 유지보수 비용을 감당할 만큼 큰지 판단해야 함.

체크리스트: 코드 튜닝 기법

속도와 크기를 모두 개선함

  • 복잡한 논리를 테이블 조회로 대체했는가?
  • 반복문을 결합했는가?
  • 부동소수점 변수 대신 정수 변수를 사용했는가?
  • 데이터를 컴파일 시점에 초기화했는가?
  • 올바른 형의 상수를 사용했는가?
  • 결과를 미리 계산했는가?
  • 공통 부분 표현식을 제거했는가?
  • 핵심 루틴을 저수준 언어로 옮겼는가?

속도를 개선함

  • 답을 알면 검사를 중단했는가?
  • case 문과 if-then-else 연쇄의 검사를 빈도순으로 배치했는가?
  • 비슷한 논리 구조의 성능을 비교했는가?
  • 지연 평가를 사용했는가?
  • 반복문 안의 변하지 않는 조건을 밖으로 옮겼는가?
  • 반복문을 펼쳤는가?
  • 반복문 안의 작업을 최소화했는가?
  • 검색 반복문에 감시 값을 사용했는가?
  • 중첩 반복문에서 가장 많이 도는 반복문을 안쪽에 두었는가?
  • 반복문 안의 연산 강도를 낮췄는가?
  • 다차원 배열을 일차원 배열로 바꿨는가?
  • 배열 참조를 최소화했는가?
  • 데이터형에 보조 인덱스를 추가했는가?
  • 자주 쓰는 값을 캐시했는가?
  • 대수 항등식을 활용했는가?
  • 논리·수학 표현식의 연산 강도를 낮췄는가?
  • 시스템 루틴의 비용과 정밀도를 검토했는가?
  • 루틴 인라인화를 측정해 보았는가?

26.7 변할수록 그대로인 것

  • 컴퓨터가 빨라지고 메모리가 늘어 일반적인 데스크톱 프로그램에서는 미세한 코드 튜닝의 영향이 거의 보이지 않을 수 있음.
  • 임베디드·실시간 시스템처럼 속도나 공간 제약이 엄격한 환경에서는 여전히 중요한 기법임.
  • 최적화 결과는 언어, 컴파일러와 버전, 라이브러리, 설정에 따라 달라져 과거보다도 예측하기 어려움.
  • 코드 튜닝은 성능과 복잡성·가독성·단순성·유지보수성 사이의 교환이며, 환경이 바뀔 때마다 다시 프로파일링해야 하는 비용도 만듦.
  • 프로파일러를 꺼내 효과를 확인할 만큼 중요한 최적화만 적용함. 측정할 가치가 없다면 코드를 어렵게 만들 가치도 없음.
  • 예시
    • Bad: 측정하지 않고 시프트가 곱셈보다 빠를 것이라 가정해 total += value << 1로 바꿈.
    • Good: 두 표현의 실행 시간이 같다면 의미가 분명한 total += value * 2를 유지하고, 목표 환경에서 차이가 확인될 때만 변경함.

참고 자료

  • 존 벤틀리, Writing Efficient Programs (Prentice Hall, 1982): 시간과 공간의 교환, 데이터형 재설계, 반복적인 튜닝 과정을 폭넓게 다룸.
  • 존 벤틀리, Programming Pearls, 2nd ed. (Addison-Wesley, 2000): 부록 4에 코드 튜닝 규칙을 요약함.
  • 릭 부스, Inner Loops (Addison-Wesley, 1997): 빠른 32비트 소프트웨어 개발을 다룸.
  • 리처드 거버, Software Optimization Cookbook (Intel Press, 2002): 인텔 아키텍처의 고성능 최적화 기법을 다룸.
  • 제프리 하산·케네스 투, Performance Tuning and Optimizing ASP.NET Applications (Apress, 2003): ASP.NET 응용 프로그램 최적화를 다룸.
  • 패트릭 킬릴리, Web Performance Tuning, 2nd ed. (O’Reilly, 2002): 웹 성능 튜닝을 다룸.
  • 크레이그 라만·렛 거스리, Java 2 Performance and Idiom Guide (Prentice Hall, 2000): 자바 성능과 관용구를 다룸.
  • 잭 시라지, Java Performance Tuning (O’Reilly, 2000), 스티브 윌슨·제프 케슬먼, Java Platform Performance (Addison-Wesley, 2000): 자바 플랫폼의 성능 전략을 다룸.

요점 정리

  • 최적화 결과는 언어, 컴파일러, 실행 환경에 따라 크게 달라진다. 각 변경을 측정하지 않으면 프로그램을 빠르게 할지 느리게 할지 알 수 없다.
  • 첫 번째 최적화가 최선인 경우는 드물다. 효과가 있는 방법을 찾은 뒤에도 더 나은 대안을 계속 시험해야 한다.
  • 코드 튜닝은 신뢰성과 유지보수성을 해칠 수 있지만 적절한 보호 장치와 측정 아래에서는 유용하다. 적용하기로 했다면 신중하게 사용한다.

results matching ""

    No results matching ""