Proven by Intelligence
보이지 않는 안전을 인텔리전스로 증명하다.
기술 인사이트를 만나보세요.
실행시간 검증, 측정한 값을 WCET라고 증명할 수 있나요?
실시간 시스템의 Task는 정해진 주기 안에 반드시 실행되어야 합니다. 그래서 가장 나쁜 조건에서도 주기를 지킬 수 있는지, 즉 WCET(Worst-Case Execution Time, 최악의 실행시간)를 확인해야 합니다.
그렇다면 WCET는 어떻게 구할까요?
가장 널리 쓰이는 방법은 타겟에서 실행시간을 직접 측정하는 것입니다. 함수 앞뒤에 타이머를 읽는 탐침 코드를 넣어 시간을 기록하거나, GPIO 핀을 켰다 끄는 코드를 넣고 오실로스코프로 펄스 폭을 확인합니다.
두 방법 모두 간단하고 익숙하지만, 코드를 실행해서 얻은 값이라는 점은 같습니다. 그렇다면 이렇게 측정한 값을 두고 다음 질문에 답할 수 있어야 합니다.
" 이 값이 WCET임을 증명할 수 있나요? "
" 측정한 상황이 Worst Case라고 어떻게 확신하나요? "
위의 질문에 선뜻 답하기 어렵습니다. 측정하는 방식에는 다음과 같은 한계가 있기 때문입니다.
🛠️ 타이밍에 영향을 주는 요소가 많습니다.
파이프라인 지연, 캐시 Hit/Miss(소요 주기가 10~100배 이상 차이), 분기 예측 실패 등이 실행시간에 영향을 미칩니다.
🛠️ 같은 코드도 실행할 때마다 시간이 다릅니다.
이 요소들 때문에 측정값이 매번 달라지고, 여러 번 측정하여 값이 안정적이어도 최악의 상황이 한 번이라도 있었는지는 알 수 없습니다.
🛠️ 탐침 코드가 측정 대상을 바꿉니다.
타이머 코드든 GPIO 토글 코드든, 넣는 순간 본래 동작과 메모리 구조에 영향을 주기 때문에 순수한 실행시간을 확인하기 어렵습니다.
결국 측정으로 얻을 수 있는 것은 ACET(Average Case Execution Time, 평균 실행시간) 수준이고, WCET는 측정만으로 확인하기 어렵습니다.
그래서 필요한 것이 분석입니다. AbsInt TimingProfiler는 타겟 보드 프로세서의 파이프라인, 캐시, 메모리 버스처럼 타이밍에 영향을 주는 하드웨어 아키텍처를 수학적으로 모델링해 두고, 최종 바이너리를 분석해 WCET를 구합니다. 실행해서 측정하는 것이 아니라 하드웨어 모델로 계산하므로 실행할 때마다 달라지는 값에 좌우되지 않고, 최종 바이너리를 그대로 분석하므로 탐침 코드를 넣을 필요도 없습니다.
이번 글에서는 1ms 주기로 CAN 메시지를 수신하는 task_1ms_CAN 함수를 TimingProfiler로 분석하고 병목 위치와 원인을 찾아 줄이는 과정을 살펴보겠습니다.
병목 위치 찾기: Call Graph와 WCET Estimate
TimingProfiler로 분석한 결과, task_1ms_CAN의 WCET는 21.204 us입니다. 이 시간이 어디에서 쓰이는지 Call Graph부터 살펴보겠습니다.
① Call Graph: 호출 흐름과 실행시간 확인
Call Graph에서는 Task 안에서 WCET에 영향을 미치는 하위 함수 호출 관계를 파악하고 각 함수 블록에 표기된 단일 함수의 실행 시간과 Task의 누적 실행 시간을 직관적으로 확인할 수 있습니다.

[Call Graph View]
하지만 위의 정보만 가지고 병목 현상의 위치와 원인을 정확하게 판단하기에 어려움이 있습니다.
② WCET Estimate: 병목 루틴 찾기
이럴 때 WCET Estimate 통계 뷰를 확인하여 병목 루틴을 찾을 수 있습니다.
초록색은 함수 자체의 순수 실행시간, 파란색은 하위 호출을 포함한 누적 실행시간을 나타냅니다. 초록색 그래프가 특정 루틴에서 유독 높다면, 해당 구간이 전체 실행 시간을 지연시키는 주요 요인입니다.

