AVR 컴파일러 최적화와 변수 사용방법이 동작속도에 주는 영향

AVR 컴파일러 최적화와 변수 사용방법이 동작속도에 주는 영향을 묘사한 이미지, 메모리 여유가 작은 ATMEGA2560은 빠르게, 메모리 여유가 많은 ATMEGA2560은 느리게 움직이는 것을 묘사했다.

ATmega2560을 기준으로 AVR-GCC 컴파일러의 최적화 옵션과 변수 사용 방식이 동작 속도 및 코드 크기에 미치는 영향을 분석한다. 오실로스코프를 이용한 속도 측정과 어셈블리어 레벨의 코드 분석을 통해, 메모리를 아끼지 않는 변수 배치와 적절한 최적화 옵션의 조합이 가져오는 임베디드 시스템의 성능 향상을 데이터로 확인한다.

임베디드 개발의 기본: 메모리를 아껴라

프로그램을 작성하다 보면 필연적으로 변수를 사용할 일이 생긴다. PC처럼 기본 메모리가 넉넉하고, 메모리가 부족하면 운영체제가 알아서 가상 메모리까지 챙겨주는 환경에서는 변수를 선언하는 데 그다지 신경을 쓸 일이 없다.

하지만 AVR 같은 마이크로컨트롤러에서도 그런 식으로 변수를 마구 선언하다 보면, 메모리 용량의 절대적 한계에 부딪혀 더 이상 변수를 하나도 추가할 수 없는 상황을 마주하게 된다.

때문에 임베디드 환경에서는 어떻게든 사용할 수 있는 메모리의 양을 확보하기 위해 메모리를 쪼개서 할당하는 경우가 많다. 또한, 컴파일된 바이너리도 메모리를 차지하기 때문에 컴파일러도 최적화를 위한 여러 옵션들을 제공한다.

그렇다면 메모리 걱정 없이 변수를 넉넉하게 선언해 사용하는 방식과, 비트 단위로 쪼개 메모리를 아껴 쓰는 방식 사이에는 실제로 어느 정도의 성능 차이가 있을까? (앞의 방식을 펑펑모드, 뒤의 방식을 쫄쫄모드라 하겠다. 특별한 이유는 없다. 그냥 본 필자 맘이다.)

또, 컴파일러를 이용해 최적화를 진행하는 것은 얼마나 큰 차이를 만들어 낼까?

이 궁금증을 해결하기 위해 ATmega2560을 사용해 두 방식의 실행 속도코드 크기를 비교해 보고, 컴파일러 최적화 옵션(-O0, -O1 등)의 변화에 따라 어떤 차이가 발생하는지 확인해 보기로 했다.

시험 방법

  • 마이크로컨트롤러: ATmega2560
  • 개발 환경: Microchip Studio (구 Atmel Studio)
  • 컴파일러: AVR-GCC
  • 측정 방법:
    • 코드 시작 부분에서 PORTA0xFF로 켠다.
    • 조건문 블록을 모두 실행한 뒤 마지막에 PORTA를 다시 0x00으로 내린다.
    • 오실로스코프로 PORTA의 펄스 폭을 측정하면, 그 구간에 코드가 실행되는 데 걸린 시간을 알 수 있다.
  • 반복 수행:
    • 각 조건에서 동일한 코드를 3번씩 루프 실행한다.
    • 이 과정을 총 10회 반복한 뒤, 측정값의 평균을 구한다.

추가로, 컴파일러 옵션에 따른 바이너리 코드의 크기 차이와 실행 시간을 비교하기 위해, 마이크로칩 스튜디오(Microchip Studio)의 컴파일 옵션에서 -O0 옵션과 -O1 옵션을 적용해 동일한 코드를 컴파일한 후, 바이너리 파일을 생성해 AVR에 업로드 하고 각각의 동작 시간을 측정해 본다.

시험 코드 작성

8비트를 저장하기 위해 8개의 변수를 사용하는 펑펑모드

