SQLite의 동적 메모리 할당
SQLite의 동적 메모리 할당
SQLite는 다양한 객체를 저장하고 데이터베이스 파일의 메모리 캐시를 만들고 질의 결과를 보관하기 위해 동적 메모리 할당을 사용해요. SQLite의 동적 메모리 할당 하위 시스템은 신뢰할 수 있고 예측 가능하며 강건하고 안전하며 효율적으로 만들기 위해 많은 노력이 들었어요.
본문
이 문서는 SQLite 내부의 동적 메모리 할당에 대한 개요를 제공해요. 대상 독자는 까다로운 환경에서 피크 성능을 위해 SQLite 사용을 튜닝하는 소프트웨어 엔지니어예요. SQLite를 사용하는 데 이 문서의 어떤 내용도 필수 지식은 아니에요. SQLite의 기본 설정과 구성은 대부분의 애플리케이션에서 잘 동작해요. 하지만 이 문서의 정보는 특별한 요구사항을 충족하거나 특이한 상황에서 실행하도록 SQLite를 튜닝하는 엔지니어에게 유용할 수 있어요.
1. 기능
SQLite 코어와 그 메모리 할당 하위 시스템은 다음 기능을 제공해요.
-
할당 실패에 대한 강건성. 메모리 할당이 실패하면(즉 malloc()이나 realloc()이 NULL을 반환하면) SQLite는 우아하게 복구해요. SQLite는 먼저 고정되지 않은(pin되지 않은) 캐시 페이지에서 메모리를 해제한 다음 할당 요청을 재시도해요. 그것도 실패하면 SQLite는 하던 일을 멈추고 SQLITE_NOMEM 오류 코드를 애플리케이션에 반환하거나, 요청된 메모리 없이 지내려 해요.
-
메모리 누수 없음. 애플리케이션은 자신이 할당한 객체를 파괴할 책임이 있어요. (예: 애플리케이션은 모든 prepared statement에 sqlite3_finalize()를, 모든 데이터베이스 연결에 sqlite3_close()를 사용해야 해요.) 하지만 애플리케이션이 협력하기만 하면 SQLite는 메모리를 누수하지 않아요. 이는 메모리 할당 실패나 다른 시스템 오류가 있을 때에도 마찬가지예요.
-
메모리 사용량 한도. sqlite3_soft_heap_limit64() 메커니즘은 애플리케이션이 SQLite가 밑으로 유지하려고 노력하는 메모리 사용량 한도를 설정하게 해줘요. SQLite는 소프트 한도에 가까워지면 새 메모리를 할당하기보다 캐시에서 메모리를 재사용하려고 해요.
-
Zero-malloc 옵션. 애플리케이션은 선택적으로 시작 시 SQLite에 몇 개의 대량 메모리 버퍼를 제공할 수 있고, 그러면 SQLite는 모든 메모리 할당 요구에 그 제공된 버퍼를 사용하고 시스템 malloc()이나 free()를 절대 호출하지 않아요.
-
애플리케이션 제공 메모리 할당자. 애플리케이션은 시작 시 SQLite에 대체 메모리 할당자에 대한 포인터를 제공할 수 있어요. 대체 메모리 할당자는 시스템 malloc()과 free() 대신 사용돼요.
-
붕괴와 단편화에 대한 증명. SQLite는 아래에 상세히 설명된 특정 사용 제약 조건 하에서 메모리 할당이 절대 실패하지 않거나 힙을 단편화하지 않는 것이 보장되도록 구성할 수 있어요. 이 속성은 메모리 할당 오류가 전체 시스템 실패에 기여할 수 있는 장기 실행 고신뢰성 임베디드 시스템에 중요해요.
-
메모리 사용량 통계. 애플리케이션은 자신이 얼마나 많은 메모리를 사용하는지 볼 수 있고, 메모리 사용량이 설계 경계에 가까워지거나 초과할 때를 감지할 수 있어요.
-
메모리 디버거와 잘 어울림. SQLite의 메모리 할당은 표준 타사 메모리 디버거(dmalloc이나 valgrind 등)를 사용해 올바른 메모리 할당 동작을 검증할 수 있도록 구조화되어 있어요.
-
할당자 호출 최소화. 시스템 malloc()과 free() 구현은 많은 시스템에서 비효율적이에요. SQLite는 malloc()과 free() 사용을 최소화하여 전체 처리 시간을 줄이기 위해 노력해요.
-
개방형 접근. 플러그형 SQLite 확장이나 심지어 애플리케이션 자체도 sqlite3_malloc(), sqlite3_realloc(), sqlite3_free() 인터페이스를 통해 SQLite가 사용하는 것과 같은 기본 메모리 할당 루틴에 접근할 수 있어요.
2. 테스트
SQLite 소스 트리의 대부분은 전적으로 테스트와 검증에 전념해요. 신뢰성은 SQLite에 중요해요. 테스트 인프라의 임무 중에는 SQLite가 동적으로 할당된 메모리를 잘못 사용하지 않는지, 메모리를 누수하지 않는지, 동적 메모리 할당 실패에 올바르게 대응하는지를 보장하는 것이 있어요.
테스트 인프라는 특수하게 계측된 메모리 할당자를 사용해 SQLite가 동적으로 할당된 메모리를 잘못 사용하지 않는지 검증해요. 계측된 메모리 할당자는 SQLITE_MEMDEBUG 옵션을 사용해 컴파일 시 활성화돼요. 계측된 메모리 할당자는 기본 메모리 할당자보다 훨씬 느리므로 프로덕션에서 사용하는 것은 권장되지 않아요. 하지만 테스트 중에 활성화되면 계측된 메모리 할당자는 다음 검사를 수행해요.
-
경계 검사. 계측된 메모리 할당자는 각 메모리 할당 양쪽 끝에 센티널(sentinel) 값을 배치해 SQLite 내부의 무언가가 할당 경계 밖에 쓰는지 검증해요.
-
해제 후 메모리 사용. 각 메모리 블록이 해제될 때 모든 바이트가 무의미한 비트 패턴으로 덮어써져요. 이는 해제된 후에 어떤 메모리도 절대 사용되지 않도록 보장하는 데 도움이 돼요.
-
malloc에서 얻지 않은 메모리 해제. 계측된 메모리 할당자의 각 메모리 할당은 해제된 모든 할당이 이전 malloc에서 왔음을 검증하는 데 사용되는 센티널을 포함해요.
-
초기화되지 않은 메모리. 계측된 메모리 할당자는 각 메모리 할당을 무의미한 비트 패턴으로 초기화해 사용자가 할당 메모리의 내용에 대해 어떤 가정도 하지 않도록 돕는답니다.
계측된 메모리 할당자를 사용하든 그렇지 않든, SQLite는 현재 대출 중인 메모리 양을 계속 추적해요. SQLite를 테스트하는 데 수백 개의 테스트 스크립트가 사용돼요. 각 스크립트 끝에서 모든 객체가 파괴되고 모든 메모리가 해제되었는지 확인하기 위한 테스트가 이루어져요. 이것이 메모리 누수를 감지하는 방법이에요. 메모리 누수 감지는 테스트 빌드와 프로덕션 빌드 중 언제나 유효하다는 점에 주목하세요. 어떤 개발자가 개별 테스트 스크립트를 실행할 때마다 메모리 누수 감지가 활성화돼요. 따라서 개발 중 발생하는 메모리 누수는 빠르게 감지되고 수정돼요.
SQLite의 메모리 부족(OOM) 오류에 대한 대응은 메모리 실패를 시뮬레이션할 수 있는 특수 메모리 할당자 오버레이를 사용해 테스트돼요. 오버레이는 메모리 할당자와 나머지 SQLite 사이에 삽입되는 계층이에요. 오버레이는 대부분의 메모리 할당 요청을 기본 할당자로 그대로 전달하고 결과를 요청자에게 다시 전달해요. 하지만 오버레이는 N번째 메모리 할당이 실패하게 설정할 수 있어요. OOM 테스트를 실행하려면 먼저 첫 번째 할당 시도에서 실패하도록 오버레이를 설정해요. 그런 다음 어떤 테스트 스크립트를 실행하고 할당이 올바르게 잡혀 처리되었는지 검증해요. 그런 다음 두 번째 할당에서 실패하도록 오버레이를 설정하고 테스트를 반복해요. 전체 테스트 절차가 메모리 할당 오류를 만나지 않고 완료될 때까지 실패 지점은 한 번에 하나씩 계속 전진해요. 이 전체 테스트 시퀀스는 두 번 실행돼요. 첫 번째 통과에서는 오버레이가 N번째 할당에서만 실패하도록 설정돼요. 두 번째 통과에서는 오버레이가 N번째와 그 이후의 모든 할당에서 실패하도록 설정돼요.
메모리 누수 감지 로직은 OOM 오버레이가 사용될 때에도 계속 동작한다는 점에 주목하세요. 이는 SQLite가 메모리 할당 오류를 만날 때도 메모리를 누수하지 않음을 검증해요. 또한 OOM 오버레이는 메모리 할당 오용을 검사하는 계측된 메모리 할당자를 포함한 어떤 기본 메모리 할당자와도 동작할 수 있다는 점에 주목하세요. 이렇게 해서 OOM 오류가 다른 종류의 메모리 사용 오류를 유발하지 않음이 검증돼요.
마지막으로, 계측된 메모리 할당자와 메모리 누수 감지기가 모두 전체 SQLite 테스트 스위트에서 동작하고, TCL 테스트 스위트가 99% 이상의 문(statement) 테스트 커버리지를 제공하며, TH3 테스트 하네스가 누수 없이 100% 분기 테스트 커버리지를 제공한다는 것을 관찰할 수 있어요. 이는 동적 메모리 할당이 SQLite 내부 어디에서나 올바르게 사용되고 있다는 강력한 증거예요.
2.1. reallocarray()의 사용
reallocarray() 인터페이스는 메모리 할당 크기 계산에서 32비트 정수 산술 오버플로를 피함으로써 다음 "heartbleed" 버그를 예방하려는 노력에서 나온 OpenBSD 커뮤니티의 최근 혁신(약 2014년)이에요. reallocarray() 함수는 단위 크기와 개수 매개변수 둘 다를 가져요. 각각 X바이트 크기의 N개 요소 배열을 담기에 충분한 메모리를 할당하려면 "reallocarray(0,X,N)"을 호출해요. 이는 "malloc(XN)"을 호출하는 전통적인 방법보다 선호되는데, reallocarray()는 XN 곱셈이 오버플로되어 malloc()이 애플리케이션이 기대한 것과 다른 크기의 버퍼를 반환하는 위험을 제거하기 때문이에요.
SQLite는 reallocarray()를 사용하지 않아요. 그 이유는 reallocarray()가 SQLite에 유용하지 않기 때문이에요. SQLite는 두 정수의 단순 곱인 메모리 할당을 절대 하지 않는 것으로 밝혀졌어요. 대신 SQLite는 "X+C" 또는 "NX+C" 또는 "MNX+C" 또는 "NX+M*Y+C" 형태의 할당을 해요. reallocarray() 인터페이스는 그런 경우에 정수 오버플로를 피하는 데 도움이 되지 않아요.
그럼에도 메모리 할당 크기 계산의 정수 오버플로는 SQLite가 다루고 싶어하는 우려 사항이에요. 문제를 예방하기 위해 SQLite의 모든 내부 메모리 할당은 부호 있는 64비트 정수 크기 매개변수를 취하는 얇은 래퍼 함수를 통해 발생해요. SQLite 소스 코드는 모든 크기 계산이 64비트 부호 정수로 수행되도록 감사(audit)돼요. SQLite는 한 번에 약 2GB 이상의 메모리 할당을 거부해요. (일반적인 사용에서 SQLite는 한 번에 약 8KB 이상의 메모리를 거의 할당하지 않으므로 2GB 할당 한도는 부담이 되지 않아요.) 따라서 64비트 크기 매개변수는 오버플로 감지에 많은 여유를 제공해요. 모든 크기 계산이 64비트 부호 정수로 수행됨을 검증하는 것과 같은 감사는 계산 중 64비트 정수를 오버플로시키는 것이 불가능함도 검증해요.
SQLite에서 메모리 할당 크기 계산이 오버플로되지 않도록 보장하는 데 사용되는 코드 감사는 모든 SQLite 릴리스 전에 반복돼요.
3. 구성
SQLite의 기본 메모리 할당 설정은 대부분의 애플리케이션에 적합해요. 하지만 특이하거나 특히 엄격한 요구사항을 가진 애플리케이션은 SQLite를 자신의 필요에 더 가깝게 맞추기 위해 구성을 조정하고 싶을 수 있어요. 컴파일 시와 시작 시 구성 옵션이 모두 제공돼요.
3.1. 대체 저수준 메모리 할당자
SQLite 소스 코드는 컴파일 시, 또는 제한적으로 시작 시 선택할 수 있는 몇 가지 서로 다른 메모리 할당 모듈을 포함해요.
3.1.1. 기본 메모리 할당자
기본적으로 SQLite는 메모리 할당 요구에 표준 C 라이브러리의 malloc(), realloc(), free() 루틴을 사용해요. 이 루틴들은 기존 할당의 크기를 반환하는 "memsize()" 함수도 제공하는 얇은 래퍼로 둘러싸여 있어요. memsize() 함수는 대출 중인(outstanding) 메모리의 바이트 수를 정확히 세는 데 필요해요. memsize()는 할당이 해제될 때 대출 중 카운트에서 몇 바이트를 제거할지 결정해요. 기본 할당자는 각 malloc() 요청에 8바이트를 항상 추가로 할당하고 그 8바이트 헤더에 할당 크기를 저장해 memsize()를 구현해요.
기본 메모리 할당자는 대부분의 애플리케이션에 권장돼요. 대체 메모리 할당자를 사용해야 할 강력한 필요가 없다면 기본을 사용하세요.
3.1.2. 디버깅 메모리 할당자
SQLite가 SQLITE_MEMDEBUG 컴파일 옵션으로 컴파일되면 시스템 malloc(), realloc(), free() 주위에 다른 무거운 래퍼가 사용돼요. 무거운 래퍼는 각 할당마다 약 100바이트의 추가 공간을 할당해요. 추가 공간은 SQLite 코어에 반환되는 할당 양쪽 끝에 센티널 값을 배치하는 데 사용돼요. 할당이 해제될 때 이 센티널들이 검사되어 SQLite 코어가 어느 방향으로든 버퍼를 초과했는지 확인해요. 시스템 라이브러리가 GLIBC일 때 무거운 래퍼는 GNU backtrace() 함수도 사용해 스택을 검사하고 malloc() 호출의 조상 함수를 기록해요. SQLite 테스트 스위트를 실행할 때 무거운 래퍼는 현재 테스트 케이스의 이름도 기록해요. 마지막 두 기능은 테스트 스위트가 감지한 메모리 누수의 출처를 추적하는 데 유용해요.
SQLITE_MEMDEBUG가 설정될 때 사용되는 무거운 래퍼는 또한 각 새 할당이 호출자에게 반환되기 전에 무의미한 데이터로 채워지도록 해요. 그리고 할당이 해제되는 즉시 다시 무의미한 데이터로 채워져요. 이 두 작업은 SQLite 코어가 새로 할당된 메모리 상태에 대해 가정하지 않고, 해제된 후 메모리 할당이 사용되지 않도록 보장하는 데 도움이 돼요.
SQLITE_MEMDEBUG가 사용하는 무거운 래퍼는 SQLite의 테스트, 분석, 디버깅 중에만 사용하기 위한 것이에요. 무거운 래퍼는 상당한 성능과 메모리 오버헤드가 있으므로 프로덕션에서 사용하지 않아야 해요.
3.1.3. Win32 네이티브 메모리 할당자
SQLite가 SQLITE_WIN32_MALLOC 컴파일 옵션으로 Windows용으로 컴파일되면 HeapAlloc(), HeapReAlloc(), HeapFree() 주위에 다른 얇은 래퍼가 사용돼요. 얇은 래퍼는 구성된 SQLite 힙을 사용하며, SQLITE_WIN32_HEAP_CREATE 컴파일 옵션이 사용되면 기본 프로세스 힙과 다를 거예요. 또한 SQLite가 assert()를 활성화하고 SQLITE_WIN32_MALLOC_VALIDATE 컴파일 옵션으로 컴파일되면 할당이 이루어지거나 해제될 때 HeapValidate()가 호출돼요.
3.1.4. Zero-malloc 메모리 할당자
SQLite가 SQLITE_ENABLE_MEMSYS5 옵션으로 컴파일되면 malloc()을 사용하지 않는 대체 메모리 할당자가 빌드에 포함돼요. SQLite 개발자는 이 대체 메모리 할당자를 "memsys5"라고 부르며, 빌드에 포함되더라도 memsys5는 기본적으로 비활성화돼요. memsys5를 활성화하려면 애플리케이션이 시작 시 다음 SQLite 인터페이스를 호출해야 해요.
sqlite3_config(SQLITE_CONFIG_HEAP, pBuf, szBuf, mnReq);
위 호출에서 pBuf는 SQLite가 모든 메모리 할당 요구를 충족하는 데 사용할 크고 연속적인 메모리 공간 청크에 대한 포인터예요. pBuf는 정적 배열을 가리킬 수도 있고 다른 애플리케이션 특정 메커니즘에서 얻은 메모리일 수도 있어요. szBuf는 pBuf가 가리키는 메모리 공간의 바이트 수인 정수예요. mnReq는 할당의 최소 크기인 또 다른 정수예요. N이 mnReq보다 작은 sqlite3_malloc(N)에 대한 호출은 mnReq로 올림돼요. mnReq는 2의 거듭제곱이어야 해요. 나중에 mnReq 매개변수가 Robson 증명에서 n 값을 낮추고 따라서 최소 메모리 크기 요구사항을 줄이는 데 중요하다는 것을 보게 될 거예요.
memsys5 할당자는 임베디드 시스템에서 사용하도록 설계됐지만, 워크스테이션에서 사용을 막을 것은 없어요. szBuf는 시스템 요구사항과 메모리 예산에 따라 보통 수백 킬로바이트에서 수십 메가바이트 사이예요.
memsys5가 사용하는 알고리즘은 "2의 거듭제곱, 최초 적합(first-fit)"이라고 부를 수 있어요. 모든 메모리 할당 요청의 크기는 2의 거듭제곱으로 올림되고, 요청은 pBuf에서 충분히 큰 첫 번째 빈 슬롯으로 충족돼요. 인접한 해제된 할당은 버디 시스템(buddy system)을 사용해 병합돼요. 적절히 사용되면 이 알고리즘은 아래에 자세히 설명된 것처럼 단편화와 붕괴에 대한 수학적 보장을 제공해요.
3.1.5. 실험적 메모리 할당자
zero-malloc 메모리 할당자에 사용된 "memsys5"라는 이름은 여러 추가 메모리 할당자가 사용 가능함을 암시하며, 실제로 그렇습니다. 기본 메모리 할당자는 "memsys1"이에요. 디버깅 메모리 할당자는 "memsys2"예요. 그것들은 이미 다뤘어요.
SQLite가 SQLITE_ENABLE_MEMSYS3으로 컴파일되면 memsys5와 유사한 또 다른 zero-malloc 메모리 할당자가 소스 트리에 포함돼요. memsys3 할당자는 memsys5와 마찬가지로 sqlite3_config(SQLITE_CONFIG_HEAP,...) 호출로 활성화되어야 해요. Memsys3는 모든 메모리 할당의 소스로 제공된 메모리 버퍼를 사용해요. memsys3와 memsys5의 차이는 memsys3가 실무에서 잘 동작하는 것처럼 보이지만 메모리 단편화와 붕괴에 대한 수학적 보장을 제공하지 않는 다른 메모리 할당 알고리즘을 사용한다는 점이에요. Memsys3는 memsys5의 전신이었어요. SQLite 개발자는 이제 memsys5가 memsys3보다 우수하며 zero-malloc 메모리 할당자가 필요한 모든 애플리케이션은 memsys3보다 memsys5를 사용해야 한다고 믿어요. Memsys3은 실험적이면서 deprecated로 간주되며 미래 SQLite 릴리스에서 소스 트리에서 제거될 가능성이 높아요.
Memsys4와 memsys6은 약 2007년에 도입된 실험적 메모리 할당자였고, 새 가치를 더하지 않는다는 것이 분명해진 후 약 2008년에 소스 트리에서 제거됐어요.
다른 실험적 메모리 할당자가 미래 SQLite 릴리스에 추가될 수 있어요. 이것들이 memsys7, memsys8 등으로 불릴 것으로 예상할 수 있어요.
3.1.6. 애플리케이션 정의 메모리 할당자
새 메모리 할당자는 SQLite 소스 트리의 일부일 필요도 없고 sqlite3.c amalgamation에 포함될 필요도 없어요. 개별 애플리케이션은 시작 시 SQLite에 자체 메모리 할당자를 제공할 수 있어요.
SQLite가 새 메모리 할당자를 사용하게 하려면 애플리케이션은 간단히 다음을 호출해요.
sqlite3_config(SQLITE_CONFIG_MALLOC, pMem);
위 호출에서 pMem은 애플리케이션 특정 메모리 할당자의 인터페이스를 정의하는 sqlite3_mem_methods 객체에 대한 포인터예요. sqlite3_mem_methods 객체는 실제로 다양한 메모리 할당 기본 연산을 구현할 함수 포인터를 포함하는 구조체일 뿐이에요.
멀티스레드 애플리케이션에서 sqlite3_mem_methods에 대한 접근은 SQLITE_CONFIG_MEMSTATUS가 활성화된 경우에만 직렬화돼요. SQLITE_CONFIG_MEMSTATUS가 비활성화되면 sqlite3_mem_methods의 메서드가 자체 직렬화 요구를 처리해야 해요.
3.1.7. 메모리 할당자 오버레이
애플리케이션은 SQLite 코어와 기본 메모리 할당자 사이에 계층 또는 "오버레이"를 삽입할 수 있어요. 예를 들어 SQLite의 메모리 부족 테스트 로직은 메모리 할당 실패를 시뮬레이션할 수 있는 오버레이를 사용해요.
오버레이는
sqlite3_config(SQLITE_CONFIG_GETMALLOC, pOldMem);
인터페이스를 사용해 기존 메모리 할당자에 대한 포인터를 얻어 만들 수 있어요. 기존 할당자는 오버레이가 저장하고 실제 메모리 할당을 하기 위한 폴백으로 사용해요. 그런 다음 위에 설명된 대로 sqlite3_config(SQLITE_CONFIG_MALLOC,...)를 사용해 오버레이가 기존 메모리 할당자 대신 삽입돼요.
3.1.8. No-op 메모리 할당자 스텁
SQLite가 SQLITE_ZERO_MALLOC 옵션으로 컴파일되면 기본 메모리 할당자가 제외되고 어떤 메모리도 절대 할당하지 않는 스텁 메모리 할당자로 대체돼요. 스텁 메모리 할당자에 대한 호출은 사용 가능한 메모리가 없다고 보고해요.
No-op 메모리 할당자는 그 자체로는 유용하지 않아요. 그것은 표준 라이브러리에 malloc(), free(), realloc()이 없을 수 있는 시스템에서 SQLite가 링크할 메모리 할당자를 갖도록 하는 자리 표시자로만 존재해요. SQLITE_ZERO_MALLOC으로 컴파일된 애플리케이션은 SQLite 사용을 시작하기 전에 sqlite3_config()와 SQLITE_CONFIG_MALLOC 또는 SQLITE_CONFIG_HEAP을 함께 사용해 새 대체 메모리 할당자를 지정해야 해요.
3.2. 페이지 캐시 메모리
대부분의 애플리케이션에서 SQLite 내부의 데이터베이스 페이지 캐시 하위 시스템은 SQLite의 다른 모든 부분을 합친 것보다 더 많은 동적 할당 메모리를 사용해요. 데이터베이스 페이지 캐시가 나머지 SQLite 전체보다 10배 이상 많은 메모리를 소비하는 것은 드문 일이 아니에요.
SQLite는 페이지 캐시 메모리 할당을 고정 크기 슬롯의 별도 구별된 메모리 풀에서 하도록 구성할 수 있어요. 여기에는 두 가지 장점이 있을 수 있어요.
-
모든 할당이 같은 크기이므로 메모리 할당자가 훨씬 더 빠르게 동작할 수 있어요. 할당자는 인접 빈 슬롯을 병합하거나 적절한 크기의 슬롯을 검색할 필요가 없어요. 모든 미할당 메모리 슬롯은 연결 리스트에 저장될 수 있어요. 할당은 리스트에서 첫 번째 항목을 제거하는 것으로 구성돼요. 해제는 단순히 항목을 리스트 시작에 추가하는 것이에요.
-
단일 할당 크기로 Robson 증명의 n 매개변수는 1이고, 할당자가 요구하는 총 메모리 공간(N)은 최대 사용 메모리(M)와 정확히 같아요. 단편화 오버헤드를 덮는 추가 메모리가 필요 없으므로 메모리 요구사항이 줄어들어요. 페이지 캐시가 SQLite 메모리 필요의 가장 큰 구성 요소를 차지하므로 이는 페이지 캐시 메모리에 특히 중요해요.
페이지 캐시 메모리 할당자는 기본적으로 비활성화돼요. 애플리케이션은 시작 시 다음과 같이 활성화할 수 있어요.
sqlite3_config(SQLITE_CONFIG_PAGECACHE, pBuf, sz, N);
pBuf 매개변수는 SQLite가 페이지 캐시 메모리 할당에 사용할 연속적인 바이트 범위에 대한 포인터예요. 버퍼는 적어도 sz*N 바이트 크기여야 해요. "sz" 매개변수는 각 페이지 캐시 할당의 크기예요. N은 사용 가능한 최대 할당 수예요.
각 페이지 캐시 할당은 데이터베이스 페이지 크기보다 크다는 점에 주목하세요. 데이터베이스 페이지 크기가 Z이면 각 페이지 캐시 메모리 할당은 Z+Y 바이트가 되며, 여기서 Y는 컴파일 옵션과 대상 프로세서에 따라 달라지는 상수예요. 특정 플랫폼과 SQLite 빌드에서 Y는 sqlite3_config()에 SQLITE_CONFIG_PCACHE_HDRSZ 옵션을 사용해 결정할 수 있어요.
SQLite가 "sz" 바이트보다 큰 페이지 캐시 항목을 필요로 하거나 N개보다 많은 항목이 필요하면 범용 메모리 할당자 사용으로 대체돼요.
3.3. 룩어사이드(Lookaside) 메모리 할당자
SQLite 데이터베이스 연결은 많고 작고 수명이 짧은 메모리 할당을 만들어요. 이는 sqlite3_prepare_v2()로 SQL 문을 컴파일할 때 가장 흔하게 발생하지만, sqlite3_step()으로 prepared statement를 실행할 때에도 덜하지만 발생해요. 이 작은 메모리 할당은 테이블과 열 이름, 파서 트리 노드, 개별 질의 결과 값, B-트리 커서 객체 같은 것을 담는 데 사용돼요. 따라서 여러 번의 malloc()과 free() 호출이 있어요. 너무 많아서 malloc()과 free()가 SQLite에 배정된 CPU 시간의 상당 부분을 사용하게 돼요.
SQLite 버전 3.6.1(2008-08-06)은 메모리 할당 부하를 줄이는 데 도움이 되도록 룩어사이드 메모리 할당자를 도입했어요. 룩어사이드 할당자에서 각 데이터베이스 연결은 단일 큰 메모리 청크(보통 60120킬로바이트 범위)를 미리 할당하고 그 청크를 각각 약 1001000바이트의 작은 고정 크기 "슬롯"으로 나눠요. 이것이 룩어사이드 메모리 풀이 돼요. 그 후 데이터베이스 연결과 관련되고 너무 크지 않은 메모리 할당은 범용 메모리 할당자를 호출하는 대신 룩어사이드 풀 슬롯 중 하나를 사용해 충족돼요. 더 큰 할당은 계속 범용 메모리 할당자를 사용하며, 룩어사이드 풀 슬롯이 모두 대출 중일 때 발생하는 할당도 마찬가지예요. 하지만 많은 경우 메모리 할당이 충분히 작고 대출 중인 것이 충분히 적어 새 메모리 요청을 룩어사이드 풀에서 충족할 수 있어요.
룩어사이드 할당은 항상 같은 크기이므로 할당과 해제 알고리즘은 매우 빠르답니다. 인접 빈 슬롯을 병합하거나 특정 크기의 슬롯을 검색할 필요가 없어요. 각 데이터베이스 연결은 사용하지 않는 슬롯의 단일 연결 리스트를 유지해요. 할당 요청은 단순히 이 리스트의 첫 번째 요소를 뽑아요. 해제는 단순히 요소를 리스트 앞에 다시 밀어 넣어요. 게다가 각 데이터베이스 연결은 이미 단일 스레드에서 실행 중인 것으로 가정되므로(이를 강제하는 뮤텍스가 이미 있음) 룩어사이드 슬롯 프리리스트에 대한 접근을 직렬화하기 위한 추가 뮤텍스가 필요 없어요. 결과적으로 룩어사이드 메모리 할당과 해제는 매우 빨라요. Linux와 Mac OS X 워크스테이션의 속도 테스트에서 SQLite는 워크로드와 룩어사이드가 구성되는 방식에 따라 최대 10%~15%의 전체 성능 개선을 보여줬어요.
룩어사이드 메모리 풀의 크기는 전역 기본값이 있지만 연결별로도 구성할 수 있어요. 컴파일 시 룩어사이드 메모리 풀의 기본 크기를 바꾸려면 -DSQLITE_DEFAULT_LOOKASIDE=SZ,N 옵션을 사용해요. 시작 시 룩어사이드 메모리 풀의 기본 크기를 바꾸려면 sqlite3_config() 인터페이스를 사용해요.
sqlite3_config(SQLITE_CONFIG_LOOKASIDE, sz, cnt);
"sz" 매개변수는 각 룩어사이드 슬롯의 바이트 크기예요. "cnt" 매개변수는 데이터베이스 연결당 총 룩어사이드 메모리 슬롯 수예요. 각 데이터베이스 연결에 할당되는 총 룩어사이드 메모리 양은 sz*cnt 바이트예요.
룩어사이드 풀은 이 호출로 개별 데이터베이스 연결 "db"에 대해 변경할 수 있어요.
sqlite3_db_config(db, SQLITE_DBCONFIG_LOOKASIDE, pBuf, sz, cnt);
"pBuf" 매개변수는 룩어사이드 메모리 풀에 사용될 메모리 공간에 대한 포인터예요. pBuf가 NULL이면 SQLite는 sqlite3_malloc()을 사용해 메모리 풀을 위한 자체 공간을 얻어요. "sz"와 "cnt" 매개변수는 각각 각 룩어사이드 슬롯의 크기와 슬롯 수예요. pBuf가 NULL이 아니면 적어도 sz*cnt 바이트의 메모리를 가리켜야 해요.
룩어사이드 구성은 데이터베이스 연결에 대출 중인 룩어사이드 할당이 없을 때만 변경할 수 있어요. 따라서 sqlite3_open()(또는 동등한 것)으로 데이터베이스 연결을 만든 직후, 연결에서 어떤 SQL 문도 평가하기 전에 구성을 설정해야 해요.
3.3.1. 두 크기 룩어사이드 (Two-Size Lookaside)
SQLite 버전 3.31.0(2020-01-22)부터 룩어사이드는 각각 다른 크기 슬롯을 가진 두 개의 메모리 풀을 지원해요. 작은 슬롯 풀은 128바이트 슬롯을 사용하고 큰 슬롯 풀은 SQLITE_DBCONFIG_LOOKASIDE가 지정한 크기(기본 1200바이트)를 사용해요. 이렇게 풀을 둘로 나누면 메모리 할당을 룩어사이드가 더 자주 덮을 수 있게 하면서도 데이터베이스 연결당 힙 사용량을 120KB에서 48KB로 줄여요.
구성은 위에 설명된 대로 매개변수 "sz"와 "cnt"로 SQLITE_DBCONFIG_LOOKASIDE 또는 SQLITE_CONFIG_LOOKASIDE 구성 옵션을 계속 사용해요. 룩어사이드에 사용되는 총 힙 공간은 계속 sz*cnt 바이트예요. 하지만 공간은 작은 슬롯 룩어사이드와 큰 슬롯 룩어사이드 사이에 할당되며, 작은 슬롯 룩어사이드에 우선권이 주어져요. "cnt"보다 총 슬롯 수가 보통 더 많을 텐데, "sz"가 보통 128바이트인 작은 슬롯 크기보다 훨씬 크기 때문이에요.
기본 룩어사이드 구성은 각각 1200바이트인 100개 슬롯(120KB)에서 각각 1200바이트인 40개 슬롯(48KB)으로 바뀌었어요. 이 공간은 결국 각각 128바이트인 93개 슬롯과 각각 1200바이트인 30개 슬롯으로 할당돼요. 따라서 더 많은 룩어사이드 슬롯을 사용할 수 있지만 훨씬 적은 힙 공간이 사용돼요.
기본 룩어사이드 구성, 작은 슬롯의 크기, 힙 공간이 작은 슬롯과 큰 슬롯 사이에 할당되는 방법의 세부사항은 모두 릴리스마다 바뀔 수 있어요.
3.4. 메모리 상태
기본적으로 SQLite는 메모리 사용량에 대한 통계를 유지해요. 이 통계는 애플리케이션이 실제로 얼마나 많은 메모리가 필요한지 결정하는 데 유용해요. 이 통계는 고신뢰성 시스템에서 메모리 사용량이 Robson 증명의 한도에 가까워지거나 초과하는지, 따라서 메모리 할당 하위 시스템이 붕괴하기 쉬운지 결정하는 데도 사용될 수 있어요.
대부분의 메모리 통계는 전역이므로 통계 추적은 뮤텍스로 직렬화되어야 해요. 통계는 기본적으로 켜져 있지만 비활성화하는 옵션이 존재해요. 메모리 통계를 비활성화함으로써 SQLite는 각 메모리 할당과 해제 시 뮤텍스에 들어가고 나오는 것을 피해요. 이 절약은 뮤텍스 연산이 비싼 시스템에서 눈에 띌 수 있어요. 메모리 통계를 비활성화하려면 시작 시 다음 인터페이스를 사용해요.
sqlite3_config(SQLITE_CONFIG_MEMSTATUS, onoff);
"onoff" 매개변수가 true면 메모리 통계 추적을 활성화하고 false면 통계 추적을 비활성화해요.
통계가 활성화되어 있다고 가정하고, 접근하려면 다음 루틴을 사용할 수 있어요.
sqlite3_status(verb, ¤t, &highwater, resetflag);
"verb" 인수는 어떤 통계에 접근할지 결정해요. 여러 가지 verb가 정의되어 있어요. 목록은 sqlite3_status() 인터페이스가 성숙해짐에 따라 늘어날 것으로 예상돼요. 선택한 매개변수의 현재 값은 정수 "current"에 쓰이고 가장 높은 과거 값은 정수 "highwater"에 쓰여요. resetflag가 true이면 호출이 반환된 후 하이-워터 마크가 현재 값으로 재설정돼요.
단일 데이터베이스 연결과 관련된 통계를 찾는 데는 다른 인터페이스가 사용돼요.
sqlite3_db_status(db, verb, ¤t, &highwater, resetflag);
이 인터페이스는 첫 번째 인수로 데이터베이스 연결에 대한 포인터를 취하고 전체 SQLite 라이브러리가 아닌 그 단일 객체에 대한 통계를 반환한다는 점을 제외하면 유사해요. sqlite3_db_status() 인터페이스는 현재 단일 verb SQLITE_DBSTATUS_LOOKASIDE_USED만 인식하지만, 미래에 추가 verb가 추가될 수 있어요.
연결별 통계는 전역 변수를 사용하지 않으므로 갱신하거나 접근하는 데 뮤텍스가 필요하지 않아요. 따라서 SQLITE_CONFIG_MEMSTATUS가 꺼져 있어도 연결별 통계는 계속 동작해요.
3.5. 메모리 사용량 한도 설정
sqlite3_soft_heap_limit64() 인터페이스는 SQLite의 범용 메모리 할당자가 한 번에 대출되도록 허용할 총 대출 메모리 양에 대한 상한을 설정하는 데 사용할 수 있어요. 소프트 힙 한도가 지정한 것보다 더 많은 메모리를 할당하려는 시도가 있으면, SQLite는 먼저 할당 요청을 계속하기 전에 캐시 메모리를 해제하려고 해요. 소프트 힙 한도 메커니즘은 메모리 통계가 활성화된 경우에만 동작하고, SQLite 라이브러리가 SQLITE_ENABLE_MEMORY_MANAGEMENT 컴파일 옵션으로 컴파일되면 가장 잘 동작해요.
소프트 힙 한도는 이런 의미에서 "소프트"해요: SQLite가 한도 아래에 머물 만큼 충분한 보조 메모리를 해제할 수 없으면, 추가 메모리를 할당하고 한도를 초과해요. 이는 완전히 실패하는 것보다 추가 메모리를 사용하는 것이 낫다는 이론으로 발생해요.
SQLite 버전 3.6.1(2008-08-06) 기준으로 소프트 힙 한도는 범용 메모리 할당자에만 적용돼요. 소프트 힙 한도는 페이지 캐시 메모리 할당자나 룩어사이드 메모리 할당자를 알지 못하고 상호작용하지 않아요. 이 결함은 미래 릴리스에서 해결될 가능성이 높아요.
4. 메모리 할당 실패에 대한 수학적 보장
동적 메모리 할당의 문제, 특히 메모리 할당자 붕괴의 문제는 J. M. Robson이 연구했고 결과는 다음과 같이 출판됐어요: J. M. Robson. "Bounds for Some Functions Concerning Dynamic Storage Allocation". Journal of the Association for Computing Machinery, Volume 21, Number 8, July 1974, pages 491-499.
(Robson의 표기법과 유사하지만 동일하지는 않은) 다음 표기법을 사용하자.
| 기호 | 의미 |
|---|---|
| N | 어떤 메모리 할당도 절대 실패하지 않음을 보장하기 위해 메모리 할당 시스템이 필요로 하는 원시 메모리 양. |
| M | 애플리케이션이 대출한 최대 메모리 양. |
| n | 가장 큰 메모리 할당과 가장 작은 것의 비율. 모든 메모리 할당 크기가 가장 작은 메모리 할당 크기의 정수배라고 가정한다. |
Robson은 다음 결과를 증명해요: N = M*(1 + (log2 n)/2) - n + 1.
쉽게 말해, Robson 증명은 붕괴 없는 동작을 보장하기 위해 어떤 메모리 할당자도 가장 큰 할당과 가장 작은 할당의 비율인 n에 의존하는 승수만큼 최대 사용 메모리 M을 초과하는 크기 N의 메모리 풀을 사용해야 한다는 것을 보여줘요. 즉 모든 메모리 할당이 정확히 같은 크기(n=1)가 아니라면 시스템은 한 번에 사용할 메모리보다 더 많은 메모리에 접근해야 해요. 더 나아가, 가장 큰 할당과 가장 작은 할당의 비율이 증가함에 따라 필요한 잉여 메모리의 양이 급격히 증가하므로, 모든 할당을 가능한 한 서로 같은 크기에 가깝게 유지해야 할 강력한 인센티브가 있어요.
Robson의 증명은 구성적(constructive)이에요. 그는 사용 가능한 메모리가 N보다 1바이트만큼 적으면 메모리 단편화로 인한 할당 실패로 이어질 할당 및 해제 연산 시퀀스를 계산하는 알고리즘을 제공해요. 그리고 Robson은 사용 가능한 메모리가 N바이트 이상이면 2의 거듭제곱 최초 적합 메모리 할당자(memsys5가 구현한 것)가 메모리 할당을 절대 실패하지 않음을 보여줘요.
M과 n 값은 애플리케이션의 속성이에요. 애플리케이션이 M과 n을 모두 알거나 적어도 알려진 상한을 갖도록 구성되고, memsys5 메모리 할당자를 사용하며, SQLITE_CONFIG_HEAP으로 N바이트의 사용 가능한 메모리 공간을 제공받으면, Robson은 애플리케이션 내에서 어떤 메모리 할당 요청도 절대 실패하지 않음을 증명해요. 다르게 말하면, 애플리케이션 개발자는 어떤 SQLite 인터페이스 호출도 SQLITE_NOMEM을 절대 반환하지 않도록 보장하는 N 값을 선택할 수 있어요. 메모리 풀은 새 메모리 할당 요청이 충족될 수 없을 정도로 단편화되지 않을 거예요. 이것은 소프트웨어 결함이 부상, 물리적 피해, 또는 대체 불가능한 데이터 손실을 일으킬 수 있는 애플리케이션에 중요한 속성이에요.
4.1. M과 n 매개변수 계산 및 제어
Robson 증명은 SQLite가 사용하는 각 메모리 할당자에 별도로 적용돼요.
- 범용 메모리 할당자(memsys5).
- 페이지 캐시 메모리 할당자.
- 룩어사이드 메모리 할당자.
memsys5 이외의 할당자에서는 모든 메모리 할당이 같은 크기예요. 따라서 n=1이고 따라서 N=M이에요. 즉 메모리 풀은 어느 순간 사용 중인 최대 메모리 양보다 클 필요가 없어요.
페이지 캐시 메모리의 사용은 SQLite 버전 3.6.1에서 다소 제어하기 어렵지만, 페이지 캐시 메모리 제어를 훨씬 쉽게 만드는 메커니즘이 후속 릴리스에 계획되어 있어요. 이 새 메커니즘 도입 전에는 페이지 캐시 메모리를 제어하는 유일한 방법은 cache_size pragma를 사용하는 것이에요.
안전 필수 애플리케이션은 보통 기본 룩어사이드 메모리 구성을 수정해서 sqlite3_open() 중 초기 룩어사이드 메모리 버퍼가 할당될 때 결과 메모리 할당이 n 매개변수를 너무 크게 만들 만큼 크지 않도록 하려 할 거예요. n을 통제하려면 가장 큰 메모리 할당을 2~4킬로바이트 아래로 유지하는 것이 가장 좋아요. 따라서 룩어사이드 메모리 할당자의 합리적인 기본 설정은 다음 중 하나일 수 있어요.
sqlite3_config(SQLITE_CONFIG_LOOKASIDE, 32, 32); /* 1K */
sqlite3_config(SQLITE_CONFIG_LOOKASIDE, 64, 32); /* 2K */
sqlite3_config(SQLITE_CONFIG_LOOKASIDE, 32, 64); /* 2K */
sqlite3_config(SQLITE_CONFIG_LOOKASIDE, 64, 64); /* 4K */
또 다른 접근은 처음에 룩어사이드 메모리 할당자를 비활성화하는 거예요.
sqlite3_config(SQLITE_CONFIG_LOOKASIDE, 0, 0);
그런 다음 애플리케이션이 별도의 더 큰 룩어사이드 메모리 버퍼 풀을 유지하고 데이터베이스 연결이 생성될 때 그것들을 배포하게 해요. 일반적인 경우 애플리케이션은 단일 데이터베이스 연결만 가지므로 룩어사이드 메모리 풀은 단일 큰 버퍼로 구성될 수 있어요.
sqlite3_db_config(db, SQLITE_DBCONFIG_LOOKASIDE, aStatic, 256, 500);
룩어사이드 메모리 할당자는 실제로 붕괴 없는 메모리 할당을 보장하는 방법이 아니라 성능 최적화로 의도된 것이므로, 안전 필수 연산에 룩어사이드 메모리 할당자를 완전히 비활성화하는 것도 불합리하지 않아요.
범용 메모리 할당자는 다양한 크기의 할당을 지원하므로 관리하기 가장 어려운 메모리 풀이에요. n이 M에 대한 승수이므로 n을 가능한 한 작게 유지하고 싶어요. 이는 memsys5의 최소 할당 크기를 가능한 한 크게 유지하는 것을 주장해요. 대부분의 애플리케이션에서 룩어사이드 메모리 할당자가 작은 할당을 처리할 수 있어요. 따라서 memsys5의 최소 할당 크기를 룩어사이드 할당 최대 크기의 2, 4, 또는 심지어 8배로 설정하는 것이 합리적이에요. 최소 할당 크기 512가 합리적인 설정이에요.
n을 작게 유지하는 것에 더해, 가장 큰 메모리 할당의 크기를 통제하고 싶어요. 범용 메모리 할당자에 대한 큰 요청은 여러 원천에서 올 수 있어요.
- 큰 문자열이나 BLOB을 포함하는 SQL 테이블 행.
- 큰 prepared statement로 컴파일되는 복잡한 SQL 질의.
- sqlite3_prepare_v2()가 내부적으로 사용하는 SQL 파서 객체.
- 데이터베이스 연결 객체의 저장 공간.
- 범용 메모리 할당자로 넘쳐흐르는 페이지 캐시 메모리 할당.
- 새 데이터베이스 연결을 위한 룩어사이드 버퍼 할당.
마지막 두 할당은 위에 설명된 대로 페이지 캐시 메모리 할당자와 룩어사이드 메모리 할당자를 적절히 구성해 제어하거나 제거할 수 있어요. 데이터베이스 연결 객체에 필요한 저장 공간은 데이터베이스 파일의 파일명 길이에 다소 의존하지만 32비트 시스템에서 2KB를 거의 초과하지 않아요. (64비트 시스템에서는 포인터 크기가 커져 더 많은 공간이 필요해요.) 각 파서 객체는 약 1.6KB의 메모리를 사용해요. 따라서 위의 3~6번 항목은 최대 메모리 할당 크기를 2KB 아래로 유지하도록 쉽게 제어할 수 있어요.
애플리케이션이 작은 조각으로 데이터를 관리하도록 설계되면 데이터베이스는 큰 문자열이나 BLOB을 절대 포함해서는 안 되므로 위 1번 항목은 요인이 되지 않아야 해요. 데이터베이스가 큰 문자열이나 BLOB을 포함한다면 증분 BLOB I/O로 읽어야 하고, 큰 문자열이나 BLOB을 포함하는 행은 증분 BLOB I/O 이외의 수단으로 절대 갱신되어서는 안 돼요. 그렇지 않으면 sqlite3_step() 루틴이 어떤 시점에 전체 행을 연속 메모리로 읽어야 하고, 그것은 적어도 한 번의 큰 메모리 할당을 수반할 거예요.
큰 메모리 할당의 마지막 원천은 복잡한 SQL 연산을 컴파일한 결과인 prepared statement를 담는 공간이에요. SQLite 개발자의 지속적인 작업이 여기 필요한 공간을 줄이고 있어요. 하지만 크고 복잡한 질의는 여전히 몇 킬로바이트 크기의 prepared statement를 요구할 수 있어요. 현재의 유일한 해결 방법은 애플리케이션이 복잡한 SQL 연산을 별도의 prepared statement에 담긴 둘 이상의 더 작고 단순한 연산으로 나누는 것이에요.
모든 것을 고려하면 애플리케이션은 보통 최대 메모리 할당 크기를 2K 또는 4K 아래로 유지할 수 있어야 해요. 이는 log2(n) 값 2 또는 3을 제공해요. 이는 N을 M의 2배에서 2.5배 사이로 제한할 거예요.
애플리케이션에 필요한 최대 범용 메모리 양은 애플리케이션이 동시에 열어 둔 데이터베이스 연결과 prepared statement 객체 수, 그리고 prepared statement의 복잡성 같은 요인에 의해 결정돼요. 주어진 애플리케이션에 대해 이 요인들은 보통 고정되어 있고 SQLITE_STATUS_MEMORY_USED를 사용해 실험적으로 결정할 수 있어요. 전형적인 애플리케이션은 약 40KB의 범용 메모리만 사용할 수 있어요. 이는 N 값 약 100KB를 제공해요.
4.2. 연성 실패 (Ductile failure)
SQLite 내부의 메모리 할당 하위 시스템이 붕괴 없는 동작으로 구성되었는데 실제 메모리 사용이 Robson 증명이 설정한 설계 한도를 초과하면, SQLite는 보통 계속 정상적으로 동작해요. 페이지 캐시 메모리 할당자와 룩어사이드 메모리 할당자는 자동으로 memsys5 범용 메모리 할당자로 장애 조치(failover)돼요. 그리고 memsys5 메모리 할당자는 M과/또는 n이 Robson 증명이 부과한 한도를 초과하더라도 단편화 없이 계속 동작하는 것이 보통이에요. Robson 증명은 이 상황에서 메모리 할당이 붕괴하고 실패할 수 있음을 보여주지만, 그런 실패는 특히 끔찍한 할당·해제 시퀀스를 요구해요. SQLite가 그런 시퀀스를 따르는 것은 관찰된 적이 없어요. 그래서 실무에서 Robson이 부과한 한도를 상당한 여유로 초과해도 해로운 영향이 없는 경우가 보통이에요.
그럼에도 애플리케이션 개발자는 메모리 할당 하위 시스템의 상태를 모니터링하고 메모리 사용이 Robson 한도에 가까워지거나 초과하면 경보를 울리도록 경고받아요. 이렇게 함으로써 애플리케이션은 실패 훨씬 전에 운영자에게 충분한 경고를 제공해요. SQLite의 메모리 통계 인터페이스는 애플리케이션에 이 작업의 모니터링 부분을 완료하는 데 필요한 모든 메커니즘을 제공해요.
5. 메모리 인터페이스의 안정성
업데이트: SQLite 버전 3.7.0(2010-07-21) 기준으로 SQLite의 모든 메모리 할당 인터페이스는 안정적인 것으로 간주되며 미래 릴리스에서 지원될 거예요.
더 알아보기 (Learn more)
- sqlite3_config() C 인터페이스
- sqlite3_soft_heap_limit64() C 인터페이스
- sqlite3_status() / sqlite3_db_status()
- SQLITE_NOMEM 오류 코드
- Compile-time options
- SQLite 테스트: Testing / TH3