25장: 코드 튜닝 전략

25장: 코드 튜닝 전략

25.1 성능이란?

품질 특성과 성능

  • 사용자는 코드의 순수 품질보다 업무 효율성을 높여주는 프로그램 특성에 더 관심을 둠.
    • 예: 파일 전송 코드가 약간 빠른 소프트웨어보다 조작 절차를 대폭 줄여주는 카드리더기가 더 뛰어난 성능 경험을 제공함.
  • 오히려 실행 속도 향상 코드에 지나치게 집중하면 유지보수성이나 가독성 등 다른 중요한 품질 특성을 해칠 수 있음.

성능 개선 관점과 코드 튜닝

  • 프로그램 요구사항: 과도한 성능 요구사항은 비현실적인 설계 복잡도와 프로젝트 비용을 유발하므로 사용자 요구를 재분석해 정밀 조정해야 함.

  • 프로그램 설계: 아키텍처 수준에서 자원 목표를 설정하고 높은 모듈화 구조를 갖춰야 비효율적 컴포넌트의 손쉬운 교체가 가능함.
  • 클래스와 루틴 설계: 적절한 데이터 구조와 알고리즘을 선택하는 것이 핵심임.
    • 예: 버블 정렬 대신 퀵 정렬, 선형 검색 대신 이진 검색
  • 운영체제 상호작용: 파일 및 메모리 I/O 과정에서 예상치 못한 운영체제 루틴 호출이나 라이브러리 과부하가 발생할 수 있음.
  • 코드 컴파일: 고성능 컴파일러의 최적화 기능을 활용하면 수동 튜닝 없이도 최적화된 기계어 코드를 얻을 수 있음.
  • 하드웨어: 소수 고객을 위한 주문형 소프트웨어의 경우 하드웨어 업그레이드가 인건비와 유지보수 비용을 줄이는 가장 경제적인 최적화 대안임.
  • 코드 튜닝: 소규모 코드(단일 루틴이나 몇 줄의 문장)를 정확히 수정하여 성능을 높이는 전술적 접근 방법임.

25.2 코드 튜닝 소개

코드 튜닝의 매력과 문제점

  • 코드 튜닝은 성능을 향상시키는 가장 효과적이거나 쉽고 저렴한 방법이 아님.

  • 코드 몇 줄을 수정해 실행 시간을 대폭 줄이는 만족감과 프로그래밍 문화의 명성 때문에 매력적으로 느껴짐.
  • 하지만 미세하게 효율적인 코드가 항상 더 나은 코드는 아니며 품질을 저해할 수 있음.

파레토 법칙 (80/20 법칙)

  • 전체 실행 시간의 80%는 코드 전체의 20% 이하(커누스의 연구에 따르면 4% 이하)에서 소비됨.
  • 줄 계수 프로파일러 등으로 측정해 과열지점을 찾은 후 해당 4% 영역 최적화에 집중해야 함.

  • 무작정 코드를 최적화하면 영향력이 없는 유휴 루프만 재작성하는 실수를 범할 수 있음.
  • 시스템을 먼저 완성하여 작동하게 만든 후, 핵심적인 극소수 영역에 최적화를 집중해야 함.

코드 튜닝에 대한 오해 (노부인들의 이야기)

  • 고급 언어의 코드를 줄이면 기계어 코드의 속도나 크기가 향상됨: 거짓임. 10번 반복하는 for 루프보다 10줄로 늘려 쓴 직선형 코드가 비주얼 베이직과 자바에서 60% 이상 빠른 성능을 보임. 고급 언어의 코드 줄 수와 최종 성능 간에는 아무 연관성이 없음.

  • 어떤 연산이 아마 다른 것보다 빠르거나 작을 것임: 거짓임. 프로그래밍 언어, 컴파일러 버전, 라이브러리, 프로세서에 따라 성능 특성이 달라지므로 항상 직접 측정해서 확인해야 함.
  • 코드를 작성하면서 최적화해야 함: 거짓임. 완성 전에는 전체 시간의 50%를 차지하는 4%의 병목을 파악할 수 없어 불필요한 96%의 코드를 최적화하느라 시간을 낭비하게 됨. 또한 가독성, 정보 은닉, 정확성 등 더 중요한 품질 목표를 해침.
  • 빠른 프로그램은 정확한 프로그램만큼 중요함: 거짓임. 올바르게 작동하지 않는 프로그램은 아무리 실행 속도가 빨라도 무의미함.

