13.7.2.3 자율주행 레이더 센서 트래킹(Kalman Filter) 맞춤형 슬라이딩 윈도우(Sliding Window) 캐싱 구조

13.7.2.3 자율주행 레이더 센서 트래킹(Kalman Filter) 맞춤형 슬라이딩 윈도우(Sliding Window) 캐싱 구조

비중첩 텀블링 윈도우(Tumbling Window, 13.7.2.2 참조)가 데이터를 뭉텅이로 토막 내는 거친 도축에 가깝다면, 슬라이딩 윈도우(Sliding Window) 는 파이프라인의 스트림 타임라인 위를 뱀처럼 기어가며 상태를 이어가는(Overlapping) 궁극의 이음매 연산술이다.

앞차와의 거리를 추적하는 레이더(Radar) 데이터가 초당 10번(10Hz) 들어온다고 치자. 자율주행 알고리즘 개발자가 “과거 1초(10개 패킷) 동안의 평균 속도와 노이즈 편차를 잡아내어 추적(Kalman Filtering) 하라” 고 명령했다.
만역 여기서 1초짜리 텀블링 윈도우를 때려버리면 차는 1초 동안 눈먼 장님이 되었다가 1초마다 툭툭 판단을 던지며 시스템을 전복시킨다. 판단은 0.1초마다 계속 부드럽게 이루어지되(Slide Step), 판단의 뇌 속에는 늘 과거 1초 치의 기억(Size)을 붙들고 있어야 한다.
본 절에서는 낡은 기억을 버리고 새로운 기억을 채우는 메모리 링버퍼(Ring Buffer) 철학과, 타임 커널의 무한 스태킹(Stacking)을 방어하는 슬라이딩 지휘 런북을 전개한다.

1. Window Size 와 Slide Step 사이즈의 비대칭 지배

슬라이딩 윈도우 아키텍처는 매니페스트(YAML)에서 본능적으로 두 개의 파라미터 톱니바퀴를 결속시켜야 명제가 성립된다.

  1. 윈도우 사이즈 (Size): 무거운 연산(예: 칼만 궤적 보정)을 태우기 위해 한 번에 움켜쥐어야 하는 과거의 공간 크기. (예: 1초, 또는 과거 10개의 데이터)
  2. 슬라이드 스탭 (Step): 그 무거운 윈도우 연산을 “얼마나 자주, 얼마나 미끄러지면서(Slide) 다시 깨워 호출할 것인가?“를 결정짓는 파동. (예: 0.1초 혹은 1개의 데이터 삽입마다)

이 격차(Size > Step)가 만들어내는 시공간의 결과는 찬란하다.
0.1초마다 새로운 레이더 패킷이 큐 배관을 타고 오퍼레이터에 안착될 때(Step 충족), 그 파이프라인 관문 스레드는 뒷단으로 데이터 1개를 내보내는 것이 아니라 “거대한 10개의 중첩(Overlapped) 배열” 하나를 통째로 포장해서 연산 코어로 쑤셔 넣는다(Window Invocation).

2. 링버퍼(Ring-Buffer)와 메모리 카피리스(Copy-less) 슬라이딩 전략

1번 작동마다 10짜리 배열을 무지성으로 메모리에 10개씩 만들어 낸다면, 파이프라인 RAM 대역폭은 1초도 안되어 스레기 조각(Garbage) 배열들로 찢어져 나간다.
Zenoh-Flow 급의 고성능 스트림 통제기에서 슬라이딩 윈도우를 작성하는 C++ 엔지니어는 절대 std::vector 나 동적 재할당 메모리 복사를 쓰지 않는다. 고정 길이 링버퍼(Ring Buffer) 포인터 락을 파이프라인 메모리에 강제한다.

// [Radar 슬라이딩 윈도우를 위한 Circular/Ring Buffer 캐싱 런북]

// 1. 메모리 재할당(Re-alloc)을 아예 말살하기 위해 10개짜리 고정 상자를 힙에 말뚝 박는다.
std::array<zflow::Data, 10> history_ring_buffer;
int head_index = 0;

void on_slide_step(zflow::Data new_radar_capsule) {
    
    // 2. 가장 오래된 기억(과거 1초 전 데이터) 포인터를 소규모 덮어쓰기 파괴(Overwrite)
    // 메모리를 이동(shift)하지 않고! 단순히 인덱스 헤드(head) 위치만 전진(Ticking)시킨다.
    history_ring_buffer[head_index] = std::move(new_radar_capsule);  
    head_index = (head_index + 1) % 10; // 원형(Ring) 꼬리물기 강제 이동
    
    // 3. 링버퍼 공간 자체가 과거 10개를 온전히 들고 있는 무결점의 Window 객체다!
    // 이 메모리 블록을 묶어 Kalman Filter 모듈 스레드로 넘겨 연산(Invoke)을 태운다.
    invoke_kalman_fusion(history_ring_buffer);
}

이 원형 슬라이딩 배관법을 박아넣는 순간, 메모리 이동 함수(memmove)나 버퍼 복사는 수학적 ’제로(0)’에 수렴한다. 데이터는 미끄러지지 않으며, 오직 그 데이터를 지목하는 가벼운 인덱스 화살표(Head Pointer)만이 쉴 틈 없이 과녁을 미끄러지며(Slide) 돌아가며 무결한 윈도우 배열 그룹을 칼만 필터 엔진 앞으로 바쳐대는 기만 전술이 완성된다.

3. 타임라인 지연(Late Data) 중첩 보정의 충돌

문제는 이 슬라이딩의 그물이 워터마크(Watermark)와 만날 때 발생한다.
이 방식은 앞선 13.7.2.2 텀블링이나 13.7.1.3보다 훨씬 더 타임 지터(Jitter) 충돌에 민감하다. 지각(Late) 데이터 하나가 과거의 빈 구멍으로 파고들면, 이미 지나쳐 연산이 가해진 여러 개의 슬라이딩 윈도우 결괏값들 자체를 “그때 연산 결과는 오류였다!” 고 뒤집어엎고(Retraction / Re-emission) 연산을 수 번이나 재구동 해야 하는 지옥의 시계열 무결성 재조정(Stitching) 스레드가 발생할 여지가 농후하다.

그러므로 하드 리얼타임 데이터 플로우 망에서, 슬라이딩 창문 런북은 C++ 메모리 안의 무결점 링버퍼 최적화를 전제로 하되, “이미 지나친 슬라이딩 윈도우 창틈에 끼여 뒤늦게 도착한 과거(1초 전) 데이터를 보상하려는 미련“을 모두 도살(Watermark Dropping)해 버리는 거친 꼬리 자르기 설계와 함께 병용되어야만 99.9% 의 제어 레이턴시 한계를 준수하는 극단적 통치권을 이룩할 수 있다.