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:
복잡한 표현식을 테이블 조회로 대체함
- 여러 조건을 거쳐 값을 분류하는 복잡한 논리는 입력 조합을 인덱스로 사용하는 테이블로 바꿀 수 있음.
- 테이블 정의에는 충분한 설명이 필요하지만, 긴 조건문보다 수정하기 쉽고 조회 비용도 줄어들 수 있음.
-
예시
// 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()로 분리하면 캐시나 더 빠른 알고리즘을 한 번 적용해 모든 호출부가 함께 이득을 얻음.
- Bad:
루틴을 인라인으로 다시 작성함
- 현대 환경에서는 루틴 호출 비용이 매우 작아 인라인화가 성능을 높인다고 단정할 수 없음.
- 언어의
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 저수준 언어로 다시 작성함
- 응용 프로그램 전체를 고급 언어로 작성함.
- 충분히 테스트해 정확성을 확인함.
- 성능 개선이 필요하면 프로파일링으로 과열지점을 찾음.
- 실행 시간을 지배하는 작은 부분만 어셈블리어나 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를 유지하고, 목표 환경에서 차이가 확인될 때만 변경함.
- Bad: 측정하지 않고 시프트가 곱셈보다 빠를 것이라 가정해
참고 자료
- 존 벤틀리, 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): 자바 플랫폼의 성능 전략을 다룸.
요점 정리
- 최적화 결과는 언어, 컴파일러, 실행 환경에 따라 크게 달라진다. 각 변경을 측정하지 않으면 프로그램을 빠르게 할지 느리게 할지 알 수 없다.
- 첫 번째 최적화가 최선인 경우는 드물다. 효과가 있는 방법을 찾은 뒤에도 더 나은 대안을 계속 시험해야 한다.
- 코드 튜닝은 신뢰성과 유지보수성을 해칠 수 있지만 적절한 보호 장치와 측정 아래에서는 유용하다. 적용하기로 했다면 신중하게 사용한다.