올바른 튜닝 시점

  • 고수준 설계를 적용해 모듈화와 변경 용이성을 확보하며 프로그램을 정확하게 작성함.
  • 프로그램이 완성되고 정상 동작할 때 성능을 측정하고, 실제 튜닝이 필요하다고 판단될 때만 최적화를 진행함.

컴파일러 최적화

  • 최신 컴파일러의 최적화 기능은 복잡하고 교묘한 코드보다 직관적이고 분명한 코드를 더 잘 최적화함.
  • 수동 코드 튜닝 전에 최적화 컴파일러 옵션을 설정하는 것만으로도 40% 이상의 성능 향상을 기대할 수 있음.
  • 컴파일러나 자바 가상 머신(JVM)의 성능 특성을 측정하여 적합한 환경을 선택하는 것이 우선임.

    언어 / 환경 컴파일러의 최적화를 사용하지 않은 시간 컴파일러의 최적화를 사용한 시간 시간 절약 성능 비율
    C++ 컴파일러 1 2.21 1.05 52% 2:1
    C++ 컴파일러 2 2.78 1.15 59% 2.5:1
    C++ 컴파일러 3 2.43 1.25 49% 2:1
    C# 컴파일러 1.55 1.55 0% 1:1
    비주얼 베이직 1.78 1.78 0% 1:1
    자바 VM 1 2.77 2.77 0% 1:1
    자바 VM 2 1.39 1.38 <1% 1:1
    자바 VM 3 2.63 2.63 0% 1:1

25.3 느리고 비대한 부분

  • 코드를 튜닝할 때는 프로그램에서 느리고 비대한 부분을 찾아 집중적으로 최적화해야 함.
  • 어떤 부분이 느리고 비대한지 확실히 파악하기 위해 항상 프로파일링을 우선 수행해야 함.

비효율성의 공통적인 원인

  • 불필요한 I/O 연산 방지: 입력/출력(I/O) 연산은 성능 저하의 주원인이므로, 공간이 허용한다면 인메모리 데이터 구조를 활용함.
  • 메모리 페이징: OS 메모리 페이지 교체 연산은 단일 페이지 연산보다 훨씬 느리므로, 배열 접근 순서를 조정해 페이지 오류를 최소화해야 함.
      // 1. 페이지 오류를 많이 유발하는 초기화 루프 (열 중심 접근)
      for ( column = 0; column < MAX_COLUMNS; column++ ) {
          for ( row = 0; row < MAX_ROWS; row++ ) {
              table[ row ][ column ] = BlankTableElement();
          }
      }
    
      // 2. 페이지 오류를 줄인 초기화 루프 (행 중심 접근)
      for ( row = 0; row < MAX_ROWS; row++ ) {
          for ( column = 0; column < MAX_COLUMNS; column++ ) {
              table[ row ][ column ] = BlankTableElement();
          }
      }
    
  • 시스템 호출: 커널 Context Switching이 수반되는 시스템 루틴 호출은 비용이 크므로, 호출을 줄이거나 직접 작성한 저수준 서비스로 대체함.
  • 인터프리트 언어: 인터프리트 언어는 기계어 코드 생성 및 실행 전 각 명령을 처리해야 하므로 컴파일 언어 대비 현저한 성능 손실이 발생함.

    언어 언어의 타입 C++에 비례한 실행 시간
    C++ 컴파일 1:1
    비주얼 베이직 컴파일 1:1
    C# 컴파일 1:1
    자바 바이트코드 1.5:1
    PHP 인터프리트 >100:1
    파이썬 인터프리트 >100:1
  • 오류: 방치된 디버깅 코드, 메모리 누수, DB 테이블 색인 누락 등 단순 오류가 주요 성능 병목의 원인이 됨.

