25장 - 코드 튜닝 전략

Edited by / 임수형 (sulogc)

이 장은 역사적으로 논란이 많았던 성능 튜닝 문제를 논의한다. 1960년대, 1980년대, 2000년대 등 컴퓨터 자원이 제한적일 때는 효율화가 최고의 관심사였고, 때론 성능 개선의 노력이 코드를 이해하고 유지보수기 어렵게 만들기도 하였다. 성능은 전략과 전술이라는 두 가지 측면에서 설명 할 수 있다.

  • 전략: 정의, 중요도, 개선 접근법

추후 최적화 할 때 시간낭비하지 않도록 대충이라도 읽어보자.

25-1 성능이란?

프로그램 성능 향상에 코드 튜닝과 그 외외의 방법에 대해 알아보자.

a. 품질의 특성과 성능

개발자들은 코드를 더 잘 만들수록 고객이 소프트웨어를 더 좋아할 것으로 생각한다. 그러나 사용자는 코드의 품질보다 구체적인 프로그램의 특성에 더 많은 관심을 둔다.

디지털 카메라의 사진을 윈도우에 6장씩 나누는 대신 한 번에 업로드하는 소프트웨어는 그 업로드 속도에 관계없이 성능이 좋다고 느낀다.

성능과 코드 속도가 관련이 없는지 파악해야 한다.

b. 성능과 코드 튜닝

속도나 크기 등 효율화를 최우선 순위로 선택했다면, 뭘 향상 시킬지보다 각 관점에서 효율화가 무엇일지 생각한다.

  • 프로그램 요구사항

배리 보엠은 TRW 시스템 구축시 1초 이하의 응답시간이란 요구사항에 1억 달러가 소모될 것을 예상했다. 요구사항을 분석한 결과 사용자는 실행 시간의 90% 정도에 대해 4초 이하의 응답 시간에 만족할 것이라고 판단했고, 시스템 비용이 7000만 달러로 줄어들었다. 성능 문제를 해결하기 전에 문제를 잘 정의해야 한다.

  • 프로그램 설계

프로그램 설계에는 주로 프로그램을 여러 개의 클래스로 나누는 작업이 포함된다. 설계에 따라 성능이 결정되기도 한다.

데이터 측정 소프트웨어를 예로, 13제곱한 변수를 포함한 13차 다항식 계산 최적화를 다른 하드웨어와 수십 개의 3차 다항식을 해결하는 상위 레벨 설계로 해결했다.

프로그램의 크기와 속도의 목표를 합리적으로 달성할 수 있게 아키텍처를 설계해야한다.

  • 클래스와 루틴 설계

이 수준에서는 데이터형과 알고리즘을 선택하는 것으로, 프로그램의 메모리 사용과 실행 속도에 영향을 미친다.

  • 운영체제 상호작용

프로그램이 외부 파일이나 동적 메모리, 출력 장치를 다룬다면 운영체제와 상호작용할 것이다. 컴파일러는 예상치 못 한 시스템 호출을 일으킬 수 있으니 주의해야한다.

  • 코드 컴파일
  • 하드 웨어
  • 코드 튜닝

코드 튜닝은 코드가 더 효율적으로 작동하도록 정확하게 수정하는 방법이다. 코드 튜닝은 단일 클래스나 단일 루틴, 더 일반적으로 몇 줄의 코드에 영향을 미치는 작은 규모의 변경을 가리킨다.

25-2 코드 튜닝 소개

코드 튜닝은 성능을 향상시키는 가장 효과적인 방법은 아니다. 프로그램 아키텍처, 클래스 설계, 알고리즘 선택이 일반적으로 더 극적인 향상을 가져온다. 또한, 하드웨어 구입 등이 훨씬 더 쉽다. 게다가 코드를 수동으로 조정하기에 시간이 들고 유지보수에 좋지 않아 성능을 향상시키는 가장 저렴한 방법도 아니다.

그러나 낭만이 있다.

한 가지 매력은 자연의 법칙을 무시하는 것처럼 보인다는 것이다. 
20마이크로 초에 실행되는 루틴을 몇 줄을 변경하여 2마이크로 초에 실행되게 만들면 정말 흐뭇하다. 

a. 파레토 법칙.

파레토 법칙은 20%의 노력으로 80%의 결과를 얻을 수 있다는 법칙이다. 베리보엠은 프로그램의 20%에 해당하는 루틴이 실행시간 80%를 소비한다고 보고했다.

커누스는 포트란 프로그램의 실증적 연구에서 프로그램의 4% 이하가 일반저긍로 실행 시간의 50% 이상을 차지한다는 사실을 발견했다.

