콘텐츠로 이동

(KO) CUBRID Private 할당기 — 스레드별 Lea 힙, C++ STL 할당기 래퍼, 빌드 모드별 라우팅

목차

범용 malloc은 쿼리 엔진이 수백 개의 워커 스레드에서 초당 수백 만 건의 작은 할당을 돌릴 때 경합점이 된다. 비용은 두 갈래로 나뉜다. 첫째, 전역 free-list 한 개가 모든 malloc/free를 한 뮤텍스 뒤에 줄세운다(요즘 할당기는 사이즈 클래스별 빠른 경로로 이를 부드럽게 만들지만, 느린 경로에서는 결국 캐시 라인을 공유 한다). 둘째, 할당이 스레드를 가로지른다. 워커 A가 잡은 블록을 워커 B가 free하면, 그 블록은 할당기가 인식한 스레드 로컬 캐시로 돌아가지 워커 A의 캐시로 돌아가지 않는다. 결과적으로 힙이 사이즈 클래스 조각으로 흩어지고, 어느 스레드도 그 조각을 깔끔하게 재사용하지 못한다. 교과서적인 답은 tcmallocjemalloc이 대중화한 스레드별 아레나다. 워커마다 자기 부기 풀을 하나씩 두고, 할당과 해제는 그 풀 안에서만 일어나며, free된 영역은 자기를 만든 아레나에 머무른다. 학술적으로 자주 인용되는 첫 사례는 Berger 외의 Hoard (PLDI 2000)이고, tcmalloc (Ghemawat & Menage, 2007)과 jemalloc (Evans, 2006) 이 산업화된 형태다.

CUBRID는 두 라이브러리보다 앞선 시기에 만들어졌으니 답도 더 오래되고 단순하다. Doug Lea의 dlmalloc 을 자체 내부 할당기로 (customheaps) 패키징해 들여왔고 — 1996년 이래 GNU libc의 기본값 자리를 지킨 free-list 기반 fast-bin 할당기다. 워커 스레드마다 Lea 힙을 한 개씩 인스턴스화한 다음, “엔진 내부 인데 트랜잭션 단위로 살아 있는” 모든 할당을 그 힙으로 보낸다. 결과는 고전 알고리즘으로 짠 스레드별 아레나가 OS malloc 위 한 단계, 힙을 쓰는 서브시스템 아래 한 단계에 끼어 있는 모양새 다. 아레나 계층이 customheapshl_register_lea_heap / hl_lea_alloc / hl_lea_free 식의 함수 군이고, 스레드별 상태가 THREAD_ENTRY::private_heap_id이며, 공개 API가 db_private_alloc / db_private_free / db_private_realloc 매크로 군이다.

두 번째 축은 경합과 직교한 이야기 — C++ 쪽이다. STL 컨테이너 (std::vector, std::map, std::list 등)는 allocator 템플릿 매개변수를 받고, 그 매개변수는 정해진 컨셉을 만족해야 한다(Stepanov가 SGI STL에 도입했고 C++03 §20.1.5에 명문화된 뒤 C++11에서 다소 완화된 규칙이다). CUBRID는 C 쪽 코드가 쓰는 스레드별 힙을 STL 컨테이너도 똑같이 쓰게 만들고 싶어서 C API db_private_alloccubmem::private_allocator<T>로 감쌌다. 생성 시점에 (thread_p, heap_id) 쌍을 잡아 두는 타입이고, STL 컨셉을 만족하므로 std::vector<int, private_allocator<int>>가 자연히 스레드별 힙으로 빠진다. private_unique_ptr<T>은 unique_ptr 풍의 래퍼고, PRIVATE_BLOCK_ALLOCATOR는 가변 길이 바이트 버퍼로 스트리밍하는 코드를 위한 cubmem::block_allocator 풍의 래퍼다.

