때로는 성급한 최적화도 재미있다 (2025)
요약
ICMP Echo Request 기록을 위한 링 버퍼 구조체를 최적화하는 실험적 과정을 다룹니다. 공용체 사용, 정밀도 축소, 비트필드 적용 등을 통해 메모리 사용량을 12KiB에서 4KiB로 줄이는 과정을 설명합니다.
핵심 포인트
- 공용체를 활용해 송신 시각과 지연 시간을 통합하여 메모리 절감
- 비트필드 적용 시 구조체 패딩으로 인한 추가 절감 한계 확인
- 필드 배치와 정렬 요구사항이 메모리 크기에 미치는 영향 분석
- 소스 주소 대신 ICMP 식별자를 활용한 구조체 크기 최소화
- 연결성 모니터링 시스템의 ICMP Echo Request 기록 구조체를 줄이는 과정에서
링 버퍼 메모리 사용량이 12KiB에서 4KiB로 감소함
sent_ns
와 received_ns
를 모두 저장하지 않고 수신 후에는 지연 시간만 남기도록 공용체를 쓰자 배열 크기가 8KiB로 줄어듦
- 나노초 정밀도 대신 100마이크로초 단위를 쓰고
received
를 비트필드로 바꿨지만, 구조체 패딩 때문에 추가 절감은 발생하지 않음
- 소스 주소 대신 ICMP
identifier
의 일부 의미를 4비트 카운터로 대체하면서 구조체가 8바이트로 줄고 512개 배열이 4KiB가 됨
- 애플리케이션은 메모리 제약이 없었기 때문에 실용적 필요는 없었지만, 필드 배치와 비트 접근 비용까지 따지는 최적화 실험이 됨
문제 설정: ping 기록을 저장하는 방식
- 연결성 모니터링 시스템은 여러 서버에 ICMP Echo Request를 보내고, 1분·5분·15분 구간의 지연 시간과 패킷 손실 평균을 관찰함
- 처음 떠올린 저장 방식은 512개 엔트리의 링 버퍼이며, 각 엔트리는 송신 시각, 수신 시각, 소스 주소, 시퀀스 번호, 수신 여부를 담음
- 초기 구조체 배열
pings_rb[512]
의 크기는 12KiB로 측정됨
struct ping_timestamp {
uint64_t sent_ns;
uint64_t received_ns;
in_addr_t source_addr;
uint16_t seq_no;
bool received;
};
첫 번째 절감: 송신 시각과 경과 시간을 공용체로 통합
- 실제로 남기고 싶은 값은 수신 이후의
received - sent
지연 시간이므로, 송신 시각과 경과 시간을 동시에 보관할 필요가 없음
sent_ts
와 elapsed_ts
를 공용체로 묶은 구조체는 같은 슬롯을 송신 전에는 송신 시각으로, 수신 후에는 경과 시간으로 사용함
- 이 변경 뒤 512개 배열 크기는 12KiB에서 8KiB로 줄어듦
struct ping_timestamp_2 {
union {
uint64_t sent_ts;
uint64_t elapsed_ts;
};
in_addr_t source_addr;
uint16_t seq_no;
bool received;
};
두 번째 시도: 정밀도 축소와 비트필드
- ping 시간은 수십·수백·수천 밀리초 단위로 측정되므로 나노초 정밀도를 모두 저장할 필요가 없음
- 시간 단위를 100마이크로초, 즉 0.1ms 단위로 바꾸면 43비트로 최대 20년 동안의 ping 추적이 가능함
received
의 참·거짓 값에 8비트를 쓰는 것은 과하므로 비트필드를 적용함
- 하지만
ping_timestamp_3
의 배열 크기도 8KiB로 유지되어 추가 절감이 발생하지 않음
struct ping_timestamp_3 {
uint64_t sent_or_elapsed_ts: 43;
uint64_t received: 1;
uint64_t seq_no: 16;
in_addr_t source_addr;
};
구조체 패딩 때문에 줄어들지 않은 크기
ping_timestamp_2
는 마지막에 정렬 요구사항을 맞추기 위한 패딩 바이트가 붙음
ping_timestamp_3
는 첫 8바이트에 시간, 수신 여부, 시퀀스 번호를 넣지만, 뒤에 소스 주소와 패딩이 남음
- 비트필드를 적용했어도 36비트의 패딩이 남아 구조체 전체 크기가 줄지 않음
- 단순히 bool을 비트로 줄이는 것만으로는 메모리 배치와 정렬 문제를 해결하지 못함
소스 주소 제거와 4비트 카운터
- 제품이 모바일 데이터 네트워크에서 동작하는 동안 소스 주소가 자주 바뀌기 때문에 기존 구조체는 소스 주소를 보관함
- 주소가 바뀔 때 시퀀스 번호도 재설정되며, 과거에는 서로 다른 소스 주소와 같은 시퀀스 번호를 가진 패킷이 동시에 처리된 적이 있음
- ICMP Echo Request에는 애플리케이션이 자신이 보낸 패킷을 식별할 수 있는 16비트
identifier
필드가 있음
- 전체 16비트를 모두 쓸 필요가 없으므로, 남는 4비트를 소스 주소 변경 시 증가하는 롤링 카운터로 사용함
- 이 카운터는 애플리케이션의 다른 위치에서 감시되는 소스 주소 변경에 맞춰 증가함
struct ping_timestamp {
uint64_t elapsed_or_sent_ts : 43;
uint64_t received : 1;
uint64_t counter: 4;
uint64_t seq_no: 16;
};
최종 결과와 필드 배치
- 최종 구조체는 소스 주소 필드를 제거하고 64비트 안에 시간, 수신 여부, 카운터, 시퀀스 번호를 담음
- 512개 링 버퍼 배열 크기는 4KiB가 되어 한 페이지 데이터로 줄어듦
- 초기 12KiB 대비 총 8KiB를 절감함
- 필드 순서는
seq_no
가 16비트 경계에 맞도록 조정되어, 로드 시 시프트 없이 단일 ldrh
명령으로 읽을 수 있음
elapsed_or_sent_ts
를 읽을 때는 마스크만 필요함
추가 최적화: 수신 비트 접근 비용 줄이기
struct ping_timestamp {
uint64_t elapsed_or_sent_ts : 43;
uint64_t counter: 4;
uint64_t not_received : 1;
uint64_t seq_no: 16;
};
결론
- 최적화 결과는 메모리 사용량을 12KiB에서 4KiB로 줄였지만, 애플리케이션 자체는 메모리 제약을 받지 않음
- 실제 필요성과 별개로 구조체 레이아웃, 패딩, 비트필드, 명령어 수준 접근 비용을 따져보는 실험이 됨
- 마지막 주석에서는 “문제”라는 표현도 느슨하게 쓴 것이며, 벤치마크조차 하지 않았다고 밝힘
AI 자동 생성 콘텐츠
본 콘텐츠는 GeekNews의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기