int main(void)
{
  DDRA=DDRB=0xFF;
	unsigned char a=0, b=0, c=0, d=0, e=0, f=0, g=0, h=0;
	a=b=c=d=e=f=g=h=rand();
	PORTA=0XFF;
	if (a==1)	{	PORTB=0x00;	}
	if (b==1)	{	PORTB=0x00;	}
	if (c==1)	{	PORTB=0x00;	}
	if (d==1)	{	PORTB=0x00;	}
	if (e==1)	{	PORTB=0x00;	}
	if (f==1)	{	PORTB=0x00;	}
	if (g==1)	{	PORTB=0x00;	}
	if (h==1)	{	PORTB=0x00;	}
	... 2번 반복 ...
	PORTA=0x00;
}

이 방식은 8비트를 저장하기 위해 8비트 변수 8개를 사용한다. 즉, 상태값 8개를 저장하는 데 총 64비트를 사용하는 구조이다. AVR 입장에서 보면, 각 if 문은 변수에 저장된 값을 1과 비교하고, 조건이 맞으면 분기하는 비교적 단순한 흐름을 따른다.

1개의 변수에 8비트를 저장하는 쫄쫄모드

int main(void)
{
  DDRA=DDRB=0xFF;
	unsigned char a=0;
	a=rand();
	PORTA=0XFF;
	if (a & (1<<0))	{	PORTB=0x00;	}
	if (a & (1<<1))	{	PORTB=0x00;	}
	if (a & (1<<2))	{	PORTB=0x00;	}
	if (a & (1<<3))	{	PORTB=0x00;	}
	if (a & (1<<4))	{	PORTB=0x00;	}
	if (a & (1<<5))	{	PORTB=0x00;	}
	if (a & (1<<6))	{	PORTB=0x00;	}
	if (a & (1<<7))	{	PORTB=0x00;	}
	... 2번 반복 ...
	PORTA=0x00;
}

이 방식은 8비트를 저장하기 위해 8비트 변수 1개만 사용한다. 즉, 상태값 8개를 하나의 변수 안에 비트 단위로 나누어 저장하는 구조이다. 메모리 사용량만 보면 64비트에서 8비트로 줄어들기 때문에 훨씬 효율적이다.

다만 코드의 양은 비슷해 보여도, 각 if 문을 실행하기 전에 특정 비트가 켜져 있는지 확인하기 위한 비트 연산이 추가된다. 따라서 메모리는 아낄 수 있지만, 실제 실행 속도에서는 이 추가 연산이 영향을 주게 된다.

시험 결과

생성된 바이너리 코드의 크기와 실행에 걸린 시간을 측정한 결과는 다음과 같다.

최적화 및 변수 사용 방법에 따른 코드 크기와 실행 시간
  8변수 (64bit) 1변수 (8bit)
최적화 옵션 -O1 -O0 -O1 -O0
코드 크기 706B 1082B 834B 1250B
10회 평균 실행 시간 271.8µs 8480µs 6378µs 18245µs
편차 0.32 0 3.2 5

펑펑모드 vs 쫄쫄모드

실행 시간 측정 결과, -O1 기준으로 펑펑모드는 약 271.8µs, 쫄쫄모드는 약 6378µs가 걸렸다. 두 방식 사이에는 약 6106.2µs, 즉 약 6.1ms의 차이가 발생했다.

최적화 없이 컴파일한 -O0 옵션에서는 격차가 더 커졌다. 펑펑모드는 약 8480µs, 쫄쫄모드는 약 18245µs로 측정되었으며, 두 방식 사이에는 약 9765µs, 즉 약 9.8ms의 차이가 발생했다.

즉, 8개의 변수를 따로 사용하는 펑펑모드가 실행 속도가 빠르다는 것을 알 수 있다. 반대로 쫄쫄모드는 메모리 사용량은 줄일 수 있지만, 실행에 더 많은 시간이 필요하다.

최적화 옵션 비교

펑펑모드에서는 -O0 대비 -O1 적용 시 코드 크기가 1082B에서 706B로 376B가 줄었고, 실행 시간은 8480µs에서 271.8µs로 줄어 약 8208.2µs가 짧았다.