세 번째 축은 빌드 모드 분기다 (cubrid-sa-cs-runtime.md). 같은 소스가 세 가지로 컴파일된다.

  • cub_server (SERVER_MODE) — 엔진이 데몬으로 돌고, 모든 요청이 워커 스레드 위에서 처리되니 스레드별 Lea 힙이 자연 스러운 자리.
  • libcubridcs (CS_MODE) — 네트워크로 갈라진 클라이언트 쪽이라 서버측 힙이라는 게 없다. 할당은 클라이언트 워크스 페이스(db_ws_alloc)로 간다.
  • libcubridsa (SA_MODE) — 엔진이 단일 스레드 관리 유틸 안에 링크되어 들어간 형태. 서버 로직이 활성화돼 있으면 전역 Lea 힙(private_heap_id)으로, 그렇지 않으면 워크 스페이스로 간다.

db_private_alloc은 이 세 모드 위로 한 겹 덮은 단일 정문이다. SA_MODE에서 모드를 갈라 주는 표시자가 블록마다 8바이트짜리 헤더(PRIVATE_MALLOC_HEADER)다. 블록이 Lea 힙에서 왔는지 워크스페이스에서 왔는지를 그 안에 적어 두므로, 호출자가 어디 로 갔는지 모르는 상태로 db_private_free만 부르더라도 free가 올바른 경로로 돌아간다. SERVER_MODE에는 이 헤더가 없다. free 시점에 어느 힙으로 돌려보낼지는 그 스레드의 현재 private_ heap_id가 답해 주지, in-band 태그가 답하지 않는다.

THREAD_ENTRYHL_HEAPID private_heap_id 한 개를 든다. 힙 자체는 customheaps가 관리하는 Lea 힙이고(Doug Lea의 dlmallocexternal/ 아래 자체 사본으로 가져온 것이다). 서버 스레드가 시작될 때 부트 시퀀스가 db_create_private_heap() 을 부르면 그 안에서 hl_register_lea_heap()이 돌아 새 힙 id를 받아오고, 그 id를 스레드 엔트리에 박아 둔다. 그 시점부터 모든 db_private_alloc(thread_p, size)은 스레드 엔트리의 private_heap_id를 읽어 hl_lea_alloc으로 보낸다. 스레드가 끝날 때 db_destroy_private_heaphl_unregister_lea_heap 으로 힙을 통째로 떨군다. 요청 경계마다 힙을 재사용하고 싶으면 db_clear_private_heap이 힙을 드롭하지 않고 비우기만 한다(재등록 비용을 안 내고 깨끗한 상태로 돌아오는 길).

// db_create_private_heap — src/base/memory_alloc.c
HL_HEAPID
db_create_private_heap (void)
{
HL_HEAPID heap_id = 0;
#if defined (SERVER_MODE)
heap_id = hl_register_lea_heap ();
#else /* SERVER_MODE */
if (db_on_server)
{
heap_id = hl_register_lea_heap ();
}
#endif /* SERVER_MODE */
return heap_id;
}

힙 id를 일시적으로 갈아 끼우는 두 보조 함수가 있다. db_change_private_heap은 스레드의 힙 id를 새 값으로 바꾸고 이전 값을 돌려준다. 어느 코드 묶음의 할당을 통째로 한 private 힙 안으로 격리하고 싶을 때 쓰는데, 예를 들어 파서 는 PARSER_CONTEXT마다 힙을 따로 두고 세션이 끝날 때 db_destroy_private_heap 한 번으로 트리 전체를 한꺼번에 떨군다. db_replace_private_heap은 새 힙을 한 개 할당해 스레드의 힙 자리에 꽂고 이전 힙 id를 돌려주므로, 호출자가 이전 힙을 알아서 db_destroy_private_heap해 주면 된다.

C 쪽 할당의 정문은 db_private_alloc이다. SERVER_MODE에서는 호출 스레드의 private_heap_id를 읽어 hl_lea_alloc으로 보내고, 힙 id가 0이면 OS malloc으로 fallback한다(스레드 초기 화가 아직 끝나지 않은 상태에서는 OS 할당기로 떨어뜨리고, 누군 가 나중에 free해 주리라 믿는 모양새다).