[WCET Estimate View]
샘플에서는 Task 안의 반복문 task_1ms_CAN.L1이 가장 높게 나타납니다.
<task_1ms_CAN.L1가 WCET에 미치는 영향>
- 호출 횟수: 32회
- Self 실행시간: 11.410us(53.81%)
- 누적 실행시간: 14.567us(69%)
즉, 이 반복문 하나가 Task 전체 실행 시간의 약 69%를 차지합니다.
원인 코드 찾기: Variable Usage와 소스코드
병목이 발생하는 지점(루틴)을 특정했다면 이제 Variable Usage 탭에서 원인을 찾아보겠습니다.
① Variable Usage: Read/Write 횟수가 많은 변수 찾기
특정 변수의 Read/Write 비율이 지나치게 높게 나타난다면, 불필요한 메모리 접근이 실행 시간을 증가시키고 있을 가능성이 큽니다.

[Variable Usage View]
이 그래프에서 Read, Write가 많은 변수는 다음과 같습니다.
- msg_buffer0: Write 132회(전체 Write의 70.59%)
- msg_length: Read 33회
반복문이 32번 실행되는 동안 이 변수들을 계속 읽고 쓰고 있다는 뜻입니다. 그렇다면 코드의 어느 부분에서 접근하는지 확인해 보겠습니다.
② Show in call graph, Show source: 변수 접근 위치를 Call Graph 및 소스코드에서 확인

[show in call graph, show source 기능]
의심되는 변수를 클릭하여 Show in call graph를 실행하면, 해당 변수에 접근하는 명령어를 Call Graph에서 확인할 수 있습니다. 반복 구간 안에서 ldrb, ldr, strb 같은 메모리 접근 명령어가 반복할 때마다 실행되고 있습니다.
이어서 해당 영역을 클릭해 Show source를 실행하면 대응되는 소스코드로 바로 이동합니다. 소스코드를 확인한 결과 반복문 안에서 Read/Write가 계속 발생하고 있었습니다.
즉, 아래 두 가지가 실행시간 증가의 주된 원인으로 판단됩니다.
- 반복 조건: msg_length를 반복할 때마다 메모리에서 다시 읽음
- 구조체 초기화: can_frame을 반복문 안에서 선언하고 초기화하기 때문에 매번 Write가 발생
코드 수정 및 결과 검증
원인을 파악했으니 소스코드를 수정하고 다시 분석해 보겠습니다.
① 코드 수정: 반복문 안의 Read/Write를 밖으로 옮기기
반복할 때마다 읽던 msg_length를 반복문 진입 전 지역 변수로 한 번만 읽도록 분리하고, 초기화 구문 역시 반복문 밖으로 이동시켜 메모리 접근 오버헤드를 최소화했습니다.

[수정 코드]
② 재분석 수행: 수정 전후 비교
수정한 뒤 같은 방법으로 다시 분석하면 아래와 같이 개선 효과를 확인할 수 있습니다.

[전체 실행시간 감소]

[WCET Estimate 시간 감소]

[Variable Usage에서 msg_length, msg_buffer 제외]
재분석 결과, Variable Usage에서 Read/Write 횟수가 눈에 띄게 줄었고, 병목의 원인이던 msg_buffer0과 msg_length는 목록에서 빠졌습니다. 전체 변수 접근 횟수도 425회에서 183회로 약 57% 줄었습니다. WCET Estimate의 실행 시간도 함께 낮아져, 병목이 완화된 것을 확인할 수 있습니다.

[코드 수정 전 vs 후 결과]
마치며
이번 글에서는 TimingProfiler로 바이너리를 분석해 WCET를 확인하고, 실행 시간 개선까지 진행했습니다.
✅ WCET Estimate로 병목 구간 확인
✅ Variable Usage와 소스코드를 통해 원인 확인
✅ 코드 수정 후 재분석으로 개선 효과 비교
같은 프로세스를 측정 방식으로 진행했다면, 21.204us와 16.367us의 차이가 코드 수정 때문인지 측정할 때마다 달라지는 값 때문인지 구분하기 어려웠을 것입니다.
반면 분석 방식은 같은 하드웨어 모델과 같은 기준으로 결과를 계산하므로, 수정 전후에 따른 결과를 비교할 수 있었습니다. 게다가 병목이 어디에 있고 무엇이 원인인지까지 보여 주기 때문에 개선 방향도 손쉽게 찾을 수 있습니다.
즉, 측정만으로 WCET임을 증명하기 어렵지만 하드웨어 모델을 기반으로 최종 바이너리를 분석하면 Task의 WCET를 보장할 수 있습니다.
솔루션과 관련하여 소개 및 데모가 필요하시면 아래의 연락처로 문의해 주시기 바랍니다.
📧 absint@mdssphere.co.kr ✍️ 문의남기기
