주어진 환경에서 코드를 최적화시켜 생산 비용을 낮출 수 있는 방법을 제시한다.
거의 모든 임베디드 장비에 들어가는 것이 바로 RAM 과 ROM 이다. 그 만큼 중요한 데, 점점 하드웨어의 가격이 낮아지고 있기 때문에 예전만큼 최적화에 대한 압박은 줄었다.
하지만, 최적화에 대한 관심만큼은 점점 높아지고 있다. 여기서는 크게 RAM 최적화와 ROM 최적화에 대해 알아본다.
그전에 C 프로그램이 어떻게 RAM 또는 ROM 에서 동작하는지 알아보도록 하겠다.
들어가기 전에
프로그램을 ROM 에서 실행하면 RAM 에서 실행할 때 보다 속도가 떨어진다. RAM 이 넉넉하고 실행속도가 중요하다면 RAM 에서 실행하는 것이 더 좋다. 프로그램을 RAM 에서 실행할 때만 텍스트 섹션을 RAM 에 로드한다.
메모리의 특성상 프로그램 런타임에 변하지 않는 코드들이 ROM 에 저장되고, 런타임에 값이 변하는 것들은 RAM 에 저장된다.
ROM 은 텍스트와 데이터 세그먼트로 구성된다. 텍스트 세그먼트에는 프로그램 코드와 상수가 저장된다.
데이터 세그먼트에는 초기화된 전역변수와 정적변수가 저장된다.
- ROM : 프로그램 코드와 상수, 초기화된 전역 변수와 정적 변수들
- RAM : ROM 의 초기화된 전역 변수와 정적 변수들의 복사본, 초기화 되지 않은 전역 변수와 정적 변수, 지역 변수, 함수의 인자 및 함수 호출시 발생하는 문맥
이상한 점은 위의 그림을 보면 데이터 세그먼트는 ROM 에 있는데, 전역 변수와 정적 변수는 말 그대로 값이 변할 수 있는 변수이다.
그런데 이값을 ROM 에 저장한다면 런타임에 변한 값이 적용되지 않고, 계속해서 초기값만을 갖고 있게 될 것이다. 그래서 데이터 세그먼트를 RAM 에 복사해서 런타임 시 값의 변동을 저장할 수 있게 한다.
만약 속도를 위해 프로그램을 RAM 에 로딩해서 실행한다면 이 과정을 생략한다.
데이터 세그먼트에는 ROM 의 데이터 세그먼트에서 복사된 초기화된 전역 변수와 정적 변수가 저장되어 있다.
bss 세그먼트는 스타트업(startup) 코드에 의해서 0 으로 자동 초기화된다. 전역 변수와 정적 변수를 프로그램 내에서 초기화하지 않아도 0 으로 자동 초기화되는 이유가 이 때문이다.
변수들의 저장 위치를 정리해보면 다음과 같다.
- 초기화 되는 전역 변수와 정적 변수 → 데이터 세그먼트
- 초기화되지 않은 전역 변수와 정적 변수 → bss 세그먼트
- 지역 변수와 함수 인자 → 스택
- 레지스터 변수 → 레지스터
- 문자열과 상수 → 텍스트 세그먼트
다음은 각 변수의 종류별 특징을 표로 나타냈다.
지역변수 | 전역변수 | 정적변수 | 레지스터 변수 | |
지정자 | auto(생략가능) | extern(생략가능) | static | register |
메모리 | 스택 | 데이터 세그먼트 | 데이터 세그먼트 | 레지스터 |
선언 | 함수 내부 | 어디든 가능 | 지역(함수 내부), 전역(함수 외부) | 함수 내부 |
초기화 | 초기화 문장을 통해서만 실행, 초기화 안하면 쓰레기값 | 초기화 문장 없이도 0 으로 자동 초기화 | 초기화 문장 없이도 0 으로 자동 초기화 | 초기화 문장을 통해서만 실행, 초기화 안 하면 쓰레기값 |
유효범위 | 선언한 함수나 블록 내부 | 함수 외부, 파일 외부 | 함수 내부, 함수 외부, 파일 내부 | 선언한 함수나 블록 내부 |
유효시간 | 선언한 함수가 실행될 때부터 종료될 때까지 | 프로그램 실행될 때부터 종료될 때까지 | 프로그램 실행될 때부터 종료될 때까지 | 선언된 함수가 실행될 때부터 종료될 때까지 |
ROM 최적화
ROM 영역에 저장된 요소들(코드 + 상수 + 초기화된 전역변수)은 모두 하나의 실행 파일이다.
임베디드 환경에서는 HDD 가 없기 때문에 실행 파일을 ROM 에 저장하고 런타임에 사용되는 나머지 섹션들은 RAM 에 저장해서 프로그램을 실행한다. 때로는 프로그램의 속도를 높이려고 실행 파일을 RAM 에 로딩하여 실행시킬 수도 있다.
ROM 을 최적화 하려면 결국 실행파일을 줄여야 한다. 다음은 최적화를 위한 여러가지 방법들이다.
데드 코드 제거
사용되지 않는 계산이나, 도달하지 않는 코드를 제거한다. 컴파일러의 최적화 옵션을 사용해서 할 수 있지만, 자칫 만드시 필요한 코드를 컴파일러 마음대로 제거하는 것을 방지하기 위해 'volatile' 를 사용하여 최적화를 방지할 수 있다.
매크로나 인라인을 사용하지 않는다
속도 최적화의 경우에는 필요하지만, 크기 최적화 시에는 코드의 크기가 증가하므로 피한다.
전역변수를 초기화하지 말자
전역 변수나 정적 변수를 초기화하여 사용하면 ROM 과 RAM, 두 메모리에 모두 저장된다. 그래서 ROM 을 절약하려면 전역 변수나 정적 변수의 초기화를 생략하는 것이 좋다. 초기화를 안하면 bss 세그먼트에 저장된다. bss 세그먼트는 RAM 에 위치한다.
상수나 전역 변수 대신 지역 변수 사용
전역 변수를 지역 변수로 대체하는 것도 ROM 을 절약하는 한 방법이다. 지역 변수는 스택에 저장되기 때문에 ROM 사용을 줄일 수 있다. 마찬가지로 상수 역시 코드 크기를 늘리므로 변수로 대체한다.
표현은 간결하게, 불필요한 중간과정 생략
표준 라이브러리 함수 자제
printf() 와 같은 표준 라이브러리 함수들은 많은 조건들에 대비한 코드들이 많기 때문에 코드 크기가 증가하는 경향이 있다.
특히 임베디드 소프트웨어는 타겟 프로세서에 연결된 주변기기가 각 시스템마다 다르기 때문에 표준 라이브러리 함수보다는 직접 만들어 사용하는 경우가 많다.
RAM 최적화
함수 호출은 깊지 않게
함수 호출이 많아 질 수록 메모리의 스택 사용량이 늘어나기 때문이다.
함수는 매크로나 인라인으로 대체
함수 대신 매크로나 인라인을 사용해서 RAM 을 절약할 수 있다.
구조체나 배열 대신 포인터 활용
구조체는 대부분 크기가 큰 편이다. 함수의 인자나 리턴 값이 구조체일 때, 값에 의한 호출을 한다면, 메모리나 속도의 손실이 크다. 그러므로 참조에 의한 호출을 사용하는 것이 좋다.
비트 플래그 활용
현재 프로그램 상태를 나타내는 데 플래그를 많이 사용한다. 예를 들어 인터럽트 발생 여부를 나타내거나, 응용프로그램에서 데이터를 검색했을 때, 검색 여부를 나타내는 플래그가 필요하다. 이러한 플래그는 참, 거짓만을 나타내기 때문에 1 비트로 충분히 표현 가능하다. 하지만 C 언어에서는 boolean 형 데이터 타입이 없으므로 보통 int 형 변수를 사용한다.
이러한 방법은 프로그램에서 많이 사용되지만, 메모리 낭비가 크다. 0 과 1을 표현하기 위해서 1 비트로 충분한 데, 32 비트를 사용하기 때문이다.
프로그램은 롬에서 실행
위의 그림을 보면, ROM 에서 데이터 세그먼트만 복사되는 것처럼 나와있는데, 텍스트 세그먼트의 경우에 RAM 에 텍스트 세그먼트를 할당할 수도 있고 안할 수도 있다.
텍스트 세그먼트가 항상 RAM 에 복사될 필요는 없다. 하지만 메모리의 특성상 ROM 보다 RAM 의 속도가 빠르므로 프로그램의 성능을 높이려고 코드를 RAM 으로 로드할 수 있다. 하지만, 이 경우에는 그만큼 RAM 을 더 사용하게 되므로 RAM 을 아껴야 하는 경우에는 속도가 좀 느리더라도 RAM 으로 로드하지 않는 것이 좋다.