// db_private_alloc_release — src/base/memory_alloc.c (SERVER_MODE 분기)
heap_id = db_private_get_heapid_from_thread (thrd);
if (heap_id)
{
ptr = hl_lea_alloc (heap_id, size);
}
else
{
ptr = malloc (size);
if (ptr == NULL)
{
er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE,
ER_OUT_OF_VIRTUAL_MEMORY, 1, size);
}
}

CS_MODE에서는 같은 호출이 클라이언트 워크스페이스 (db_ws_alloc)로 redirect된다. SA_MODE에서는 블록을 PRIVATE_MALLOC_HEADER로 감싸서, 나중에 db_private_free가 어느 경로로 돌려보내야 할지 알아낼 수 있게 한다.

// db_private_alloc_release — src/base/memory_alloc.c (SA_MODE 분기, 축약)
if (private_heap_id)
{
PRIVATE_MALLOC_HEADER *h;
size_t req_sz = private_request_size (size);
h = (PRIVATE_MALLOC_HEADER *) hl_lea_alloc (private_heap_id, req_sz);
if (h != NULL)
{
h->magic = PRIVATE_MALLOC_HEADER_MAGIC;
h->alloc_type = PRIVATE_ALLOC_TYPE_LEA;
return private_hl2user_ptr (h);
}
return NULL;
}
else
{
return malloc (size);
}

헤더는 8바이트, free 시점에 매직을 검사하고, 타입은 PRIVATE_ ALLOC_TYPE_LEA(Lea 힙에서 왔다)와 PRIVATE_ALLOC_TYPE_WS (워크스페이스에서 왔다) 중 하나다. free 쪽은 매직을 보고 asserting하며 타입에 따라 갈라진다.

// db_private_free_release — src/base/memory_alloc.c (SA_MODE 분기, 축약)
PRIVATE_MALLOC_HEADER *h = private_user2hl_ptr (ptr);
if (h->magic != PRIVATE_MALLOC_HEADER_MAGIC)
{
assert (false);
return;
}
if (h->alloc_type == PRIVATE_ALLOC_TYPE_LEA)
{
hl_lea_free (private_heap_id, h);
}
else if (h->alloc_type == PRIVATE_ALLOC_TYPE_WS)
{
db_ws_free (ptr);
}

세 빌드 모드를 가로지르는 디스패치 그래프는 다음과 같다.

flowchart LR
  CALL["db_private_alloc(thread_p, size)"]

  subgraph SVR["SERVER_MODE"]
    SVR_HID["thread_p->private_heap_id"]
    SVR_NZ{"id != 0?"}
    SVR_LEA["hl_lea_alloc(id, size)"]
    SVR_MAL["malloc(size)"]
  end

  subgraph CSM["CS_MODE"]
    CS_WS["db_ws_alloc(size)"]
  end

  subgraph SAM["SA_MODE"]
    SA_ON{"db_on_server?"}
    SA_WS["db_ws_alloc(size)"]
    SA_HID{"private_heap_id != 0?"}
    SA_HDR["PRIVATE_MALLOC_HEADER로<br/>감싸기 (type = LEA)"]
    SA_LEA["hl_lea_alloc"]
    SA_MAL["malloc(size)"]
  end

  CALL --> SVR
  CALL --> CSM
  CALL --> SAM

  SVR --> SVR_HID --> SVR_NZ
  SVR_NZ -- yes --> SVR_LEA
  SVR_NZ -- no  --> SVR_MAL

  CSM --> CS_WS

  SAM --> SA_ON
  SA_ON -- no  --> SA_WS
  SA_ON -- yes --> SA_HID
  SA_HID -- yes --> SA_HDR --> SA_LEA
  SA_HID -- no  --> SA_MAL