코드를 측정해 과열지점을 찾아야 하고, 가장 많이 사용되는 몇 퍼센트를 최적화하는 데 노력을 집중해야 한다.

b. 노부인들의 이야기

다음에 소개하는 일반적인 오해를 보자.

  • 고급 언어에서 코드를 줄이면 결과적으로 기계어 코드의 속도나 크기를 향상시킨다.
  • 어떤 연산이 아마 다른 것보다 빠르거나 작을 것이다.

성능에 대해서 말할 때 “아마”라는 말은 통하지 않는다. 성능은 직접 측정해야하며, 환경에 따라 얼마든지 다를 수 있다. 즉 컴파일러에 따라 코드 튜닝이 악영향이 되기도 한다.

  • 코드를 작성하면서 최적화해야 한다.

이는 세부적인 최적화를 하느라 전역적 최적화를 놓칠 수 있다. 프로그램이 완벽하게 작동되기 전까지는 성능 병목을 규명하기가 거의 불가능하다. 개발자들이 병목을 정확하게 파악하기 드문 경우 알아낸 병목을 지나치게 없애서 다른 부분을 위태롭게 만들기도 한다. 개발 초기 최적화에 초점을 맞추면 프로그램의 본래 목표를 잘성하기가 어려워진다.

  • 빠른 프로그램은 정확한 프로그램만큼 중요하다. (작동하지 않는 빠른 프로그램을 필요없다.)

c. 튜닝 시점

고급 설계를 사용하라. 나중에 작업하기 쉽게 모듈화하고 변경하기 쉽게 만들어라. 그리고 제대로 완성되었을 때 성능을 검사하라.

25-3 느리고 비대한 부분

코드를 튜닝할 때는 프로그램에서 느리고 비대한 부분을 찾아 빠르고 날씬하게 변경해야한다. 어떤 부분이 느리고 비대한지 항상 프로그램을 프로파일링 해야되지만, 일부 연산이 오랫동안 느리고 비대한 역사를 가지고 잇으니 먼저 살펴보자.

a. 비효율성의 공통적인 원인

다음은 비효율성의 공통적인 원인이다.

  • 불필요한 입출력(I/O)

  • Page Fault

기준을 행, 열에 둠에 따라 페이지 폴트가 빈번하게 일어날 수 있다.

for ( column = 0; column < MAX_COLUMNS; column++ ) {
    for ( row = 0; row < MAX_ROWS; row++ ) {
        table[ row ][ column ] = BlankTableElement();
    }
}

for ( row = 0; row < MAX_ROWS; row++ ) {
    for ( column = 0; column < MAX_COLUMNS; column++ ) {
        table[ row ][ column ] = BlankTableElement();
        }
}
  • system call

context switching을 야기할 수 있기 때문에 매우 값비싼 연산이다.

  • 인터프리터 언어
  • 오류

25-4 측정

일반적으로 프로그램의 작은 부분이 실행 시간을 많이 차지하기 때문에, 코드를 측정해 그러한 과열지점을 찾은 후 최적화하고, 다시 측정해 코드가 얼마나 향상되었는지 평가한다.

  • 성능의 많은 측면이 직관에 반한다.
  • 경험은 최적화에 큰 도움을 주지 못한다.
sum = 0;
for ( row = 0; row < rowCount; row++ ) {
    for ( column = 0; column < columnCount; column++ ) {
        sum = sum + matrix[ row ][ column ];
    }
}


sum = 0;
elementPointer = matrix;
lastElementPointer = matrix[ rowCount - 1 ][ columnCount - 1 ] + 1;
while ( elementPointer < lastElementPointer ) {
    sum = sum + *elementPointer++;
}

위와 같이 배열 접근에서 발생하는 지연을 줄이기 위해 포인터를 사용해서 최적화를 하였으나, 연산에 소요되는 시간은 똑같았다. 컴파일러가 생성한 어셈블리 코드를 살펴본 결과, 이미 컴파일러의 최적화 담당 기능이 배열 접근을 포인터로 변환하고 있었다.

a. 측정은 정확해야 한다.

성능 측정은 정확해야 한다. 프로파일링 도구가 유용하고, 아니면 시스템 시간과 연산을 수행하는 데 걸린 시간을 기록하는 루틴을 사용해도 된다.

추가로 코드 실행 시간만 측정하고 싶으면 CPU clock tick 수를 사용하도록 한다.

25-5 반복

성능 병목을 파악하고 나면 코드 튜닝을 통해 얼마나 많이 성능을 향상시킬 수 있는지에 대해 놀랄 것이다.

데이터 암호화 표준을 구현한 소프트웨어 작성시 반복적으로 여러 조합의 최적화를 진행하여 성능 목표를 달성하였다.