메모리 예산과 배치
110분 안팎
학습 목표
sizeof와 버퍼 개수로 정적 사용량을 계산하고 연속·분산 접근의 호스트 시간을 비교합니다.
개념
메모리 예산은 저장할 객체 목록에서 시작합니다
기록16건과 메시지16건을 저장하면 원시 센서 값32개만 세어도 될까요? 각 기록에는 timestamp와 raw가 있고 큐에는 순번·측정 값·관리 필드가 있습니다. 이번 레슨은 저장 단위와 개수를 목록으로 만들고 sizeof로 호스트 객체 크기를 확인합니다. 처리 속도를 높이려고 배치를 바꿀 때도 먼저 같은 결과를 유지하는지 검사합니다. 예산과 속도는 연결되지만 한쪽 숫자가 다른 쪽의 증거를 대신하지 않습니다.
작은 budget 실습의 Record는 uint32_t timestamp_ms와 uint16_t raw를 가집니다. 필드 폭의 합은6바이트지만 sizeof(Record)는 같지 않을 수 있습니다. 구현은 정렬을 맞추기 위해 필드 사이와 객체 끝에 패딩을 둘 수 있습니다. 배열은 sizeof(Record)를 간격으로 다음 원소를 배치합니다. 따라서 records에6을 곱하는 starter를 실제 객체 크기에 개수를 곱하도록 고칩니다. 출력된 크기는 현재 컴파일 대상 ABI의 값입니다.
budget_bytes는 records와 messages가 각각0부터16이라는 호출 계약을 가집니다. 두 그룹은 이 작은 모델에서 같은 Record 배열로 취급하고 관리 비용64바이트를 더합니다. 64는 제품에서 측정한 큐 크기가 아니라 계산 연습의 고정 가정입니다. 누적 미션에서는 MessageQueue를 직접 sizeof로 계산합니다. 모형의 단순함을 실제 메모리 배치라고 주장하지 않도록 README에 적용 범위를 써 두었습니다.
포함된 배열을 다시 더하지 않습니다
큰 구조체를 sizeof로 셀 때 내부 배열도 이미 포함됩니다. Buffer의 samples16건을 더한 뒤 다시 sizeof(Buffer)를 더하면 같은 저장소를 중복 계산합니다. Pipeline에는 MessageQueue가 들어 있으므로 sizeof(Pipeline)에 큐 슬롯·mutex·조건 변수도 포함됩니다. 반대로 포인터 필드는 가리키는 배열 본체까지 포함하지 않습니다. 객체 목록에는 직접 포함인지 별도 소유인지 표시하여 누락과 중복을 같이 줄입니다.
정적 예산4096바이트를 검사하려면 범위를 먼저 정해야 합니다. 미션의 owned_bytes는 Buffer·Pipeline·EventQueue·Timer·Recovery·Controller·SpiStore·SensorBus·UART13바이트·길이 변수·Latency의 합입니다. 고정 크기 소유 객체의 저장 예산이고 데모에서는 일부가 지역 변수로 놓입니다. 이름에 정적 예산이라고 적더라도 실행 파일의 .bss나 .data 전체 실측과 같은 뜻은 아닙니다. 구조체 하나를 추가하면 목록과 검사를 함께 바꿉니다.
스택은 호출 깊이와 지역 변수에 따라 사용량이 변합니다. 함수 인자·반환 주소·정렬·라이브러리 내부 작업까지 고려해야 하므로 sizeof 목록만으로 최고점을 얻지 못합니다. pthread의 스택과 구현 내부 할당도 이번 합의 밖입니다. 힙을 쓰지 않는 응용 코드라도 운영체제 라이브러리가 메모리를 쓰지 않는다고 말할 수는 없습니다. 실제 보드에서는 타깃 링커 map, 스택 관찰, RTOS 구성값으로 후속 검증을 해야 합니다.
메모리 여유는 보고한 범위 안에서4096에서 owned_bytes를 뺀 값입니다. 한계 바로 아래라고 보드 전체가 안전한 것은 아닙니다. ISR과 태스크의 스택, DMA 버퍼, 주변장치 드라이버, 링크된 정적 데이터의 별도 예산도 필요합니다. 호스트 객체에 들어 있는 pthread 관리 필드는 실제 RTOS 큐 크기와 다릅니다. 그래도 같은 호스트에서 코드 변경 전후를 비교하는 근거로는 유용합니다. 비교 범위가 바뀌면 새 표로 표시합니다.
같은 데이터를 다른 순서로 읽습니다
layout.c는512행512열 uint32_t 표를 만들고 행 순서와 열 순서로 모든 원소를 더합니다. C 배열에서 마지막 인덱스가 이어지는 순서는 연속 접근이고 행을 건너뛰는 순서는 분산 접근입니다. 두 순회 모두 방문 횟수는 같습니다. 합이 같은지 먼저 확인해야 비교할 계산 대상이 유지됩니다. 다른 합이 나오면 빠른 쪽을 선택하는 것이 아니라 인덱스 또는 누락 오류부터 해결합니다.
배열의 연속된 이웃을 가까운 시간에 사용하면 공간 지역성을 기대할 수 있습니다. 그러나 시간 차이를 모두 캐시 미스로 설명할 수는 없습니다. 인덱스 계산·컴파일 옵션·하드웨어의 미리 읽기·작업 집합·호스트 부하도 함께 영향을 줍니다. 이 실습은 시계로 실행 시간을 재며 캐시 미스 계수기를 사용하지 않습니다. 열 순서가 특정 배수 느려야 통과한다는 조건을 넣지 않습니다. 관찰이 역전돼도 같은 합 검사가 핵심입니다.
실행 순서는 turn과 order를 이용하여 번갈아 바뀝니다. 먼저 실행한 순회만 배열 준비의 이익이나 비용을 계속 갖지 않도록 한 조치입니다. 총6쌍 중 turn0은 워밍업으로 표시하고 나머지5쌍을 비교합니다. 중앙값을 사용해 대표값을 정하고 최대값도 같이 보관합니다. 이 설계가 모든 측정 편향을 제거하지는 않으므로 데이터 크기와 시작 조건을 보고서에 남깁니다. 서로 다른 컴파일 옵션의 결과는 한 표 안에서도 구분합니다.
실습 Makefile은 설명을 읽기 쉬운 -O0 조건입니다. 실제 제품 최적화 빌드와 같지 않으므로 -O2로 다시 비교할 때는 정확성 검사도 다시 실행합니다. 최적화가 없으면 주소 계산과 반복문 비용이 더 눈에 띌 수 있습니다. 시간을 줄이려고 결과를 사용하지 않게 만들면 컴파일러가 관심 작업을 제거할 수 있습니다. layout은 각 합을 출력하여 계산의 결과를 관찰합니다. 출력 자체는 측정 구간 밖입니다.
크기와 성능을 분리해 오류를 읽습니다
budget 테스트의 빈 입력은 관리 비용만 남는지 검사합니다. 한쪽만1건인 두 사례는 기록과 메시지 중 하나를 빠뜨리는 오류를 찾습니다. 양쪽16건은 용량 경계입니다. starter는 필드 합6을 사용해서 패딩이 있는 현재 ABI에서 실패합니다. record 출력으로 실제 sizeof를 확인합니다. 다른 ABI에서 우연히6이라면 starter 결함이 드러나지 않을 수 있어 테스트를 돌리는 대상 환경도 명시합니다.
컴파일에서 unknown type name uint32_t가 나오면 budget.h의 stdint.h 포함을 확인합니다. size_t에는 stddef.h가 필요합니다. printf에서 %zu를 쓰는 이유는 sizeof 결과가 size_t이기 때문입니다. %d로 출력한 뒤 경고를 무시하면 숫자가 맞아 보여도 형식 계약이 깨집니다. -Werror에서 경고가 실패가 된 것을 실행 로직의 실패와 구별합니다. 오류 위치가 헤더이면 해당 선언을 포함한 번역 단위부터 읽습니다.
배치 최적화를 위해 구조체를 강제로 압축하는 접근은 이번 해결책이 아닙니다. 패딩을 줄이면 정렬되지 않은 접근이나 타깃별 제약을 새로 만들 수 있습니다. 통신 프레임은 앞 모듈 UART의13바이트 직렬화 계약을 그대로 사용하고 sizeof(BusRecord) 바이트를 전송하지 않습니다. 메모리 내부 배치와 와이어 형식을 분리해야 컴파일 대상이 바뀌어도 통신 규격을 보존할 수 있습니다.
코드 리뷰에서는 객체 이름·단위 크기·개수·합계·포함 관계를 같이 보여 줍니다. 제품 요구가 기록32건으로 바뀌면 용량과 sizeof 합뿐 아니라 가득 찼을 때 정책·처리 지연도 다시 확인합니다. 메모리를 줄이려고 큐를 축소한 실험에서 DROP_NEW가 늘었다면 손실과 저장 예산의 교환을 드러냅니다. 숫자를 줄였다는 사실만으로 개선이라고 결론내리지 않습니다. 측정 표에는 이전 버전과 동일한 관찰 조건을 붙입니다.
연속·분산 접근 실험은 저장 순서와 방문 순서의 관계를 익히는 데 사용합니다. 센서 기록기의 실제 경로를 개선하려면 사용하는 필드와 생산자·소비자의 방문 순서를 관찰해야 합니다. 이번 단일 스레드 표 순회로 앞 모듈 pthread의 거짓 공유를 재현했다고 말하지 않습니다. 캐시와 메모리 계층의 더 넓은 설명은 연결한 서재 장에서 읽고, 여기서는 합 검증·sizeof 예산·호스트 시간 기록을 제출합니다.
따라하기
필드 합 계산의 결함 재현
starter에서 record 크기와 owned 크기를 읽습니다. record8에 비해6바이트를 곱하여 세 조건이 실패합니다.
make -s test실행 결과
budget: 4 checks, 3 failures record=8 owned=256
sizeof 예산 구현
budget.c를 객체 크기×개수+64로 고칩니다. 아래는 solution의 실행 결과이며 ABI가 다르면 크기 숫자는 달라질 수 있습니다.
make -s test실행 결과
budget: 4 checks, 0 failures record=8 owned=320
순회 순서 실측
완성 코드에서 표 순회를 빌드하고 실행합니다. turn0은 워밍업입니다. 이후 각 순회의 합을 먼저 확인하고 시간을 비교합니다. 이 실행에서는 순회 차이가 작습니다.
make -s layout
./layout실행 결과
host turn=0 mode=row sum=34359607296 elapsed_ms=0.471 host turn=0 mode=column sum=34359607296 elapsed_ms=0.498 host turn=1 mode=column sum=34359607296 elapsed_ms=0.441 host turn=1 mode=row sum=34359607296 elapsed_ms=0.470 host turn=2 mode=row sum=34359607296 elapsed_ms=0.435 host turn=2 mode=column sum=34359607296 elapsed_ms=0.440 host turn=3 mode=column sum=34359607296 elapsed_ms=0.439 host turn=3 mode=row sum=34359607296 elapsed_ms=0.469 host turn=4 mode=row sum=34359607296 elapsed_ms=0.443 host turn=4 mode=column sum=34359607296 elapsed_ms=0.441 host turn=5 mode=column sum=34359607296 elapsed_ms=0.439 host turn=5 mode=row sum=34359607296 elapsed_ms=0.436
대표값 계산
앞 실행의 워밍업 제외 값을 사용한 독립 계산입니다. 새 실행을 분석할 때는 목록을 본인의 실측으로 바꿉니다.
from statistics import median
row=[0.470,0.435,0.469,0.443,0.436]
column=[0.441,0.440,0.439,0.441,0.439]
print(f"row_median_ms={median(row):.3f} column_median_ms={median(column):.3f}")실행 결과
row_median_ms=0.443 column_median_ms=0.440
확인 문제
실습
budget_bytes의 패딩 누락을 수정합니다. records/messages는 각각0~16이라는 계약입니다. make test로 빈·단일·16건 예산을 확인하고 make -s layout 및 ./layout으로 같은 합·6쌍 시간·워밍업 제외 중앙값을 기록합니다. 시간 우열 자체는 통과 조건이 아닙니다.
실행 명령
make test
기대 결과
budget: 4 checks, 0 failures
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 정적 소유 객체 예산과 스택 최고점의 차이를 설명해 주세요.
- 구조체 크기와 UART 프레임 크기가 다른 이유를 설명해 주세요.