C++ 래퍼 cubmem::private_allocator<T>는 자체 로직이 거의 없다. 생성 시점에 (thread_p, heap_id) 쌍을 잡아 두고, get_private_heapid로 NULL 스레드를 cubthread::get_entry() 로 풀어 주며, allocate(count)는 그대로 private_heap_allocate(thread_p, heap_id, count * sizeof(T)) 로 넘어간다.

// cubmem::private_allocator<T> — src/base/memory_private_allocator.hpp
template <typename T>
private_allocator<T>::private_allocator (cubthread::entry *thread_p)
: m_thread_p (thread_p)
{
m_heapid = get_private_heapid (m_thread_p);
register_private_allocator (m_thread_p);
}
template <typename T>
typename private_allocator<T>::pointer
private_allocator<T>::allocate (size_type count)
{
return reinterpret_cast<T *>
(private_heap_allocate (m_thread_p, m_heapid, count * sizeof (T)));
}

상태는 그 두 포인터뿐이다. operator==이 항상 true를 돌려주니 STL 컨테이너는 같은 타입의 private_allocator<T> 인스턴스 둘을 호환되는 것으로 본다. STL 할당기 컨셉의 인스턴스 호환 요구와도, SERVER_MODE에서 같은 스레드의 private_allocator<T> 인스턴스가 항상 같은 힙으로 떨어진다는 실제 동작과도 맞는다. 스레드를 가로지르는 경우(스레드 A에서 만들어 스레드 B에서 free)는 런타임이 명시적으로 assert로 방어하는 코너다.

// cubmem::private_heap_deallocate — src/base/memory_private_allocator.cpp
if (heapid != thread_p->private_heap_id)
{
/* this is not something we should do! */
assert (false);
HL_HEAPID save_heapid =
db_private_set_heapid_to_thread (thread_p, heapid);
db_private_free (thread_p, ptr);
(void) db_private_set_heapid_to_thread (thread_p, save_heapid);
}
else
{
db_private_free (thread_p, ptr);
}

fallback 경로(힙을 임시로 갈아 끼우고 free한 뒤 원래대로 돌려놓기)는 잘못된 스레드에서도 호출이 일단 성공하게는 해주 지만, assert (false)가 이건 호출자 버그다라는 신호다.

cubmem::private_unique_ptr<T>std::unique_ptr<T, private_pointer_deleter<T>>을 얇게 감싼 형태다. deleter가 db_private_free(thread_p, ptr)을 부르므로, private 할당기 로 잡은 포인터를 들고 있을 때 올바른 힙으로 풀어 주는 일을 잊지 않을 수 있는 표준 도구다.

PRIVATE_BLOCK_ALLOCATORcubmem::block 추상화 (mem_block.hpp) 용 cubmem::block_allocator다. db_private_alloc / _realloc / _free을 감싸 둔 형태라, mem_block 기반 컨테이너(예: extensible_array, packing_packer의 스트리밍 버퍼)를 쓰는 코드가 C++ 할당기 래퍼를 직접 만들지 않고도 같은 스레드 라우팅을 가져갈 수 있다.

switch_to_global_allocator_and_call(func, args...)은 비상구다.

// cubmem::switch_to_global_allocator_and_call — src/base/memory_private_allocator.hpp
HL_HEAPID save_id = db_change_private_heap (NULL, 0);
func (std::forward<Args> (args)...);
(void) db_change_private_heap (NULL, save_id);

db_change_private_heap(NULL, 0)으로 스레드의 힙을 비활성화 하고(이 동안의 할당은 OS malloc으로 떨어진다), func을 실행한 뒤, 원래 힙 id를 복원한다. 스레드의 힙이 절대 풀어 주지 않을 무언가를 잡아야 할 때 — 예를 들어 프로세스 전역 심볼 테이블에 등록할 문자열, 만든 스레드보다 오래 사는 OS 핸들 — 이 비상구를 쓴다.

fixed_size_allocator<T, /*is_private=*/true> (fixed_size_allocator.hpp)는 한 단계 위에 있다. 256개짜리 block<T> (각 노드가 sizeof(T)만큼 정렬된 슬롯) 단위로 고정 크기 셀을 쪼개 주고, 블록은 private_allocator<block<T>>으로 잡으며, 비어 있는 셀은 단일 연결 리스트로 엮어 둔다.

