콘텐츠로 이동

Placement New & 메모리 풀 구현

placement new는 이미 할당된 메모리 주소에 객체를 생성하는 C++ 기능입니다. 메모리 풀, 임베디드 시스템, 고성능 서버에서 힙 할당 오버헤드를 제거하는 핵심 도구입니다.


#include <new> // placement new 헤더
// 1. 메모리 확보 (어디서든 가능)
alignas(MyClass) char buffer[sizeof(MyClass)];
// 2. 그 메모리 위에 객체 생성
MyClass* obj = new (buffer) MyClass(arg1, arg2);
// 3. 명시적 소멸자 호출 (delete 사용 금지!)
obj->~MyClass();
// 4. 메모리는 별도로 해제 (buffer는 스택이므로 자동 해제)

placement new로 생성한 객체는 delete를 호출하면 안 됩니다. delete는 소멸자 호출 + 메모리 해제를 함께 하기 때문에 이미 관리되는 메모리를 해제하려 해 undefined behavior가 됩니다.


struct alignas(16) SIMD_Vector {
float x, y, z, w;
};
// 잘못된 예 — 정렬 보장 없음
char raw[sizeof(SIMD_Vector)];
auto* v = new (raw) SIMD_Vector(); // 16바이트 정렬 보장 안 됨 → UB
// 올바른 예 1 — alignas 사용
alignas(SIMD_Vector) char aligned[sizeof(SIMD_Vector)];
auto* v = new (aligned) SIMD_Vector();
// 올바른 예 2 — std::aligned_storage (C++11, deprecated in C++23)
std::aligned_storage_t<sizeof(SIMD_Vector), alignof(SIMD_Vector)> storage;
auto* v = new (&storage) SIMD_Vector();
// 올바른 예 3 — std::aligned_alloc (C++17)
void* mem = std::aligned_alloc(alignof(SIMD_Vector), sizeof(SIMD_Vector));
auto* v = new (mem) SIMD_Vector();
v->~SIMD_Vector();
std::free(mem);

동일 크기의 객체를 대량 생성·소멸할 때 가장 효과적입니다.

#include <cstddef>
#include <cassert>
#include <new>
template <typename T, std::size_t Capacity>
class FixedPool {
public:
FixedPool() {
// 프리 리스트 초기화
for (std::size_t i = 0; i < Capacity - 1; ++i)
blocks_[i].next = &blocks_[i + 1];
blocks_[Capacity - 1].next = nullptr;
freeHead_ = &blocks_[0];
}
template <typename... Args>
T* allocate(Args&&... args) {
if (!freeHead_) return nullptr; // 풀 소진
Block* block = freeHead_;
freeHead_ = freeHead_->next;
// 블록 메모리에 T 객체 생성
return new (&block->storage) T(std::forward<Args>(args)...);
}
void deallocate(T* ptr) {
if (!ptr) return;
ptr->~T(); // 명시적 소멸자 호출
// 블록을 프리 리스트로 반환
Block* block = reinterpret_cast<Block*>(ptr);
block->next = freeHead_;
freeHead_ = block;
}
std::size_t available() const {
std::size_t count = 0;
for (Block* b = freeHead_; b; b = b->next) ++count;
return count;
}
private:
union Block {
alignas(T) char storage[sizeof(T)];
Block* next;
};
Block blocks_[Capacity];
Block* freeHead_ = nullptr;
};
// 사용 예
FixedPool<Bullet, 256> bulletPool;
Bullet* b = bulletPool.allocate(position, velocity);
// ... 게임 로직 ...
bulletPool.deallocate(b);

장점:

  • 할당/해제: O(1)
  • 메모리 단편화 없음
  • 캐시 친화적 (연속 메모리)

레벨 로딩처럼 일괄 해제가 가능한 경우에 적합합니다.

#include <cstddef>
#include <cstdint>
#include <cassert>
class StackAllocator {
public:
explicit StackAllocator(std::size_t size)
: buffer_(new std::byte[size])
, capacity_(size)
, offset_(0) {}
~StackAllocator() { delete[] buffer_; }
// 정렬을 고려한 할당
void* allocate(std::size_t size, std::size_t alignment = alignof(std::max_align_t)) {
std::size_t aligned = align_up(offset_, alignment);
assert(aligned + size <= capacity_);
void* ptr = buffer_ + aligned;
offset_ = aligned + size;
return ptr;
}
// 마커 기반 해제
using Marker = std::size_t;
Marker getMarker() const { return offset_; }
void freeToMarker(Marker marker) {
assert(marker <= offset_);
offset_ = marker;
// 소멸자는 사용자가 직접 호출해야 함!
}
// 전체 리셋
void reset() { offset_ = 0; }
std::size_t used() const { return offset_; }
std::size_t remaining() const { return capacity_ - offset_; }
private:
std::size_t align_up(std::size_t val, std::size_t align) {
return (val + align - 1) & ~(align - 1);
}
std::byte* buffer_;
std::size_t capacity_;
std::size_t offset_;
};
// 사용 예
StackAllocator frameAlloc(1024 * 1024); // 1MB 프레임 할당기
void renderFrame() {
auto marker = frameAlloc.getMarker();
// 프레임 내 임시 데이터 할당
void* mem = frameAlloc.allocate(sizeof(TempData), alignof(TempData));
TempData* tmp = new (mem) TempData();
// ... 렌더링 ...
tmp->~TempData(); // 소멸자 명시 호출
frameAlloc.freeToMarker(marker); // 한 번에 해제
}

STL 컨테이너에 커스텀 할당기를 적용합니다.

#include <memory>
#include <vector>
template <typename T>
class PoolAllocator {
public:
using value_type = T;
PoolAllocator() = default;
template <typename U>
PoolAllocator(const PoolAllocator<U>&) noexcept {}
T* allocate(std::size_t n) {
// 풀에서 n * sizeof(T) 바이트 할당
return static_cast<T*>(globalPool.allocate(n * sizeof(T), alignof(T)));
}
void deallocate(T* ptr, std::size_t n) noexcept {
globalPool.deallocate(ptr, n * sizeof(T));
}
bool operator==(const PoolAllocator&) const noexcept { return true; }
bool operator!=(const PoolAllocator&) const noexcept { return false; }
private:
// 실제 구현에서는 스레드 로컬 또는 전역 풀 참조
static StackAllocator globalPool;
};
// 사용
std::vector<int, PoolAllocator<int>> poolVec;
poolVec.push_back(42);

// 함정 1: delete 사용
T* obj = pool.allocate();
delete obj; // 절대 안 됨! 풀 메모리를 시스템에 반환하려 함
// 올바름
obj->~T();
pool.deallocate(obj);
// 함정 2: 소멸자 미호출
pool.deallocate(obj); // 소멸자 없이 반환 → 리소스 누수
// 올바름
obj->~T(); // 먼저 소멸자
pool.deallocate(obj); // 그 다음 반환
// 함정 3: 정렬 무시
char buf[sizeof(T)]; // 정렬 보장 없음
new (buf) T(); // UB 가능
// 올바름
alignas(T) char buf[sizeof(T)];
new (buf) T();

  • new (ptr) T(args) → 지정 주소에 생성
  • 소멸은 ptr->~T() 명시 호출 (delete 금지)
  • 정렬은 alignas(T) 또는 std::aligned_alloc으로 보장
  • Fixed-size pool: O(1) 할당/해제, 게임 오브젝트에 적합
  • Stack allocator: 프레임/레벨 단위 일괄 해제에 적합
  • STL 통합: Allocator 콘셉트 구현으로 vector, map 등에 적용