쫄쫄모드에서도 비슷한 경향이 나타났다. 코드 크기는 1250B에서 834B로 416B가 줄었고, 실행 시간은 18245µs에서 6378µs로 줄어 11867µs가 짧았다.

두 조건을 비교하면 코드 크기는 544B, 실행 시간은 약 17973.2µs, 즉 약 18ms 차이가 발생했다. 메모리를 아끼기 위한 비트 단위 저장 방식보다, 변수를 넉넉하게 사용하고 컴파일러 최적화 옵션을 적용하는 방식이 실행 속도와 코드 크기 모두에서 더 유리한 결과를 보였다.

결론적으로 가장 빠르고 코드 크기도 작았던 조건은 -O1 최적화를 적용한 펑펑모드였다. 반대로 가장 느리고 코드 크기도 컸던 조건은 최적화를 적용하지 않은(-O0) 쫄쫄모드였다.

어셈블리 코드를 통해 보는 차이의 이유

변수 사용 방식(펑펑모드 vs 쫄쫄모드)에 따라 실행 속도가 크게 달라지는 가장 큰 원인은 컴파일된 어셈블리 코드의 구조가 완전히 다르기 때문이다. 여기에 컴파일러 최적화 옵션(-O0, -O1 등)까지 더해지면, 동일한 코드라도 명령어 수, 레지스터 사용량, 분기 방식이 크게 변하며 그 결과 실제 성능이 눈에 띄게 달라진다.

어셈블리 코드의 차이

먼저, 쫄쫄모드와 펑펑모드가 생성하는 어셈블리 코드를 비교해 보겠다.

쫄쫄모드(비트 검사 방식)의 어셈블리 코드

if (a & (1<<0)) { PORTB = 0x00; }
11e: 8c 01        movw  r16, r24	; 1CLK
120: 01 70        andi  r16, 0x01	; 1CLK
122: 11 27        eor   r17, r17	; 1CLK
124: 80 fd        sbrc  r24, 0		; 1~3CLK
126: 15 b8        out   0x05, r1	; 1CLK
if (a & (1<<1)) { PORTB = 0x00; }

쫄쫄모드에서는 하나의 변수 안에 여러 상태값이 비트 단위로 들어 있기 때문에, 조건을 판단할 때 특정 비트가 켜져 있는지를 확인해야 한다. 이 과정에서 andi와 같은 비트 마스크 연산, movweor 같은 레지스터 조작, 그리고 sbrc를 이용한 비트 검사 명령이 함께 나타난다.

즉, 단순히 변수 하나의 값이 1인지 비교하는 것이 아니라, 변수 안의 특정 비트를 확인하기 위한 추가 명령어들이 필요하다. 이 때문에 하나의 조건문을 처리하는 데 필요한 명령어 수가 늘어나고, 조건에 따른 실행 클럭도 많이 필요하게 된다.

펑펑모드(독립 변수 비교 방식)의 어셈블리 코드

if (a == 1) { PORTB = 0x00; }
11e: 81 30        cpi   r24, 0x01	; 1CLK
120: c1 f4        brne  .+48		; 1~2CLK
122: 15 b8        out   0x05, r1	; 1CLK
if (b == 1) { PORTB = 0x00; }

즉시값 비교(cpi) → 조건 분기(brne) → 결과 적용(out)의 단순한 3단계만으로 조건문이 끝난다. 즉, 명령어 수가 적고 흐름이 단순하기 때문에 실행 속도도 더 빠르게 나타난다.

어셈블리 명령어 수와 실행 클럭 비교
방식 명령어 수 실행 클럭
쫄쫄모드 (1변수) 5개 최대 약 7 CLK
펑펑모드 (8변수) 3개 최대 약 4 CLK

컴파일러 최적화 옵션의 영향

여기에 더해 컴파일러 최적화 옵션은 속도와 코드 크기에 직접적인 영향을 준다. 최적화를 적용하지 않는 -O0 옵션에서는 C 코드의 형태를 그대로 직역하며 불필요한 레지스터 접근이나 메모리 백업 과정을 모두 유지하기 때문에, 가장 크고 가장 느린 바이너리가 생성된다.