// cubmem::fixed_size_alloc::allocator<T, true>::expand — src/base/fixed_size_allocator.hpp
void *raw_mem = m_allocator.allocate (1);
auto deleter = [alloc = &m_allocator] (block<T> *ptr)
{
ptr->~block();
alloc->deallocate (ptr);
};
m_blocks.push_back (
std::shared_ptr<block<T>> (new (raw_mem) block<T>(), deleter));
for (node<T> &node : m_blocks.back()->nodes)
{
/* 새 블록의 노드들을 free 리스트에 엮는다 */
}

형태로 보면 cubrid-common-area.md의 AREA를 C++ 타입 매개변수 화한 미니 버전이다. AREA의 하드코딩된 타입 목록에 들어 있지 않은 자료형용 풀이 필요하면, 락프리 freelist나 hashmap이 이걸 가져다 쓴다.

디버그 빌드는 스레드별 누수 카운터를 한 겹 더 단다. register_private_allocator / deregister_private_allocatorthread_p->count_private_allocators를 증감하는 식이다. 릴리스 빌드에서는 둘 다 no-op으로 컴파일된다.

// cubmem::register_private_allocator — src/base/memory_private_allocator.cpp
void
register_private_allocator (cubthread::entry *thread_p)
{
#if defined (SERVER_MODE) && !defined (NDEBUG)
thread_p->count_private_allocators++;
#else
(void) thread_p;
#endif
}

디버그 빌드는 C 쪽 db_private_alloc_debugcuberr::resource_tracker와도 묶어 둔다. 트래커는 할당마다 (file, line, ptr)을 적어 두고 스레드 종료 시 카운트가 안 맞으면 경고를 띄운다. 둘을 묶는 자리가 memory_alloc.h_debug 매크로 군이다.

src/base/memory_alloc.h
#if !defined(NDEBUG)
#define db_private_alloc(thrd, size) \
db_private_alloc_debug(thrd, size, true, __FILE__, __LINE__)
#else
#define db_private_alloc(thrd, size) \
db_private_alloc_release(thrd, size, false)
#endif

rc_track 인자가 트래커를 켜는 스위치이고, _external 변형은 트래커에 참여하지 않으면서 private 힙으로 잡고 싶은 호출자가 쓴다(엔진이 의도적으로 호출자에게 돌려주는 블록을 트래커가 누수로 오해해서 경고를 내는 일을 막기 위해서).

  • db_create_private_heap (memory_alloc.c) — hl_register_lea_heap로 Lea 힙을 등록하고 id를 돌려준다.
  • db_destroy_private_heap (memory_alloc.c) — 힙 등록을 해제한다. heap_id == 0을 넘기면 이 스레드의 현재 힙이라 는 뜻이다.
  • db_clear_private_heap (memory_alloc.c) — hl_clear_lea_heap로 힙을 비우되 드롭하지는 않는다.
  • db_change_private_heap (memory_alloc.c) — 스레드의 힙 id를 갈고 이전 값을 돌려준다.
  • db_replace_private_heap (memory_alloc.c) — 새 힙을 만들 어 스레드의 힙 자리에 꽂고 이전 id를 돌려준다.
  • db_private_get_heapid_from_thread (memory_alloc.c, static, SERVER_MODE 한정) — 액세서.
  • db_private_set_heapid_to_thread (memory_alloc.c, SERVER_MODE 한정) — 세터, 이전 값을 돌려준다.
  • THREAD_ENTRY::private_heap_id (thread_entry.hpp) — 스레드별 힙 id 자리.
  • db_private_alloc 매크로 (memory_alloc.h) — NDEBUG 여부 에 따라 _release 또는 _debug로 디스패치.
  • db_private_alloc_release / db_private_alloc_debug (memory_alloc.c) — 빌드 모드 별 분기: CS_MODE는 db_ws_alloc으로 위임. SERVER_MODE는 스레드의 힙 id를 읽어 hl_lea_alloc을 부르고, 힙 id가 0이면 malloc으로 fallback. SA_MODE는 요청을 PRIVATE_MALLOC_HEADER로 감싸 LEA 키로 태그한 뒤 전역 private_heap_id로 보내며, db_on_server가 false거나 전역 힙이 없으면 malloc으로 fallback.
  • db_private_realloc_release / _debug (memory_alloc.c) 같은 디스패치를 hl_lea_realloc / db_ws_realloc 위에 놓는다. SA_MODE는 블록의 기존 alloc_type으로 다시 갈래를 나눈다.
  • db_private_free_release / _debug (memory_alloc.c) — SA_MODE는 PRIVATE_MALLOC_HEADER를 읽어 hl_lea_freedb_ws_free 사이에서 갈래를 결정한다. 매직이 안 맞으면 assert (false).
  • db_private_strdup / db_private_strndup (memory_alloc.c) — 편의 래퍼.
  • db_private_alloc_external / db_private_free_external / db_private_realloc_external (memory_alloc.c) — 트래커 에 참여하지 않는 변형. 공개 API 표면에서 쓰는 호출자용.

