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

2025년 11월 26일

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

하지만 AVR 같은 마이크로컨트롤러에서도 그런 감각으로 변수를 마구 선언하다 보면, 어느 순간 메모리 용량의 절대적 한계에 부딪혀 더 이상 변수를 하나도 추가할 수 없는 상황이 찾아온다. (게다가 꽤 자주 찾아온다.)

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

그렇다면, 메모리 걱정 하지 않고 변수를 펑펑 선언해 가며 사용하는 경우와 (필자는 이를 ‘펑펑모드‘라고 한다. 내 마음이다.) 과, BIT단위로 쪼개서 메모리를 아껴가며 사용하는 경우, (필자는 이걸 ‘쫄쫄모드‘라고 한다. 이것 역시 내 마음이다.) 이 둘의 성능 차이는 얼마나 날까? 또 컴파일러를 이용해 최적화를 진행하는 것은 얼마나 큰 차이를 만들어 낼까?

이 궁금증을 해결하기 위해 ATmega2560을 사용하여 두 방식의 실행 속도코드 크기, 그리고 컴파일러 최적화 옵션(-O0, -O1 등)이 어떤 영향을 주는지 직접 측정해 보기로 했다.

실험 방법

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

또한, 컴파일러 옵션에 따른 바이너리 코드의 크기차이와 실행시간을 비교하기 위해, 마이크로칩 스튜디오의 컴파일 옵션에서 -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개를 사용, 총 64비트를 사용하는 구조이다. AVR 입장에서 보면, 각 if 문은 “레지스터에 있는 값을 1과 비교하고, 같으면 분기”라는 아주 단순한 흐름을 따른다.

1개의 변수를 사용하는 쫄쫄모드

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<<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비트를 사용하는 구조이다. 코드의 양은 비슷해 보이지만 각 if 문을 실행하기 전, 8비트 저장소의 한 비트를 꺼내기 위한 연산이 추가되었다.

실험 결과

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

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

펑펑모드 vs 쫄쫄모드

실행 시간 측정에서, O1 기준 펑펑모드는 약 271.8µs, 쫄쫄모드는 약 6378µs약 6ms가까운 차이가 발생했고, 최적화가 없이 컴파일한 O0 옵션에서는 그 격차가 더 벌어져, 펑펑모드가 약 8480µs, 쫄쫄모드는 약 18245µs약 10ms가까운 차이가 발생했다.

메모리를 아끼지 않고 사용하는 펑펑모드가 동작속도 측면에서 훨씬 우월하다는 것을 알 수 있다.

최적화 옵션 비교

최적화 옵션을 적용한 결과, 최적화 옵션을 적용하지 않은 경우에 비해 8개의 변수를 사용했을 때 기준 펑펑모드는 376Byte가 감소했고 실행시간은 약 8200µs감소했다. 1개의 변수를 사용한 쫄쫄모드 역시 비슷한 양상을 보였는데, 쫄쫄모드에서는 416Byte의 용량과 11867µs의 시간을 절약할 수 있었다.

결론적으로 메모리를 아낌없이 사용하고 최적화 옵션을 적용했을 경우, 그렇지 않았을 경우와 비교해 544Byte의 메모리 용량과 17974µs의 실행 시간을 아낄 수 있다.

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

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

어셈블리 코드의 차이

먼저, 쫄쫄모드(비트 단위로 변수 값을 꺼내는 방식)와 펑펑모드(바이트 단위로 독립 변수를 사용하는 방식)가 생성하는 어셈블리 코드를 확인해 보겠다.

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

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; }

비트 추출을 위해 AND 연산과 보조 레지스터 조작이 반복적으로 필요하고, sbrc 명령을 통한 비트 검사 과정에서도 여러 단계를 거친다. 결국 레지스터 연산 자체가 많아지고 분기 판단 과정도 복잡해져 실행 시간이 길어질 수밖에 없다.

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

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)의 단순한 세 단계만으로 조건문이 끝난다. 즉, 명령어 수가 적고 흐름이 단순하기 때문에 실행 속도도 더 빠르게 나타난다.

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

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

여기에 더해, 컴파일러 최적화 옵션은 속도와 코드 크기에 직접적인 영향을 준다. -O0에서는 불필요한 코드까지 모두 유지되기 때문에 가장 크고 가장 느린 바이너리가 생성된다.

반면 -O1부터는 기본적인 최적화가 적용되어 불필요한 코드를 제거하고, 단순한 루프 최적화 등을 수행하면서 성능이 크게 개선된다. 실제 실험에서도 -O0-O1 사이에 속도가 수 배에서 최대 10배 이상 차이가 났다.

결국 쫄쫄모드의 복잡한 명령 흐름은 최적화를 통해서도 한계가 있으며, 반대로 펑펑모드는 구조 자체가 단순하기 때문에 최적화를 적용하지 않아도 기본 성능이 우수한 편이다. 변수 사용 방식과 최적화 옵션의 조합이 동일한 동작을 하는 코드라고 하더라도 그 성능을 결정하는 중요한 차이를 만드는 것이다.

컴파일러 최적화 옵션

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

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

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

최적화가 버그를 만든다

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

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

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

결론

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

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

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

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

🔄 갱신 내역

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