AVR 마이크로 컨트롤러에서 비트단위 연산을 이용해 1바이트 변수 1개로 8비트를 사용할 경우와, 1바이트 변수 8개로 8비트를 사용할 경우의 동작 속도를 비교해 보고, Microchip Studio (Atmel Studio)의 컴파일 최적화 옵션에 따라 생성되는 코드의 크기와 어셈블리 코드를 비교해 본다.
AVR과 같이 메모리가 넉넉하지만은 않은 환경에서 개발을 진행하다 보면, 메모리 영역 크기의 제한으로 인해 변수의 비트단위까지 짜내는 일이 종종 발생 한다. 물론, 메모리가 넉넉하다면야 필요할 때 마다 독립된 변수를 선언해서 사용하는 것이 코드를 작성하기에도 편하고, 동작시간을 줄이는 것에도 도움이 되지만, 그렇게 마구 사용하다 보면 메모리 제한에 걸려 필요한 변수를 더 이상 선언할 수 없는 상황에 마주치게 되는 것이다.
메모리 걱정 하지 않고 변수를 펑펑 선언해 가며 사용하는 것 (필자는 이를 '펑펑모드'라고 한다. 내 마음이다.) 과, BIT단위로 쪼개서 메모리를 아껴가며 사용하는 것 (필자는 이걸 '쫄쫄모드'라고 한다. 이것 역시 내 마음이다.). 과연 실제 동작에는 어떤 차이가 있을까? 8개의 1bit 데이터를 사용해야 한다 가정하고, 8개의 변수를 선언해서 사용할 때와, 1개의 8bit 변수를 선언한 후, 비트시프트 연산을 해 가며 해당 비트의 값을 읽을 때의 코드를 작성하여, 두 방식의 실행에 걸리는 시간을 테스트 해 본다.
시험 코드 작성
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; } if (h==1) { PORTB=0X00; } ... 2번 반복 ... PORTA=0x00; }
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; }
실험 방법
오실로스코프를 이용해 PORTA가 켜졌다 꺼지는 시간을 측정하면, 각 코드가 실행하는데 걸린 시간을 측정할 수 있다. 한번만 돌리면 시간이 너무 짧아질 수 있으니, 3번씩 돌리고 총 10회 실행해 본 다음 평균을 내어 보기로 한다. 또한, 컴파일러 옵션에 따른 바이너리 코드의 크기차이와 실행시간을 비교하기 위해, 마이크로칩 스튜디오의 컴파일 옵션에서 -O0 옵션과 -O1 옵션을 적용해 동일한 코드를 컴파일한 후, 바이너리 파일을 생성해 AVR에 업로드 하고 각각의 동작 시간을 측정해 본다.
실험 결과
생성된 바이너리 코드의 크기과 실행에 걸린 시간
8변수 (64bit) | 1변수 8bit | |||
최적화옵션 | O1 | O0 | O1 | O0 |
코드크기 | 706B | 1082B | 834B | 1250B |
10회 평균 실행 시간 | 271.8us | 8480us | 6378us | 18245us |
편차 | 0.32 | 0 | 3.2 | 5 |
쫄쫄모드에서 생성된 어셈블리 코드
if (a & (1<<0)) { PORTB=0X00; }
11e: 8c 01 movw r16, r24
120: 01 70 andi r16, 0x01 ; 1
122: 11 27 eor r17, r17
124: 80 fd sbrc r24, 0
126: 15 b8 out 0x05, r1 ; 5
if (a & (1<<1)) { PORTB=0X00; }
펑펑모드에서 생성된 어셈블리 코드
if (a==1) { PORTB=0X00; }
11e: 81 30 cpi r24, 0x01 ; 1
120: c1 f4 brne .+48 ; 0x152 <main+0x3e>
122: 15 b8 out 0x05, r1 ; 5
if (b==1) { PORTB=0X00; }
명령줄 | 총 클럭 | |
쫄쫄모드 (1변수) | 5 | 5CLK |
펑펑모드 (8변수) | 3 | 2.5CLK |
결론
- 컴파일러 최적화 옵션에 따라 다르긴 하지만, 동작시간이 적게는 2배에서 많게는 10배 가까지 차이가 난다. 변수 사용방법은 생각보다 훨씬 크게 시스템 동작속도에 영향을 미친다. 메모리 여유가 있으면, 비트연산 하지 말고 그냥 변수 선언해서 하는게 여러모로 이득이다.
- 컴파일러 최적화 옵션은 사용하는게 여러모로 좋다. 바이너리 코드의 크기가 줄어들어 제한된 메모리 영역을 좀 더 효율적으로 사용할 수 있다. 다만, 최적화 과정에서 예기치 못한 동작이 발생할 수 있음은 항상 기억해 두고 있어야 할 것이다. 이럴 경우 어셈블리를 직접 확인해 보면 원인을 찾아낼 수 있다.
결국, 메모리가 받쳐준다면 막 써대는게 좋다는 결론이다. 문제는 메모리는 하드웨어이고, 하드웨어는 돈이다. 즉, 돈 많으면 다 된다.
작성 : 2020. 11. 30. 13:12 / 수정 : 2024. 12. 06.