리소스 트래커 연동 (디버그 한정)

섹션 제목: “리소스 트래커 연동 (디버그 한정)”
  • cuberr::resource_tracker (resource_tracker.hpp) — 스레드별 (file, line, ptr) 로그. 할당 시 increment, free 시 decrement, 스레드 종료 시 누수 보고.
  • 모든 _debug 변형으로 흘러가는 rc_track 인자가 호출 단위 참여 여부를 결정한다.
  • cubmem::private_allocator<T> (memory_private_allocator.hpp) — STL 할당기 컨셉. 생성 시점에 (thread_p, heap_id) 쌍을 잡고 allocate / deallocateprivate_heap_allocate / _deallocate로 forward.
  • cubmem::private_unique_ptr<T> / cubmem::private_pointer_deleter<T> (memory_private_allocator.hpp) — unique_ptr 풍 래퍼.
  • cubmem::PRIVATE_BLOCK_ALLOCATOR (memory_private_allocator.cpp) — cubmem::block 기반 컨테이너용 block_allocator. realloc 성장 시 블록을 두 배로 늘린다.
  • cubmem::get_private_heapid (memory_private_allocator.cpp) — NULL 스레드를 cubthread::get_entry()로 풀어 준다. SA_MODE는 0을 돌려준다.
  • cubmem::private_heap_allocate / _deallocate (memory_private_allocator.cpp) — cross-heap-id assert
    • fallback 경로.
  • cubmem::register_private_allocator / cubmem::deregister_private_allocator (memory_private_allocator.cpp) — 디버그 한정 count_private_allocators 증감.
  • cubmem::switch_to_global_allocator_and_call (memory_private_allocator.hpp) — 스레드의 힙을 일시 비활성화하는 비상구.

Private 할당기 위에 올린 타입드 슬랩

섹션 제목: “Private 할당기 위에 올린 타입드 슬랩”
  • cubmem::fixed_size_alloc::allocator<T, true> (fixed_size_allocator.hpp) — block<T> 노드 단위로 타입드 슬랩, 블록은 private_allocator<block<T>>에서 잡고, free 리스트는 node<T>::m_next로 엮인다.
  • cubmem::fixed_size_alloc::allocator<T, false> (fixed_size_allocator.hpp) — 같은 슬랩을 std::unique_ptr<block<T>> (즉 평범한 new / delete) 위에 얹은 형태. is_private 불린이 둘 중 하나를 고른다.
  • PRIVATE_MALLOC_HEADER (memory_alloc.h) — 매직 + alloc_type 의 8바이트 헤더.
  • PRIVATE_ALLOC_TYPE_LEA / PRIVATE_ALLOC_TYPE_WS (memory_alloc.h) — alloc_type enum.
  • private_request_size / private_hl2user_ptr / private_user2hl_ptr (memory_alloc.h) — 헤더를 건너 뛰는 포인터 산술 매크로.
