(KO) CUBRID Private 할당기 — 스레드별 Lea 힙, C++ STL 할당기 래퍼, 빌드 모드별 라우팅
목차
학술적 배경
섹션 제목: “학술적 배경”범용 malloc은 쿼리 엔진이 수백 개의 워커 스레드에서 초당 수백
만 건의 작은 할당을 돌릴 때 경합점이 된다. 비용은 두 갈래로
나뉜다. 첫째, 전역 free-list 한 개가 모든 malloc/free를 한
뮤텍스 뒤에 줄세운다(요즘 할당기는 사이즈 클래스별 빠른 경로로
이를 부드럽게 만들지만, 느린 경로에서는 결국 캐시 라인을 공유
한다). 둘째, 할당이 스레드를 가로지른다. 워커 A가 잡은 블록을
워커 B가 free하면, 그 블록은 할당기가 인식한 스레드 로컬
캐시로 돌아가지 워커 A의 캐시로 돌아가지 않는다. 결과적으로
힙이 사이즈 클래스 조각으로 흩어지고, 어느 스레드도 그 조각을
깔끔하게 재사용하지 못한다. 교과서적인 답은 tcmalloc과
jemalloc이 대중화한 스레드별 아레나다. 워커마다 자기
부기 풀을 하나씩 두고, 할당과 해제는 그 풀 안에서만 일어나며,
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 위
한 단계, 힙을 쓰는 서브시스템 아래 한 단계에 끼어 있는 모양새
다. 아레나 계층이 customheaps의 hl_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_alloc을 cubmem::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 태그가 답하지 않는다.
CUBRID의 구현
섹션 제목: “CUBRID의 구현”THREAD_ENTRY는 HL_HEAPID private_heap_id 한 개를 든다. 힙
자체는 customheaps가 관리하는 Lea 힙이고(Doug Lea의
dlmalloc을 external/ 아래 자체 사본으로 가져온 것이다).
서버 스레드가 시작될 때 부트 시퀀스가 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_heap이 hl_unregister_lea_heap
으로 힙을 통째로 떨군다. 요청 경계마다 힙을 재사용하고
싶으면 db_clear_private_heap이 힙을 드롭하지 않고 비우기만
한다(재등록 비용을 안 내고 깨끗한 상태로 돌아오는 길).
// db_create_private_heap — src/base/memory_alloc.cHL_HEAPIDdb_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.hpptemplate <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>::pointerprivate_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.cppif (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_ALLOCATOR는 cubmem::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.hppHL_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.hppvoid *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_allocator이
thread_p->count_private_allocators를 증감하는 식이다. 릴리스
빌드에서는 둘 다 no-op으로 컴파일된다.
// cubmem::register_private_allocator — src/base/memory_private_allocator.cppvoidregister_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_debug를
cuberr::resource_tracker와도 묶어 둔다. 트래커는 할당마다
(file, line, ptr)을 적어 두고 스레드 종료 시 카운트가 안
맞으면 경고를 띄운다. 둘을 묶는 자리가 memory_alloc.h의
_debug 매크로 군이다.
#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)#endifrc_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 자리.
C 쪽 할당 API
섹션 제목: “C 쪽 할당 API”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_free와db_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인자가 호출 단위 참여 여부를 결정한다.
C++ STL 할당기 래퍼
섹션 제목: “C++ STL 할당기 래퍼”cubmem::private_allocator<T>(memory_private_allocator.hpp) — STL 할당기 컨셉. 생성 시점에(thread_p, heap_id)쌍을 잡고allocate/deallocate를private_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불린이 둘 중 하나를 고른다.
SA_MODE의 in-band 태그
섹션 제목: “SA_MODE의 in-band 태그”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) — 헤더를 건너 뛰는 포인터 산술 매크로.
소스 검증 (2026-05-07 기준)
섹션 제목: “소스 검증 (2026-05-07 기준)”| 심볼 | 파일 | 라인 |
|---|---|---|
db_create_private_heap | src/base/memory_alloc.c | ~295 |
db_clear_private_heap | src/base/memory_alloc.c | ~316 |
db_change_private_heap | src/base/memory_alloc.c | ~338 |
db_replace_private_heap | src/base/memory_alloc.c | ~360 |
db_destroy_private_heap | src/base/memory_alloc.c | ~390 |
db_private_alloc_release | src/base/memory_alloc.c | ~437 |
db_private_realloc_release | src/base/memory_alloc.c | ~571 |
db_private_free_release | src/base/memory_alloc.c | ~780 |
db_private_strdup | src/base/memory_alloc.c | ~697 |
db_private_get_heapid_from_thread | src/base/memory_alloc.c | ~1002 |
db_private_set_heapid_to_thread | src/base/memory_alloc.c | ~1020 |
cubmem::private_allocator<T> (선언) | src/base/memory_private_allocator.hpp | ~57 |
cubmem::private_allocator<T>::allocate | src/base/memory_private_allocator.hpp | ~225 |
cubmem::switch_to_global_allocator_and_call | src/base/memory_private_allocator.hpp | ~349 |
cubmem::PRIVATE_BLOCK_ALLOCATOR | src/base/memory_private_allocator.cpp | ~76 |
cubmem::get_private_heapid | src/base/memory_private_allocator.cpp | ~82 |
cubmem::private_heap_allocate | src/base/memory_private_allocator.cpp | ~96 |
cubmem::private_heap_deallocate | src/base/memory_private_allocator.cpp | ~118 |
cubmem::register_private_allocator | src/base/memory_private_allocator.cpp | ~138 |
cubmem::fixed_size_alloc::allocator<T, true>::expand | src/base/fixed_size_allocator.hpp | ~191 |
PRIVATE_MALLOC_HEADER | src/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_alloc이db_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 Leadlmalloc을 자체 사본으로 가져온 코드다.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_allocate와private_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_allocvsdb_private_alloc_external)는 무엇인가. 유일한 차이가 리소스 트래커 참여 여부 (rc_track = falsefor external,truefor 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.cpp나memory_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.hpp—THREAD_ENTRY::private_heap_id자리.src/base/resource_tracker.hpp— 디버그 빌드의 누수 트래커.src/object/work_space.c—db_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.md—cubthread::entry가private_heap_id를 호스팅한다.cubrid-thread-manager-ng.md— 워커별 컨텍스트 freelist 가 STL 기반 캐시에private_allocator<T>를 쓴다.cubrid-overview-base-infra.md— 섹션 개요.cubrid-design-philosophy.md—tcmalloc/jemalloc대신 자체dlmalloc사본을 고른 사연.