공통적인 연산의 상대적인 성능 비용

  • 변수 할당, 루틴 호출, 정수 및 부동 소수점 연산 등 대다수 공통 연산의 비용은 대략 비슷함.
  • 난해한 수학 함수나 다형성 루틴 호출은 일반 연산보다 다소 비싼 비용을 소모함.
  • 코드 튜닝을 통한 속도 향상은 기본적으로 비싼 연산 루틴을 상대적으로 저렴한 루틴으로 대체하여 얻어짐.

25.4 측정

  • 프로그램의 일부 구간이 실행 시간을 많이 차지하므로 코드를 측정해 과열지점을 찾고, 최적화 후에는 재측정하여 향상도를 평가해야 함.
  • 성능의 많은 특성은 직관에 반하며, 장비·언어·컴파일러의 변화로 과거 경험이 효용을 잃으므로 실제 측정 전까지는 최적화 효과를 장담할 수 없음.
      // 1. 행렬 요소를 더하기 위한 직관적인 C++ 예제
      sum = 0;
      for ( row = 0; row < rowCount; row++ ) { 
          for ( column = 0; column < columnCount; column++ ) { 
              sum = sum + matrix[ row ][ column ];
          } 
      }   
    
      // 2. 포인터 연산으로 코드 튜닝을 시도한 C++ 예제
      sum = 0;
      elementPointer = matrix;
      lastElementPointer = matrix[ rowCount - 1 ][ columnCount - 1 ] + 1;
      while ( elementPointer < lastElementPointer ) { 
          sum = sum + *elementPointer++;
      }
    
      //저자의 컴파일러에서는 동일한 어셈블리어가 나왔다.
    

측정은 정확해야 한다

  • 프로파일링 도구를 활용하거나 연산에 소모된 시스템 시간을 기록하는 정밀 루틴을 사용해야 함.
  • 날짜 시각이 아닌 프로그램에 할당된 CPU 클럭 틱 수만 집계하여 타 프로그램 영향과 시스템 구동/측정 오버헤드를 배제해야 함.

25.5 반복

  • 한 가지 기법만으로는 큰 성능 향상을 얻기 어렵지만, 여러 최적화 기법을 결합하고 지속적으로 반복 시도하면 누적되어 비약적인 성능 향상을 달성함.
  • 극단적인 코드 튜닝은 실행 속도를 획기적으로 개선하는 대신, 가독성과 유지보수성을 극도로 떨어뜨릴 수 있으므로 주의가 필요함.
    • 예: DES(데이터 암호화 표준) 알고리즘 구현 시 초기 실행 시간이 21분 40초였으나, 수십 차례의 반복적인 튜닝 과정을 거쳐 목표치인 37초까지 단축시킴 (단, 최종 어셈블리 코드는 유지보수가 매우 어려워짐).

25.6 코드 튜닝 단계 요약

  1. 이해하고 변경하기 쉬운 잘 설계된 코드를 사용하여 소프트웨어를 개발한다.
  2. 성능이 좋지 않다면
    • a. 나중에 “마지막으로 좋았던 상태”로 돌아올 수 있도록 작동하는 버전의 코드를 저장한다.
    • b. 과열지점을 찾기 위해서 시스템을 측정한다.
    • c. 성능이 취약한 것이 부적절한 설계 때문인지, 데이터형이나 알고리즘 때문인지를 판단하고 코드 튜닝이 적절한지 판단한다. 코드 튜닝이 적절하지 않다면 1단계로 돌아간다.
    • d. c 단계에서 규명된 병목을 튜닝한다.
    • e. 한 번에 하나씩 성능을 측정한다.
    • f. 코드의 성능이 향상되지 않았다면 a 단계에서 저장했던 코드로 되돌아간다
  3. 2단계를 반복한다