심볼파일라인
db_create_private_heapsrc/base/memory_alloc.c~295
db_clear_private_heapsrc/base/memory_alloc.c~316
db_change_private_heapsrc/base/memory_alloc.c~338
db_replace_private_heapsrc/base/memory_alloc.c~360
db_destroy_private_heapsrc/base/memory_alloc.c~390
db_private_alloc_releasesrc/base/memory_alloc.c~437
db_private_realloc_releasesrc/base/memory_alloc.c~571
db_private_free_releasesrc/base/memory_alloc.c~780
db_private_strdupsrc/base/memory_alloc.c~697
db_private_get_heapid_from_threadsrc/base/memory_alloc.c~1002
db_private_set_heapid_to_threadsrc/base/memory_alloc.c~1020
cubmem::private_allocator<T> (선언)src/base/memory_private_allocator.hpp~57
cubmem::private_allocator<T>::allocatesrc/base/memory_private_allocator.hpp~225
cubmem::switch_to_global_allocator_and_callsrc/base/memory_private_allocator.hpp~349
cubmem::PRIVATE_BLOCK_ALLOCATORsrc/base/memory_private_allocator.cpp~76
cubmem::get_private_heapidsrc/base/memory_private_allocator.cpp~82
cubmem::private_heap_allocatesrc/base/memory_private_allocator.cpp~96
cubmem::private_heap_deallocatesrc/base/memory_private_allocator.cpp~118
cubmem::register_private_allocatorsrc/base/memory_private_allocator.cpp~138
cubmem::fixed_size_alloc::allocator<T, true>::expandsrc/base/fixed_size_allocator.hpp~191
PRIVATE_MALLOC_HEADERsrc/base/memory_alloc.h~301