반면 -O1부터는 기본적인 최적화 규칙이 적용된다. 불필요한 코드를 제거하고, 상수를 미리 계산하거나 단순한 루프 구조를 최적화하면서 전체적인 성능이 크게 개선된다. 실제 시험 에서도 -O0-O1 사이에 속도가 수 배에서 수십 배 이상 차이가 났다.

결국 쫄쫄모드의 복잡한 명령 흐름은 컴파일러 최적화를 거치더라도 근본적인 명령어 수를 줄이는 데 한계가 있다. 반면 펑펑모드는 단순한 구조를 지니고 있어, 최적화를 적용하지 않아도 기본 성능이 우수한 편이다. 비록 겉보기에는 동일한 동작을 하는 C 코드일지라도, 어떤 변수 사용 방식을 선택하고 어떤 최적화 옵션을 조합하느냐가 임베디드 시스템의 성능을 결정짓는 핵심 요인이 되는 것이다.

컴파일러 최적화 옵션

이번 시험에서는 -O0-O1 두 가지 옵션만 확인했지만, AVR-GCC가 제공하는 최적화 옵션은 좀 더 다양하다. 각각의 최적화 옵션에 대해 살펴보도록 하겠다.

-O0
[최적화 없음] 아무런 최적화를 하지 않는 옵션이다. 코드 크기가 가장 크고 실행 속도도 가장 느리다. 디버깅에는 유리하지만 실제 동작용(배포용)으로는 거의 사용되지 않는다.
-O1
[기본 최적화] 사용되지 않는 코드(Dead Code) 제거, 단순 루프 최적화 등 기본적인 최적화가 활성화된다. -O0 대비 속도와 코드 크기가 크게 개선되며, 일반적인 AVR 펌웨어 개발에서 가장 많이 사용하는 옵션이기도 하다.
-O2 / -O3
[고수준 최적화] 공통 부분식 제거, 루프 전개(Loop Unrolling) 등 보다 공격적인 최적화를 수행한다. 다만 AVR처럼 구조가 단순한 8비트 RISC MCU에서는 -O1 대비 체감 성능 향상이 크지 않을 때가 많다. 경우에 따라 코드 크기만 늘어날 수도 있어 프로젝트 성격에 따라 주의가 필요하다.
-Os
[크기 최적화] 코드 크기를 최소화하는 데 목적이 있으며, 플래시 메모리 용량이 빠듯한 칩에서 자주 활용된다. 실행 속도는 경우에 따라 -O2보다 느릴 수도 있지만, 크기와 속도 사이에서 가장 적절한 균형을 찾는 데 유용한 옵션이다.

정리하면, 최적화 옵션은 단순히 속도를 빠르게 만드는 기능이 아니라 코드 구조 자체를 바꾸는 옵션이며, 어떤 옵션을 선택하느냐에 따라 실행 속도는 수 배에서 10배 이상 차이 날 수 있다.

최적화가 버그를 만든다

컴파일러 최적화는 분명 속도와 코드 크기를 개선하는 데 큰 도움이 되지만, 상황에 따라서는 작성한 코드가 최적화 과정에서 사라져 버리는 경우도 발생한다. 컴파일러가 프로그램의 결과에 영향을 주지 않는다고 판단하면, 해당 코드를 자동으로 제거하기 때문이다.

예를 들어, 반복문 내부에서 계산한 값이 실제로 어디에도 사용되지 않으면 반복문 전체가 삭제될 수 있다. 또한 조건문의 입력값이 컴파일 시점에 결정될 수 있다면, 해당 조건문은 항상 참 또는 항상 거짓으로 간주되어 분기 코드가 통째로 사라지기도 한다.

이런 최적화는 대부분의 경우 유리하지만, PORTAPORTB처럼 하드웨어 레지스터를 직접 다루거나, 정밀한 지연 시간(Delay)을 구현해야 하는 코드에서는 예상치 못한 동작을 만들 수 있다. 따라서 최적화 옵션을 사용할 때는 의도한 동작이 유지되는지 한 번쯤 어셈블리 코드를 확인해 보는 것이 안전하다.

