YAML-CPP 메모리 조각화 해결을 위한 커스텀 할당기 설계 및 구현

YAML-CPP는 C++ 기반 YAML 파서 라이브러리로 대규모 데이터 처리 시 메모리 관리 문제가 발생할 수 있습니다. 특히 고빈도 파싱 작업에서 표준 할당기의 한계로 인해 메모리 조각화가 심화되며, 이는 성능 저하와 불안정한 동작을 유발합니다. 본 문서에서는 노드 트리 구조의 메모리 할당 패턴을 분석하고 맞춤형 할당기 구현을 통해 최적화하는 방법을 제시합니다.

메모리 조각화의 근본 원인

YAML 문서의 중첩 구조 처리 시 NodeData 객체가 반복적으로 생성/소멸되며 작은 크기의 메모리 블록이 분산됩니다. 특히 src/node_data.cpp에서 확인되는 표준 할당 패턴이 문제를 야기합니다:

NodeData::NodeData() {
  properties = reinterpret_cast<PropertyMap*>(::operator new(sizeof(PropertyMap)));
  children = reinterpret_cast<NodeList*>(::operator new(sizeof(NodeList)));
}

이러한 개별 할당 방식은 수천 개의 노드를 처리할 때 운영체제의 메모리 관리 부하를 증가시키고, 가용 메모리 공간이 조각화되어 실제 사용 가능량이 감소합니다.

메모리 관리 추상화 인터페이스

YAML-CPP는 include/yaml-cpp/node/detail/memory.h에서 확장 가능한 메모리 인터페이스를 제공합니다:

class MemoryManager {
public:
  virtual void* request(size_t bytes) = 0;
  virtual void release(void* ptr) = 0;
  virtual ~MemoryManager() = default;
};

이 인터페이스를 기반으로 특정 작업 부하에 최적화된 할당 전략을 구현할 수 있습니다.

메모리 풀 기반 할당기 구현

고정 크기 블록을 미리 할당하는 풀 관리 방식은 조각화를 효과적으로 억제합니다:

class BlockAllocator : public MemoryManager {
  struct MemoryBlock {
    char* start;
    size_t capacity;
    size_t used;
  };
  
  std::vector<MemoryBlock> blocks;
  const size_t BLOCK_SIZE = 4096;
  
public:
  void* request(size_t size) override {
    if(blocks.empty() || blocks.back().remaining() < size) {
      allocateNewBlock();
    }
    return blocks.back().allocate(size);
  }
  
  void release(void* ptr) override {
    // 블록 내 해제 처리 (풀 관리 특성상 전체 해제)
  }
};

이 구현은 노드 객체 생성 시 시스템 콜 호출 횟수를 대폭 감소시키며, 메모리 배치 최적화를 통해 조각화를 방지합니다.

실전 적용 전략

1. 노드 생성 프로세스 수정
기존 new 연산자 대신 커스텀 할당기 사용:

// src/node_builder.cpp 수정
NodeData* createNode() {
  auto* data = static_cast<NodeData*>(
    MemoryManager::global().request(sizeof(NodeData))
  );
  new(data) NodeData(); // Placement new
  return data;
}

2. 스레드 안전성 강화
다중 스레드 환경에서 충돌 방지를 위한 뮤텍스 통합:

class ThreadSafeAllocator : public BlockAllocator {
  std::mutex lock;
public:
  void* request(size_t size) override {
    std::scoped_lock guard(lock);
    return BlockAllocator::request(size);
  }
};

3. 메모리 사용량 모니터링
할당 통계 수집을 통한 성능 분석:

class TracingAllocator : public MemoryManager {
  size_t totalAllocated = 0;
  MemoryManager* target;
public:
  void* request(size_t size) override {
    totalAllocated += size;
    return target->request(size);
  }
};

고급 최적화 기법

임베디드 시스템 최적화
src/ptr_vector.h의 컨테이너를 사용해 메모리 오버헤드 감소:

// 기존: std::vector<Node*>
// 개선: CompactVector<Node> (연속 메모리 블록 사용)

데이터 스트림 처리
일회성 처리 작업에 적합한 영역 기반 할당기:

class RegionAllocator : public MemoryManager {
  std::vector<char> buffer;
  size_t offset = 0;
public:
  void* request(size_t size) override {
    if(offset + size > buffer.size()) {
      buffer.resize(buffer.size() * 2 + size);
    }
    void* ptr = buffer.data() + offset;
    offset += size;
    return ptr;
  }
  
  void reset() { offset = 0; } // 전체 메모리 일괄 해제
};

이 할당기는 대용량 YAML 데이터 처리 후 reset() 호출로 즉시 메모리를 정리할 수 있습니다.

태그: yaml-cpp memory-allocator c++-optimization

8월 4일 10:36에 게시됨