라인 번호는 이 리비전에 한정된 힌트다. 파일이 옮겨지면 심볼 이름으로 재검색하라.

  • AREA와 private 할당기의 분리. cubrid-common-area.md (AREA)는 고정 크기 객체용이며 객체 수명이 여러 요청에 걸쳐 있다. private 할당기는 가변 크기 할당용이며 수명이 요청 또는 명시적 db_clear_private_heap 사이클로 닫힌다. 둘이 공존하지만 AREA의 셀 자체는 db_private_alloc을 거치 지 않는다. 다만 AREA의 블록 단위 부기 배열은 db_private_ alloc으로 잡는다.
  • 클라이언트 쪽의 워크스페이스 vs private 할당기. CS_MODE 에서는 모든 db_private_allocdb_ws_alloc (work_space.c)로 redirect된다. 워크스페이스는 OID 키 기반 객체 워크스페이스다. private 할당기라는 이름은 서버 쪽 추상이고, 클라이언트 쪽에서는 같은 호출이 워크스페이스의 할당기로 떨어진다. 그래서 dbi-cci의 헤더 전용 코드가 어느 쪽에 자리 잡든 관계없이 db_private_alloc을 그대로 쓸 수 있다.
  • SA_MODE의 헤더 태깅. SA_MODE는 모든 블록에 8바이트짜리 PRIVATE_MALLOC_HEADER를 붙여, 그 블록이 LEA 힙에서 왔는지 워크스페이스에서 왔는지를 기억해 둔다. SERVER_MODE는 그렇게 하지 않는다. free 시점의 라우팅은 스레드의 현재 private_heap_id가 정해 주지 in-band 태그가 정하지 않는다. 이 차이가 memory_alloc.h#if defined (SA_MODE) 블록 에 명시적으로 적혀 있고, db_private_alloc_release / db_private_realloc_release / db_private_free_release의 SA_MODE 분기에서 실제로 실행된다.
  • Lea 힙은 dlmalloc이다. customheaps는 1990년대 후반의 Doug Lea dlmalloc을 자체 사본으로 가져온 코드다. hl_lea_alloc / _free / _realloc이 그 위에 얹힌 함수 군이 다. 사본의 본체는 src/base/customheaps.{c,h}external/에 들어 있다. 이 선택은 tcmalloc / jemalloc보다 앞선다. 사연은 cubrid-design-philosophy.md에 정리해 두었다.
  • assert(false) on cross-heap free. private_heap_allocateprivate_heap_deallocate는 요청 된 힙이 스레드의 현재 힙과 다르면 assert를 건다. 힙 id를 잠깐 갈아 끼웠다가 호출 후 복원하는 fallback 경로는 switch_to_global_allocator_and_call 같은 정당한 cross- heap 사용을 위해 남겨둔 것이고, 그 외 자리에서 트리거되면 코드 스멜로 본다.
  • private_allocator<T>::operator==이 무조건 true인 이유. STL 할당기 컨셉의 인스턴스 호환 요구와도, SERVER_MODE 에서 같은 스레드의 private_allocator<T> 인스턴스가 항상 같은 힙으로 떨어진다는 사실과도 모두 들어맞는다. 다른 스레드에서 만들어진 두 인스턴스가 같다고 보고되는데, 컨테이너가 스레드를 옮겨 다닌다면 버그가 되겠지만, 실제로 CUBRID의 STL 컨테이너는 수명 동안 한 스레드에 묶여 있다.
  • API가 두 줄인 이유 (db_private_alloc vs db_private_alloc_external)는 무엇인가. 유일한 차이가 리소스 트래커 참여 여부 (rc_track = false for external, true for internal) 하나뿐이다. 엔진 외부로 노출되는 헤더용 빌드 시점 선택일 가능성이 가장 크지만, 소스에는 명시되어 있지 않다.
  • Windows 스텁이 NULL을 돌려주는 이유. db_private_alloc_release / _debug와 realloc / free 변형이 Windows에서는 (memory_alloc.c#if defined (WINDOWS) 분기에서) return NULL로 막혀 있다. Windows 빌드가 기능 일부가 빠진 채로 동작하는 것인지, 아니면 다른 곳에 Windows 전용 경로가 따로 사는지는 현재 소스로는 답이 안 나온다.
  • count_private_allocators의 소비자가 이 파일에 없다. register_private_allocator이 이 카운터를 올리지만, memory_private_allocator.cppmemory_alloc.c에는 스레드 종료 시 이 값을 검사하는 코드가 보이지 않는다. thread_entry의 소멸자에서 검사할 수도 있겠지만 확인되지 않은 추측이다.
  • src/base/memory_alloc.{h,c} — C 쪽 API와 스레드별 힙 상태.
  • src/base/memory_private_allocator.{hpp,cpp} — C++ STL 할당기 래퍼.
  • src/base/fixed_size_allocator.hpp — private 할당기 위에 올린 타입드 슬랩.
  • src/base/customheaps.{c,h} — Lea 힙 (dlmalloc) 래퍼.
  • src/thread/thread_entry.hppTHREAD_ENTRY::private_heap_id 자리.
  • src/base/resource_tracker.hpp — 디버그 빌드의 누수 트래커.
  • src/object/work_space.cdb_ws_alloc / db_ws_free / db_ws_realloc. 클라이언트 쪽 워크스페이스 할당기.
  • 교차 참조:
    • cubrid-common-area.md — AREA 슬랩 풀(고정 크기 객체).
    • cubrid-sa-cs-runtime.md — SA_MODE / SERVER_MODE / CS_MODE 빌드 분기.
    • cubrid-thread-worker-pool.mdcubthread::entryprivate_heap_id를 호스팅한다.
    • cubrid-thread-manager-ng.md — 워커별 컨텍스트 freelist 가 STL 기반 캐시에 private_allocator<T>를 쓴다.
    • cubrid-overview-base-infra.md — 섹션 개요.
    • cubrid-design-philosophy.mdtcmalloc / jemalloc 대신 자체 dlmalloc 사본을 고른 사연.