결론

이번 시험을 통해 변수 사용 방식과 컴파일러 최적화 옵션이 AVR 코드의 성능에 얼마나 큰 영향을 미치는지 확인할 수 있었다. 같은 기능을 수행하는 코드라도 구조가 단순한 펑펑모드는 빠르게, 레지스터 조작이 복잡해지는 쫄쫄모드는 상대적으로 느리게 동작한다.

또한 -O0-O1 사이에서처럼, 최적화 옵션 하나만으로도 실행 속도가 수 배에서 10배 이상 차이 날 수 있다. 특별한 이유가 없다면 최소한 -O1 정도의 최적화 옵션은 사용하는 것이 좋다.

선택의 기준은 단순하다. 메모리를 아끼려면 쫄쫄모드, 속도가 중요하고 메모리가 허락한다면 펑펑모드. 프로젝트의 특성과 목적에 따라 두 방식 중 가장 자연스러운 방향을 선택하면 된다.

결국, 메모리가 받쳐준다면 막 써대는 게 좋다는 결론이다. 문제는 메모리는 하드웨어이고, 하드웨어는 돈이다. 그렇다, 결론은, 돈 많으면 다 된다.

FAQ

AVR-GCC는 무엇인가?
AVR-GCC는 C 언어를 이용해 작성한 코드를 AVR용 실행 바이너리 코드로 컴파일 해 주는 컴파일러로, 공개 컴파일러인 GCC를 기반으로 만들어진 AVR 전용 컴파일러이다.
AVR-GCC 이외에 다른 컴파일러도 있는가?
AVR에서 사용할 수 있는 다른 컴파일러도 존재한다. 하지만 별도의 비용이 발생하거나 특정한 목적으로만 사용할 수 있는 등 제약이 있는 경우가 많다. 컴파일러마다 부가 기능이나 특화된 분야는 다를 수 있지만, 일반적인 AVR 개발에서는 AVR-GCC만으로도 대부분의 작업을 충분히 수행할 수 있다.
AVR 개발시 메모리 부족은 자주 발생하는 문제인가?
소규모 코드에서는 흔한 문제는 아니다. 하지만 코드 규모가 커지면 AVR처럼 메모리가 제한된 마이크로컨트롤러에서는 생각보다 자주 마주하게 된다. 나중에 메모리 부족이 발생하면 이미 작성한 코드의 변수 사용 방식이나 데이터 구조를 다시 바꿔야 할 수 있으므로, 처음 설계할 때 대략적인 메모리 필요량을 계산해 두는 것이 좋다. 본 필자의 개인적인 의견으로는, 임베디드 환경에서는 일단 메모리를 아끼는 방향으로 접근하는 편이 안전하다.
-O1, -Os 같은 옵션은 모든 컴파일러에서 동일하게 사용되는가?
아니다. 최적화 옵션의 이름과 동작 방식은 컴파일러마다 다를 수 있다. 이 글에서 설명한 -O1, -Os 같은 옵션은 AVR-GCC에서 사용하는 최적화 옵션이다. 다른 컴파일러를 사용할 경우에는 해당 컴파일러의 설명서를 참고해야 한다.
컴파일러 최적화로 인한 문제를 막기 위한 방법은 무엇인가?
컴파일러 최적화로 인해 필요한 변수 접근이나 반복문이 제거되는 것을 막기 위해서는 volatile 키워드를 사용할 수 있다. volatile로 선언된 변수는 하드웨어 레지스터, 인터럽트, 외부 요인 등에 의해 값이 바뀔 수 있다고 컴파일러에 알려 주기 때문에, 컴파일러가 해당 변수 접근을 임의로 생략하거나 최적화하지 않도록 만드는 데 도움이 된다.

갱신 내역

  • 최초 작성
  • 블로그 이전
  • 블로그 이전 및 수정
  • 수정

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다