메모리 사용량과 실행 속도 향상을 위한 코드 최적화 기법
상태바
메모리 사용량과 실행 속도 향상을 위한 코드 최적화 기법
  • 이현도 IAR Systems 과장
  • 승인 2019.07.19 09:00
  • 댓글 0
이 기사를 공유합니다

소프트웨어 개발에서 코드 최적화는 중요한 과정이다. 동일한 기능의 소스 코드라도 코드의 수행 속도나 사이즈 변화 등에 목적을 두고 다양한 기법으로 이를 최적화할 수 있다. 최적화된 소스 코드는 같은 성능의 하드웨어에서도 수행 속도를 최대한으로 높일 수 있으며, 코드의 메모리 사용량도 최소화할 수 있다. 특히 메모리 사용량의 감소는 같은 기능을 구현해도 더 적은 메모리를 사용한다는 말과 같아, 이로 인한 비용 절감 효과도 기대할 수 있다. 아울러 실행 속도가 빠른 코드는 애플리케이션의 응답성을 높여주므로 다양한 기능 구현에 도움이 된다. 

 

코드 최적화 범위

코드 최적화 옵션에 대한 범위는 전체 애플리케이션 혹은 각각의 C 파일 단위로 지정할 수 있다. 경우에 따라 개별 함수에 적용되는 최적화를 배제할 수도 있다. 또 프로젝트 옵션 대신 #pragma optimize 지시어를 활용하면 최적화 단계와 형식을 함수 단위로 지정하는 것도 가능하다.

 

코드 최적화 단계

최적화는 None, Low, Medium, High 4단계로 이뤄져 있다. 이 중 High 단계의 경우 다시 Balanced, Size, Speed에 관한 설정 등 세부적인 변환 방식에 따른 옵션을 지원한다.

[표1]

최적화를 위한 코드 변형

컴파일러의 최적화 기능을 구현하는 과정에서, 코드를 여러 방식으로 변형해 사이즈가 더 작고 실행 속도가 빠른 코드가 만들어지게 된다. 또 컴파일러는 소스 코드의 로직을 분석해 동일한 결과를 구현할 수 있는 보다 효과적인 실행 코드를 생성한다. 다만 이런 경우 코드는 개발자가 작성한 프로그램 로직과 다른 방식으로 실행되므로 디버깅 시 문제가 발생할 수 있다는 점에 주의해야 한다. 

 

코드 최적화를 위한 다양한 변환 기법

1. Common subexpression elimination: 공통된 코드를 중복 실행하지 않고 한 번 실행한 값을 활용해 최적화하는 방법이다. [표1]에 기술된 것처럼 b * C의 공통 코드를 한 번만 실행해 처리한다.

[그림1]

2. Loop Unrolling: 루프(Loop) 코드는 대체로 실행에 많은 시간이 소요된다. 특히 다중루프의 경우 더 많은 시간이 소요될 수밖에 없다. 따라서 지정한 범위에 도달했는지 검사하기 위해 값을 비교하고 증가시키는 등의 작업이 진행되는데, 코드에서 이런 작업 과정을 줄일 수 있다면 실행 속도를 보다 빠르게 할 수 있다.

[그림2]
[그림2]

[그림2]의 예제는 루프의 For문에 대한 코드를 삭제하고 만든 코드다. 다만 반복 횟수가 많은 경우에는 이런 방식으로 구현할 수 없으며, 반복되는 루프의 횟수를 줄일 수 있다면 그만큼 실행 속도도 빨라진다.

3. Function inlining: 함수를 호출하면 복귀할 위치를 스택에 저장하고 해당 함수로 점프한다. 만약 해당 함수에서 일반적으로 사용하는 레지스터를 스택에 보관(Push)하고 함수의 실행을 마치면, 레지스터의 값을 복원(Pop)하고 보관해 둔 주소로 점프하는 과정을 거친다. 이런 시간을 줄이기 위해서는 작은 코드인 함수를 직접 삽입해 사용하는 방법이 있다. [그림3]의 예제처럼 함수를 호출하지 않고 코드를 삽입한 상태로 변형해보자.

[그림3]
[그림3]

4. Code motion: 코드를 이동해 보다 효율적인 실행 코드를 생성한다. [그림4] 예제의 경우 x * x의 부분은 루프를 돌면서 지속적인 시간 소요가 발생되지만, 값의 변화는 없다. Code motion은 이처럼 해당 코드를 이동해 최적화된 코드를 생성하는 방법을 말한다. 

[그림4]
[그림4]

5. Type-based alias analysis: 데이터 타입이 다르면 별도의 선언이 없는 경우, 서로 다른 위치에 데이터가 존재하게 된다. 예를 들어 char a;와 int b;의 경우, 변수 a와 b는 서로 다른 위치에 있게 된다는 말이다. [그림5]의 경우는 short *p1과 long *p2의 변수는 서로 독립적으로 인식된다. *p2값이 0이고, 변함이 없으므로 return *p2의 실행 코드를 return 0으로 변경한다. 이때 컴파일러가 인식하는 상황과 다르게 임의로 포인터를 설정해 처리하면 동작 오류를 발생시킬 수 있으니 주의해야 한다.

[그림5]
[그림5]

6. Static clustering: static 또는 global 변수가 사용하는 함수에서보다 효율적인 처리를 위해서는 해당 변수들의 위치를 변경한다[그림6].

[그림6]
[그림6]

7. Instruction scheduling: 여러 개의 파이프라인을 사용하는 경우, 앞의 파이프라인에서 결과가 나오지 않으면 뒤의 파이프라인이 기다리는 상태가 된다. 이를 Stall이라고 말하며, Instruction scheduling은 이런 Stall 현상을 최소화해 명령어를 재배열하는 방법을 말한다. 참고로 Cortex-M3 코어의 경우 총 3개의 파이프라인으로 구성돼 있다.

[그림7]
[그림7]

맺음말

코드 최적화는 메모리 사용량의 절감과 함께 최상의 성능을 구현하기 위한 과정이다. 하지만 코드가 컴파일러 분석에 의해 자동으로 분석되고 변형되므로, 개발된 코드의 동작도 의도와 다르게 변형될 수 있다는 점을 기억해야 한다. 따라서 소스 코드의 동작 테스트는 반드시 병행되는 것이 좋다. 이처럼 코드 변형에 대한 기술 이해와 명확한 코드 작성, 그리고 최적화 기능을 활용해 최상의 퍼포먼스와 최적의 사이즈를 갖는 실행 코드를 만들기 바란다.

 

글 | IAR Systems 이현도 과장
홈페이지 | https://www.iar.com/kr