체크리스트: 코드 튜닝 전략

전체적인 프로그램 성능

  • 프로그램의 요구사항을 변경함으로써 성능을 개선할 것을 고려했는가?
  • 프로그램의 설계를 수정함으로써 성능을 개선할 것을 고려했는가?
  • 클래스의 설계를 수정함으로써 성능을 개선할 것을 고려했는가?
  • 운영체제와의 상호작용을 피함으로써 성능을 개선할 것을 고려했는가?
  • I/O를 피함으로써 성능을 개선할 것을 고려했는가?
  • 인터프리트 언어 대신 컴파일 언어를 사용하여 성능을 개선할 것을 고려했는가?
  • 컴파일러 최적화 사용을 통한 성능 향상을 고려했는가?
  • 다른 하드웨어로 교체함으로써 성능을 개선할 것을 고려했는가?
  • 최후의 수단으로 코드 튜닝을 고려했는가?

코드 튜닝 접근 방법

  • 코드 튜닝을 시작하기 전 프로그램이 완전히 정확한가?
  • 코드 튜닝을 시작하기 전에 성능 병목을 측정했는가?
  • 각 코드 튜닝 변경의 효과를 측정했는가?
  • 의도한 성능 향상이 없을 때 코드 튜닝 변경을 복구시켰는가?
  • 각 병목의 성능을 향상시키기 위해 한 가지 이상의 변경을 반복적으로 시도했는가?

참고 자료

  • 코니 스미스 외, 『Performance Solutions』(Addison-Wesley, 2002): 개발 전 단계에서의 성능 구현, 웹 응용 프로그램 및 확장성 전략을 다룸.
  • 요셉 뉴커머, 「Optimization: Your Worst Enemy」(2000): 비효과적인 최적화 전략의 여러 가지 함정을 상세히 다룸.
  • 도널드 커누스, 『컴퓨터 프로그래밍의 예술 1~3』(한빛미디어, 2006~2008): 기초 알고리즘, 준수치적 알고리즘, 정렬 및 검색을 수식과 가상 언어로 깊이 있게 다룬 고전 참고서임.
  • 로버트 세지윅, 『Algorithms in Java/C++/C』: 정렬, 검색, ADT, 그래프 알고리즘 등 광범위한 문제 해결 방법을 체계적으로 조사 정리함.

요점 정리

  • 성능은 소프트웨어 품질 전체의 한 측면일 뿐이며 일반적으로 가장 중요한 항목도 아니다. 정밀하게 튜닝된 코드는 전체적인 성능의 한 측면일 뿐이며 일반적으로 그 효과가 눈에 보이지도 않는다. 프로그램 아키텍처, 상세 설계, 자료 구조와 알고리즘 선택이 일반적으로 코드의 효율성보다 프로그램의 수행 속도와 크기에 더큰 영향을 미친다.
  • 양적인 측정은 성능 최대화의 핵심이다. 그것은 성능 향상이 정말 중요한 영역을 찾는 데 필요하고 최적화가 소프트웨어의 성능을 향상시켰는지를 검증하는 데도 필요하다.
  • 대부분의 프로그램은 작은 코드 영역에서 대부분의 시간을 보낸다. 코드를 측정하기 전까지는 그것이 어느 코드인지 알 수 없다.
  • 코드 튜닝을 통해 원하는 만큼의 성능을 개선하기 위해서는 일반적으로 여러 번 반복해야 한다.
  • 초기 코드 작성 시 성능 작업을 준비하는 가장 좋은 방법은 이해하고 수정하기 쉬운 분명한 코드를 작성하는 것이다.

results matching ""

    No results matching ""