| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- 다이나믹프로그래밍
- c++
- BFS
- 구현
- 컴퓨터비전
- lv2
- Stack
- cpp
- 백준
- 프로그래머스
- 2018 KAKAO BLIND RECRUITMENT
- level2
- 다이나믹 프로그래밍
- JavaScript
- C
- 동적계획법
- 최적화
- 그리디
- java
- 코틀린
- 임베디드
- 누적합
- 이분탐색
- level3
- 자바
- 컴퓨터 비전
- 우선순위큐
- dp
- kotlin
- dfs
- Today
- Total
코드를 느껴바라
[최적화] 실행속도 관점에서 보는 C 코드 최적화 본문
앞선 글에서는 ROM과 RAM 관점에서 코드 최적화를 정리했다.
이번에는 완전히 다른 축으로 넘어가 보자.
임베디드에서 “빠르다”는 건 단순히 체감 성능이 아니라,
- 제어 주기(예: 1ms 루프)를 지킬 수 있는가
- 인터럽트 지연이 허용 범위인가
- 데드라인을 넘기지 않는가
같은 시간 제약과 직결된다.
즉 실행속도 최적화는 “더 빠르게”가 아니라
“정해진 시간 안에 끝나게”를 목표로 하는 경우가 많다.
임베디드에서 실행시간이 왜 중요한가
PC는 CPU도 빠르고 OS가 완충 역할을 해준다.
하지만 MCU는 자원이 제한되고, 작업이 대부분 실시간 이벤트에 의해 결정된다.
- ADC 샘플링 주기
- PWM 업데이트 타이밍
- CAN/LIN 통신 타이밍
- 센서 필터링 주기
- 모터 제어 루프 주기
이런 코드에서 “조금 느린 코드”는 곧 “제어 실패”로 이어질 수 있다.
실행속도 최적화의 큰 방향
속도를 올리는 방법은 크게 두 갈래다.
- CPU가 하는 일을 줄인다 (연산/분기/메모리 접근 감소)
- CPU가 일을 더 효율적으로 하게 만든다 (캐시/정렬/레지스터 활용 유도)
중요한 건, 이 과정이 ROM/RAM 최적화와 충돌할 수 있다는 점이다.
빠르게 하려고 펼치면(인라인/언롤링) 코드가 커지고,
코드를 줄이려고 모아두면(함수화) 호출 오버헤드가 생길 수 있다.
“느린 연산”을 알아두면 절반은 먹고 들어간다
임베디드에서 상대적으로 비용이 큰 것들부터 의심하는 게 좋다.
정수 나눗셈과 나머지
정수 연산 중에서 나눗셈(/)과 나머지(%)는 보통 비싸다.
특히 루프 안에서 반복되면 체감이 확 난다.
- 분모가 2의 거듭제곱이면 시프트로 대체 가능
- 상수가 고정이면 곱셈+시프트 형태로 최적화되는 경우도 있음(컴파일러가 해주기도 함)
단, “무조건 곱셈으로 바꿔라”는 위험하다.
오버플로우/정밀도/부호 처리 때문에 결과가 달라질 수 있고,
컴파일러 최적화 옵션에 따라 이미 바뀌어 있을 수도 있다.
부동소수점 연산
FPU가 없는 MCU에서는 float/double이 특히 무겁다.
있더라도 변환이 섞이면 비싸질 수 있다.
- 가능하면 fixed-point(고정소수점)로 설계
- 변환은 마지막 단계에서 한 번만
- 상수는 0.2f처럼 타입을 명확히 해서 불필요한 승격을 줄임
메모리 접근 패턴이 속도를 좌우한다
CPU는 연산 자체보다 메모리를 건드리는 비용이 커서 느려지는 경우가 많다.
전역/volatile 접근 최소화
메모리-mapped I/O나 인터럽트로 바뀌는 값은 volatile을 붙인다.
하지만 volatile은 컴파일러 최적화를 억제해서, 같은 값을 여러 번 읽게 만들 수 있다.
그래서 레지스터/volatile 값을 여러 번 쓸 거면 로컬 변수에 한 번 담아 쓰는 습관이 유효하다.
// 예시: volatile 레지스터를 여러 번 읽지 않도록
uint32_t status = STATUS_REG; // STATUS_REG는 volatile이라고 가정
if (status & ERR_MASK)
{
...
}
if (status & READY_MASK)
{
...
}
이건 “무조건 빠르다”가 아니라, 불필요한 메모리 접근을 줄일 가능성이 크다는 전략이다.
데이터 정렬과 구조체 패딩
정렬이 깨지면 CPU가 한 번에 못 읽어서 여러 번 접근하거나 예외 경로를 타는 MCU도 있다.
특히 통신 패킷 구조체를 그대로 메모리에 얹어 처리할 때 체감이 난다.
분기(조건문)는 생각보다 비싸다
분기는 파이프라인/분기예측(있는 코어라면)에 영향을 준다.
MCU 종류에 따라 정도는 다르지만, “자주 도는 루프에서의 분기”는 최적화 후보 1순위다.
조건을 단순화하고, 루프 안에서 고정되는 건 밖으로 뺀다
루프 내부에서 매번 같은 계산을 하면 그만큼 느려진다.
상수/고정값/루프마다 변하지 않는 값은 밖에서 미리 계산하는 게 유리할 때가 많다.
switch가 유리한 경우
하나의 값으로 다중 분기하는 경우 if-else 연쇄보다 switch가 점프 테이블로 떨어져 빠를 수 있다.
단, 케이스의 분포/범위에 따라 컴파일러가 점프 테이블을 만들지 않을 수도 있다.
결국 “코드로 추측”하지 말고 생성된 어셈블리/맵으로 확인하는 게 확실하다.
함수 호출 비용과 인라인의 트레이드오프
함수 호출은 오버헤드가 있다.
- 레지스터 저장/복원
- 스택 사용
- 분기
그래서 “자주 호출되는 짧은 함수”는 인라인이 속도에 도움이 된다.
하지만 인라인은 ROM을 늘릴 수 있고, I-cache가 작은 환경에서는 오히려 역효과도 날 수 있다.
실무적으로는 이런 기준이 현실적이다.
- ISR/타이트 루프 내부: 짧은 함수는 인라인 고려
- 덜 자주 호출: 함수로 유지해서 코드 크기/가독성 확보
- 성능이 애매하면 disassembly 확인
루프 최적화는 임베디드에서 체감이 크다
임베디드에서 시간 먹는 코드는 대개 루프다.
루프 언롤링
반복 횟수가 작고 루프 오버헤드가 큰 경우 언롤링이 빨라질 수 있다.
대신 코드가 커진다.
카운트다운 루프
일부 아키텍처는 0 비교 형태가 더 효율적인 분기를 제공하는 경우가 있다.
이런 건 “아키텍처 특성”이라, 강의 슬라이드가 말하는 최적화 포인트랑도 연결되는 부분이다.
테이블(lookup table)은 “연산을 메모리로 바꾸는” 전략
복잡한 계산을 매번 하는 대신, 미리 계산한 결과를 테이블로 만들어 두고 인덱싱만 한다.
- 삼각함수/감마/보정값
- CRC 테이블
- 상태 전이 테이블
이건 속도에 매우 효과적이지만,
- ROM이 늘고
- 캐시/메모리 대역폭 영향을 받을 수 있다
즉 실행속도 최적화의 대표적인 “ROM과의 교환”이다.
폴링 vs 인터럽트는 정답이 없다
흔히 “인터럽트가 더 효율적”이라고 생각하지만, 상황에 따라 반대가 된다.
- 이벤트가 너무 잦으면 ISR 오버헤드가 커져서 전체가 느려질 수 있음
- 폴링은 단순하지만 바쁜 대기(busy wait)가 되면 전력/성능 둘 다 악화
결국 핵심은 “주기/빈도/데드라인”이다.
실시간 루프 안에서 폴링이 더 안정적인 경우도 충분히 있다.
최적화는 측정 없이 하면 추측이 된다
실행속도 최적화는 특히 그렇다.
“빨라 보이는 코드”가 실제로는 느릴 수 있다.
확인할 때 많이 쓰는 방법은 두 가지다.
- disassembly로 실제 명령어 수/메모리 접근 패턴 확인
- 타이밍 측정(사이클 카운터, 타이머 핀 토글, 트레이스 등)
임베디드에서는 IDE에 따라 트레이스/프로파일링 도구 지원이 제한될 수도 있어서,
현실적으로는 “어셈블리 확인 + 간단한 타이밍 측정”만으로도 충분히 의미 있는 결론을 낼 수 있다.
정리
실행속도 최적화에서 가장 중요한 질문은 이거다.
- 이 코드는 어디에서 시간을 쓰는가?
- 그 시간이 연산 때문인가, 메모리 접근 때문인가, 분기 때문인가?
- 그리고 그 최적화가 ROM/RAM에는 어떤 대가를 요구하는가?
임베디드 최적화는 결국 속도, ROM, RAM의 균형 문제다.
한쪽만 보고 밀어붙이면 다른 쪽에서 터진다.
그래서 가장 좋은 순서는 보통 이렇다.
- 병목을 찾는다
- 가장 큰 병목부터 줄인다
- 변경 전/후를 측정한다
- ROM/RAM 증가가 허용 범위인지 확인한다
'개발 > 임베디드(Embedded)' 카테고리의 다른 글
| [Autosar] 차량용 통신 구조, 계층(Layer)별로 완벽히 이해하기 (1) | 2026.03.23 |
|---|---|
| [Autosar] AUTOSAR의 정의, 등장 배경과 필요성 (5) | 2026.03.19 |
| [메모리] .bss와 .data는 실제로 어디에 존재하는가 (0) | 2026.03.02 |
| [최적화] ROM, RAM 관점에서 보는 C 코드 최적화 (0) | 2026.03.02 |
| [최적화] 컴파일러 관점에서 보는 최적화의 원리 (0) | 2026.03.02 |