본문으로 건너뛰기

TCB v102 — 컬럼 인코딩과 암호화

상태: 구현 완료 (암호화 제외) · 대상 버전: 102 (101을 대체, 101은 배포된 적이 없으므로 호환 경로 없음)

현재 형식 버전은 104입니다 — TCB v103이 presence 비트맵으로 이 버전을 대체하였고, TCB v104가 인코딩 4종을 더하며 다시 대체하였습니다. 이 문서의 인코딩 9종과 디스크립터 배치는 그 뒤로도 그대로입니다 — 바뀐 것은 배열도 인코딩된다는 것이고(v104 3절), 그래서 아래 「스칼라만 인코딩합니다」는 v102 시점의 결정입니다.

.tcb의 컬럼 블록에 값 인코딩을 도입하고, 파일 단위 대칭키 암호화의 자리를 확정합니다. 지금까지 블록은 값을 원문 그대로 나열하였고(v101), 이 스펙은 블록마다 인코딩 하나를 골라 적을 수 있게 합니다. 무엇을 왜 고르는지는 변환기가 결정하고, 파일은 그 결정을 디스크립터에 적으며, 리더는 적힌 대로 풉니다. 암호화는 이 버전에서 스펙만 확정하고 구현은 뒤로 미룹니다 — 자리를 먼저 잡아 두는 것은, 나중에 붙일 때 형식 버전을 다시 올리지 않기 위해서입니다.

이름 — LiteBinary에서 Tcb로

이 작업과 함께 런타임·변환기의 LiteBinary* 이름을 전부 Tcb*로 바꿉니다. 형식의 이름이 TCB(Tabbit Compiled Binary)인데 코드가 다른 이름을 쓰는 것은 역사적 우연일 뿐입니다.

지금바뀐 뒤
LiteBinaryFormat / LiteBinaryWriter (변환기)TcbFormat / TcbWriter
LiteBinaryReader / LiteBinaryTable / LiteBinaryColumn / LiteBinaryException (모든 언어 런타임)TcbReader / TcbTable / TcbColumn / TcbException
lib/*/tabbit/LiteBinaryReader.*, lite_binary_reader.* 등 파일 이름각 언어 관례로 TcbReader.*, tcb_reader.*

1. 변경 근거 — 측정 결과

한 데이터셋(67개 테이블 · 109,218행 · 7.83 MB, 벤치마크)의 바이트 구성:

타입현재 바이트왜 큰가
string5.40 MB (69%)행마다 원문 그대로 반복. 최대 컬럼은 유니크 3개를 103,395번 적음
i322.21 MB (28%)값이 0~750이어도 고정 4바이트. id 컬럼의 델타는 대부분 1
varint(enum)0.12 MB유니크 3개가 3개의 런으로 정렬돼 있는데 행마다 1바이트
기타(f32·i64·bool)0.10 MB

값 하나하나는 조밀하지만 값 사이의 중복을 전혀 모델링하지 않는 것이 v101이고, 그 중복이 파일의 대부분입니다. 컬럼 지향 배치는 같은 성질의 값을 이미 한 블록에 모아 두었으므로, 인코딩은 그 블록 안에서만 닫히면 됩니다.

같은 데이터셋에 이 스펙의 인코딩을 전부 적용한 실측은 7.83 MB → 166 KB입니다. gzip한 JSON(1.10 MB)보다 6.6배 작고, 외부 압축 라이브러리 없이 그렇습니다.

거기까지 두 단계였습니다. 정수·문자열의 중복을 지우는 첫 묶음(RAW·VARINT·DELTA·RLE·DELTA_RLE·DICT·DICT_RLE)이 360 KB까지 데려갔고, 남은 것을 다시 측정해 보니 중복이 아니라 접두사였습니다 — 문자열 7,568개 중 둘 이상의 컬럼에 등장하는 것은 412개뿐이라 전역 문자열 풀은 1.8%밖에 못 줄였을 것이고, 대신 정렬 후 접두사 공유가 62%를 줄였습니다. 실수 컬럼이 통째로 RAW에 남아 있던 것도 그때 드러났습니다.

배치가 애초에 왜 컬럼 지향인지 — 그리고 그 선택이 이 인코딩들을 어떻게 가능하게 했는지 — 는 왜 컬럼 지향인가에 있습니다.

2. 바꾸지 않는 것

바이너리 형식의 불변식은 전부 유지됩니다.

  • 모르는 컬럼 건너뛰기 = advance(byteLength) 한 번. 인코딩 메타데이터(사전 포함)는 전부 컬럼 블록 에 있고, byteLength는 인코딩된 블록의 총 바이트입니다. 리더가 인코딩을 몰라도 건너뛰기는 성립합니다.
  • 어떤 스키마 변경도 감지되지 않는 읽기 오류가 되지 않습니다. 아는 컬럼에 모르는 인코딩이 적혀 있으면 필드 이름과 인코딩 번호와 함께 멈춥니다.
  • 승격 표는 그대로입니다. 인코딩은 디코드 후 원소 타입의 값으로 풀리므로 승격과 직교합니다. varint 컬럼이 RLE로 적혀 있어도 i32 멤버가 읽습니다.
  • 스키마 베이스라인은 무관합니다. 인코딩은 스키마가 아니라 매 내보내기의 선택이고, 같은 스키마의 두 내보내기가 다른 인코딩을 골라도 계약 위반이 아닙니다.
  • 행 순서는 파일에 적힌 그대로입니다. 압축률을 위한 행 재배열은 하지 않습니다 — 행 순서는 리더가 리스트를 만드는 순서라 관찰 가능한 의미입니다.

버전 정수는 101 → 102입니다. 형식 버전은 하나뿐이라는 정책도 그대로입니다 — 102가 101을 대체하고, 101을 읽는 경로는 없습니다.

3. 헤더와 디스크립터

디스크립터에 encoding 바이트 하나가 들어갑니다. flags는 예약이 아니라 정의된 비트를 갖게 됩니다(4절의 암호화). 그 외 배치는 v101과 같습니다.

fixed32 version = 102
fixed8 flags bit0 = 암호화(4절), bit1 = 압축(예약), 나머지 = 예약(0)
[flags bit0가 서 있으면 여기부터 암호화 envelope — 4절]
counter32 rowCount
counter32 columnCount
columnCount ×: ── 디스크립터 ──
counter32 tag
fixed8 wire v101과 동일 (하위 4비트 원소, 5~6비트 종류)
fixed8 encoding 이 스펙의 5절. 리더가 모르는 값이면 거부
counter32 count
fixed32 byteLength 인코딩된 블록의 총 바이트 (backpatch 때문에 fixed32)
columnCount ×: ── 데이터 ──
블록: encoding이 정한 배치

디스크립터는 보통의 경우 컬럼당 8바이트가 됩니다(v101은 7바이트).

헤더 검증 규칙의 변경

v101의 「어느 컬럼이든 한 행이 최소 1바이트」 하한은 encoding = 0(RAW)인 컬럼에만 적용합니다. 인코딩된 블록은 행 수와 무관하게 작아질 수 있으므로(RLE 한 런이 10만 행을 덮음) 그 하한이 성립하지 않습니다.

  • byteLength 합 = 헤더 뒤 남은 바이트 전부 — 유지.
  • RAW 컬럼의 rowCount ≤ byteLength 하한 — 유지.
  • 인코딩된 컬럼의 하한 — 없음. 대신 디코드가 행 수·런 합계·사전 인덱스를 검사하며(7절), 어긋나면 그 컬럼과 함께 멈춥니다.

이 완화로 남는 위험: 모든 컬럼이 인코딩된 손상 파일이 큰 rowCount를 선언하면, 행 리스트 할당이 디코드 검사보다 먼저 일어납니다. v101이 막던 「손상된 행 수 = 20억 행 할당」의 방어선이 그만큼 얇아지는 것이고, 이는 신뢰할 수 없는 입력을 읽는 형식이 아니라는 전제(자기 파이프라인이 만든 파일을 읽음)에서 수용합니다.

4. 암호화 — 대칭키, 스펙만 확정

클라이언트에 배포되는 기획 데이터의 캐주얼한 데이터마이닝과 수정을 차단하는 layer입니다. 위협 모델은 다음과 같습니다 — 대칭키는 클라이언트 안에 실리므로, 바이너리에서 키를 꺼낼 수 있는 공격자에게는 장벽이 아닙니다. 그 공격자를 차단하는 형식은 존재하지 않고, 이 layer가 얻는 것은 「파일을 에디터로 열면 다 보인다」와 「값을 바꿔 넣어도 그대로 로드된다」 두 가지를 없애는 것입니다.

v102에서는 형식만 확정하고 구현하지 않습니다. 확정해 두는 이유는 envelope이 헤더 배치에 관여하기 때문입니다 — 자리를 지금 정해야 암호화를 붙이는 날 버전을 다시 올리지 않습니다.

fixed32 version = 102 평문 — 리더는 버전과 flags를 먼저 봅니다
fixed8 flags bit0 = 1
fixed8 cipher 1 = ChaCha20. 그 외 값은 거부
bytes12 nonce
── 여기부터 암호문 (keystream XOR, 길이 불변) ──
bytes4 magic = 54 43 42 00 "TCB\0" — 복호가 맞았는지의 즉답
counter32 rowCount
counter32 columnCount
…이하 3절과 동일…
  • 암호는 ChaCha20 (RFC 8439, 256비트 키)입니다. 모든 언어에 외부 의존성 없이 수십 줄로 구현되고, 하드웨어 가속이 없는 환경에서도 빠르며, 스트림 XOR이라 길이가 변하지 않습니다 — byteLength 합계 검증을 비롯한 구조 검사가 평문 기준 그대로 성립합니다.
  • nonce는 난수가 아니라 평문의 SHA-256 앞 12바이트입니다. 난수 nonce는 같은 입력에서 다른 파일을 만들어 골든 트리와 결정적 출력을 깨뜨립니다. 내용이 같다는 사실(같은 파일 = 같은 바이트)이 노출되는 것은 이 위협 모델에서 수용합니다. 키가 같고 내용이 다르면 nonce가 달라지므로 keystream 재사용이 없습니다.
  • MAC은 없습니다. 키가 클라이언트에 있는 이상 MAC은 무결성 보장이 아니라 손상 감지인데, 손상 감지는 구조 검증(블록 길이 합, CheckBlockEnd)이 이미 합니다. 대신 암호문 헤더의 magic 4바이트가 「키가 다르다」와 「파일이 손상됐다」를 구분해 줍니다 — 잘못된 키는 magic에서 그 말로 멈춥니다.
  • 키는 recipe에 평문으로 적지 않습니다. recipe는 환경 변수 이름 또는 키 파일 경로를 받고(hex 64자 = 32바이트), 리더 쪽은 ReadAsync가 키를 인자로 받습니다. 키를 어디에 보관하고 어떻게 클라이언트에 넣을지는 사용자의 결정입니다.
  • layer 순서는 인코딩 → (압축, 예약) → 암호화입니다. 압축은 반복이 있어야 일하므로 암호문 위에서는 무의미하고, 그래서 암호화가 항상 바깥입니다.
  • flags bit0가 서 있는데 키가 없거나 cipher 값을 모르면, 테이블 리더는 그 자리에서 멈춥니다. v102 구현이 릴리스되는 시점에는 「bit0 = 미구현 기능」이므로 flags ≠ 0 거부이라는 v101의 동작과 실질적으로 같습니다.

5. 인코딩

번호와 적용 대상

번호이름적용 가능한 원소 (스칼라만)
0RAW전부
1VARINTi32
2DELTAi32
3RLEi32, varint, bool
4DELTA_RLEi32
5DICTstring, i64, f32, f64
6DICT_RLEstring, i64, f32, f64
7DICT_FRONTstring
8DICT_FRONT_RLEstring

사전은 원소로 매개변수화됩니다. DICT 계열의 사전 항목은 그 컬럼 원소의 RAW 형태 그대로입니다 — string은 counter32 길이 + UTF-8, i64·f64는 fixed64, f32는 fixed32. 그래서 인코딩 번호가 늘지 않고도 사전이 문자열 밖으로 나갑니다. 실측에서 f32 컬럼은 값의 5.7%만 유니크였고(0.0이 2,450번), i64도 마찬가지였습니다 — 「실수는 압축이 안 된다」는 것은 값 하나를 볼 때의 이야기이고, 컬럼으로 보면 기획 데이터의 실수는 몇 개의 값이 반복됩니다.

  • 스칼라만 인코딩합니다(종류 0). 고정 배열·가변 배열은 항상 RAW입니다. 가변 배열의 원소를 varint로 옮기면 6.4 KB를 더 줄일 수 있지만(전체의 1.8%), 인코딩에 종류 의존성을 들이는 값으로는 모자랍니다.
  • uuid는 항상 RAW입니다. 16바이트짜리 사전 항목은 반복이 아주 심하지 않으면 손해이고, 실측 데이터셋에 uuid 컬럼이 없습니다.
  • foreign 참조 인덱스는 i32이므로 i32 규칙을 그대로 따릅니다. datetime·timespan은 i64 원소이므로 사전이 걸릴 수 있고, 그때도 리더는 틱을 받아 자기 타입을 만듭니다.
  • 리더는 위 표에 없는 (원소, 인코딩) 조합을 만나면 — 예: string 컬럼에 DELTA, uuid 컬럼에 DICT — 필드 이름과 함께 멈춥니다. 모르는 번호도 같습니다.

공통 원자

기존 용어를 그대로 씁니다: counter32는 지그재그로 부호를 접은 뒤 varint32, stringcounter32 바이트 길이 + UTF-8. 아래의 「값」은 전부 counter32로 적습니다 — i32 값, enum 값, 런 길이, 사전 인덱스 모두.

각 인코딩의 블록 배치

0 — RAW. v101과 동일. i32는 fixed32, varint는 counter32, string은 길이 + UTF-8, 행 순서대로 rowCount개.

1 — VARINT.

rowCount ×: counter32 value

2 — DELTA.

counter32 first (rowCount ≥ 1일 때)
(rowCount − 1) ×: counter32 delta

delta[i] = value[i+1] − value[i]32비트 wrapping 뺄셈으로 계산하고, 복원은 wrapping 덧셈입니다. int32 두 값의 차는 int32를 넘칠 수 있지만(최솟값→최댓값), 2의 보수에서 wrapping 연산은 모든 쌍을 정확히 왕복시킵니다. 64비트 정수가 기본인 언어(Python·PHP·Ruby·Dart·TypeScript)는 덧셈 뒤 하위 32비트로 자르고 부호 확장해야 합니다 — 이 규칙이 적합성 코퍼스의 경계값 케이스로 고정됩니다.

3 — RLE.

반복 ×:
counter32 runLength ≥ 1
counter32 value

런 길이의 합이 정확히 rowCount여야 합니다. 넘치거나 모자라면 그 컬럼과 함께 멈춥니다.

4 — DELTA_RLE. 델타 스트림(위 2번 인코딩의 정의)의 RLE입니다.

counter32 first (rowCount ≥ 1일 때)
반복 ×:
counter32 runLength ≥ 1
counter32 delta

런 길이의 합이 정확히 rowCount − 1이어야 합니다. rowCount ≤ 1이면 런이 없습니다.

5 — DICT.

counter32 dictCount ≥ 0
dictCount ×: entry 원소의 RAW 형태. 첫 등장 순서
rowCount ×: counter32 index 0 ≤ index < dictCount

사전 항목은 값의 첫 등장 순서로 적습니다 — 정렬이 아니라 등장 순서인 것은 출력을 결정적으로 만들면서 writer가 정렬 비용 없이 한 번의 순회로 사전을 만들 수 있기 때문입니다. 범위를 벗어난 인덱스는 그 컬럼과 함께 멈춥니다.

6 — DICT_RLE. 사전은 5번과 같고, 인덱스 스트림이 RLE입니다.

counter32 dictCount
dictCount ×: entry
반복 ×:
counter32 runLength ≥ 1
counter32 index

런 길이의 합이 정확히 rowCount여야 합니다.

7 — DICT_FRONT. string 전용. 사전이 바이트 오름차순으로 정렬되고, 각 항목은 앞 항목과 공유하는 접두사 길이만 적고 나머지만 씁니다.

counter32 dictCount ≥ 0
dictCount ×: 바이트 오름차순
counter32 shared 앞 항목과 공유하는 바이트 수. 첫 항목은 0
counter32 restLength
restLength 바이트 UTF-8의 나머지
rowCount ×: counter32 index

이게 있는 이유는 기획 데이터의 문자열이 중복이 아니라 접두사를 공유하기 때문입니다. 02_BLOCK_INT·02_CRI_DAMAGE_FLOAT·02_CRI_INT, 또는 N등급 근거리 0성급·N등급 근거리 10성급 — 서로 다른 값이라 사전은 전부 보관해야 하지만 앞부분이 계속 겹칩니다. 실측 데이터셋에서 문자열 바이트의 62%가 이것으로 사라집니다.

shared앞 항목의 길이를 넘을 수 없습니다. 넘으면 그 컬럼과 함께 멈춥니다. 이 항목 자신의 길이는 shared + restLength로 정의되므로 따로 검사할 것이 없습니다. 정렬은 UTF-8 바이트열의 사전식 순서입니다 — 로케일이나 문자 단위가 아니라 바이트이므로, 어느 언어에서 인코딩해도 같은 순서가 나옵니다.

8 — DICT_FRONT_RLE. 사전은 7번과 같고, 인덱스 스트림이 RLE입니다.

counter32 dictCount
dictCount ×: shared / restLength / 바이트
반복 ×:
counter32 runLength ≥ 1
counter32 index

사전 순서가 인코딩마다 다른 이유. DICT·DICT_RLE는 첫 등장 순서, DICT_FRONT 계열은 바이트 오름차순입니다. 정렬은 접두사 공유를 위해서만 필요하고, 정렬하지 않는 쪽은 순회 한 번으로 사전을 만들 수 있습니다. 어느 쪽이든 런 구조는 바뀌지 않습니다 — 어느 행끼리 값이 같은지는 사전의 번호 매김과 무관하므로, 같은 컬럼에 대해 RLE가 만드는 런의 개수는 순서와 상관없이 같습니다.

경계 사례

  • rowCount = 0: 모든 인코딩의 블록이 0바이트로 같아지므로, 아래 선택 규칙(크기가 같으면 낮은 번호)에 따라 항상 RAW가 됩니다.
  • rowCount = 1: DELTA·DELTA_RLE는 first 하나뿐입니다.
  • 빈 문자열은 사전의 정상 항목입니다.

6. Writer — 후보 전수 시도

변환기는 통계 휴리스틱을 쓰지 않습니다. 적용 가능한 인코딩을 전부 인코딩해 보고 가장 작은 것을 고릅니다. 크기가 같으면 낮은 번호를 고릅니다.

  • 인코딩 시간은 이 형식의 설계 조건에서 중요하지 않고(서버·클라 시작 시 읽기만 빠르면 됨), 전수 시도는 휴리스틱보다 단순하며 정의상 최적입니다.
  • 선택이 결정적이므로 같은 입력은 같은 바이트를 냅니다 — 골든 트리와 형식 고정 테스트가 이 성질에 기댑니다.
  • 후보 인코딩은 임시 버퍼에 쓰고, 선택된 것만 본 버퍼로 갑니다. 컬럼 값을 한 번 모아 두 번 이상 순회하는 비용은 내보내기 시간으로 지불합니다.

7. Reader — 컬럼 커서

생성되는 테이블 리더의 행 루프는 유지하고, 값을 꺼내는 자리만 커서로 바뀝니다. 커서는 런타임(각 언어의 TcbReader 곁)에 한 벌 있고, 컬럼을 열 때 (원소, 인코딩)을 받아 상태를 갖습니다.

  • 정수 커서 (varint·i32 원소, 인코딩 0~4): next()가 int32를 돌려줍니다. DELTA 계열은 누적기, RLE 계열은 (남은 런 길이, 현재 값)이 상태의 전부입니다. i64·f64 멤버로의 승격은 커서가 돌려준 값을 생성 코드가 넓히는 것이므로 커서와 무관합니다.
  • 문자열 커서 (string 원소, 인코딩 0·5·6): DICT 계열은 블록 헤더에서 사전을 한 번 디코드해 문자열 배열로 들고, next()는 그 배열을 인덱싱합니다. 참조를 공유할 수 있는 언어에서는 유니크당 한 번만 문자열을 만듭니다 — 10만 행 컬럼의 문자열 할당이 유니크 개수로 줄어드는 것은 파일 크기와 별개의 이득입니다.
  • 그 외 원소는 커서 없이 v101처럼 직접 읽습니다.

디코드 검사는 5절의 규칙(런 합계, 사전 인덱스 범위)에 더해, 블록 소비량이 byteLength와 정확히 같은지 확인하는 기존 CheckBlockEnd가 그대로 마지막 방어선입니다.

8. 형식 고정 — 실제 파일 한 개, 전부

v101 문서의 SecondTable(1행 3컬럼)을 v102로 다시 적으면 39바이트입니다. 이 바이트열이 형식 고정 테스트의 새 내용이 됩니다.

66 00 00 00 version 102
00 flags
02 rowCount counter32, 지그재그 2 → 1
06 columnCount counter32, 지그재그 6 → 3

02 tag 1
02 wire: 원소 i32(2), 종류 스칼라(0)
01 encoding: VARINT — 값 1은 varint 1바이트 < fixed32 4바이트
02 count 1
01 00 00 00 byteLength 1

04 tag 2
06 wire: 원소 string(6), 종류 스칼라(0)
00 encoding: RAW — 유니크 1개짜리 1행 컬럼은 사전이 오히려 큼
02 count 1
06 00 00 00 byteLength 6

06 tag 3
02 wire: 원소 i32(2), 종류 스칼라(0)
01 encoding: VARINT
02 count 1
01 00 00 00 byteLength 1

02 index 블록: 지그재그 2 → 1
0a 67 61 6d 6d 61 label 블록: 길이 5 + "gamma"
3c amount 블록: 지그재그 60 → 30

1행짜리 테이블에서도 선택 규칙이 그대로 작동하는 것을 보여줍니다: i32 컬럼은 VARINT가 RAW보다 작고(1바이트 < 4바이트), 문자열 컬럼은 RAW가 DICT보다 작습니다(6바이트 < 8바이트).

9. 검증 게이트

게이트변경
형식 고정8절의 39바이트로 교체. 스펙에서 손으로 적어 테스트 소스에 적어 두는 성질 유지
적합성 코퍼스 ×13인코딩 커버리지: 9개 인코딩 각각이 실제로 선택되는 컬럼을 코퍼스에 넣고, 모든 언어가 왕복. wrapping 델타의 경계값(int32 최솟값·최댓값 인접 행) 포함
인코딩 선택 단정writer 단위 테스트가 커버리지 테이블의 컬럼별 선택 결과를 단정 — 선택 규칙의 회귀를 여기서 잡음
스큐 코퍼스 (C#)evolution-v1/v2 재생성. 모르는 인코딩 거부 케이스 추가
디스크립터 정합성커밋된 .tcb 검사를 3절의 새 규칙(RAW만 행 하한)으로 갱신
골든 트리변환기 출력에서 재기록

10. 예상 효과 (측정 기반)

그 데이터셋의 컬럼별 통계로 계산한 추정입니다.

원소별로 어디에 바이트가 남았는지:

원소v101v102 실측주 인코딩
string5.40 MB100.3 KBDICT_FRONT·DICT
f3275 KB26.4 KBDICT·DICT_RLE
i322.21 MB24.5 KBDELTA_RLE·RLE·VARINT
varint(enum)0.12 MB3.8 KBRLE
i6420.8 KB3.5 KBDICT_RLE
bool1.3 KB0.1 KBRLE
배열·uuid13 KB13 KBRAW 유지
합계7.83 MB166 KB

인코딩별 실제 사용량(852개 컬럼):

인코딩컬럼 수바이트
DICT_FRONT11391.2 KB
RAW15627.7 KB
DICT4016.9 KB
DELTA_RLE1186.1 KB
DICT_FRONT_RLE165.4 KB
RLE2395.4 KB
DICT_RLE993.6 KB
DELTA241.8 KB
VARINT470.6 KB

RAW가 컬럼 수의 18%인데 바이트의 17%입니다. 압축할 반복이 없는 컬럼(uuid·배열·전부 유니크한 문자열)이 남은 부피의 대부분이고, 이 데이터에서 더 줄일 수 없는 바닥입니다.

파일 크기 밖의 효과도 실측했습니다: 전체 로드가 24 ms → 8 ms, 로드 중 총 할당이 35.5 MB → 12.4 MB, 그중 가비지가 7.9 MB → 0.5 MB. 그리고 DICT 컬럼이 문자열 객체를 유니크당 하나만 만들므로 로드 후 상주 메모리가 27.6 MB → 11.9 MB입니다 — 「상주는 형식과 무관」이라던 벤치마크 문서의 결론이 이것으로 바뀌었습니다.

그 뒤 디코드가 런 단위로 값을 넘기게 하면서 8 ms가 다시 5 ms가 됐습니다. 형식은 그대로입니다 — RLE 계열 블록은 이미 「이 값이 N번」이라고 적혀 있는데 리더가 그것을 행마다 한 번씩 풀어주고 있었을 뿐이고, 커서가 N을 그대로 넘기게 한 것입니다. 런 길이 인코딩이 파일에서 지운 반복은 디코드에서도 지울 수 있다 — 이 절의 인코딩들이 파일 크기 다음으로 낸 값입니다.

이 통로는 모든 언어의 리더 전부NextSameI32·NextSameString(언어별 표기)으로 있습니다. 런이 없는 인코딩은 1을 답하므로 호출하는 쪽 루프는 9가지 인코딩 전부에서 그대로 맞습니다. 언어마다 이득이 얼마나 다른지는 벤치마크에 있습니다.

한 테이블을 끝까지 따라간 사례는 벤치마크에 있습니다: LevelCurve 95,490행 × 9컬럼이 이름 있는 JSON 13.2 MB에서 360,594 B가 됩니다.

11. v102에서 하지 않는 것

  • 암호화의 구현. 형식은 4절에서 확정하였고, 변환기와 모든 언어 런타임의 ChaCha20은 뒤 단계입니다. 그때까지 모든 파일은 flags bit0 = 0으로 나갑니다.
  • zstd 등 범용 압축 레이어. flags bit1이 자리로 남아 있습니다. 인코딩만으로 gzip한 JSON보다 작아지므로, 모든 언어에 압축 의존성을 다는 비용은 그 뒤에 남는 0.5 MB를 보고 판단합니다.
  • Bit Packing · Delta-of-Delta. 등차 구간은 DELTA_RLE가 이미 처리하고, varint 뒤의 잔여 이득이 모든 언어 구현 비용을 정당화하지 못합니다.
  • 행 재배열. 2절 참조.
  • i64·배열 인코딩. 5절 참조. 필요해지는 데이터가 나타나면 인코딩 번호는 자리가 있습니다.

12. 구현 순서

  1. 개명LiteBinary*Tcb*. 완료.
  2. v102 프레임 — 인코딩 상수와 버전 102, 디스크립터의 encoding 바이트, writer의 후보 전수 시도, C# 커서, 형식 고정 테스트 교체. 완료.
  3. DICT / DICT_RLE — 문자열 스칼라. 완료.
  4. VARINT / DELTA / RLE / DELTA_RLE — 정수·enum 스칼라. 완료.
  5. 나머지 언어 런타임 + 적합성 코퍼스의 인코딩 커버리지 — 모든 언어 왕복 전부 통과. 완료.
  6. 벤치 재측정과 문서 개정benchmark.md·binary-format.md·exports.md·로드맵. 완료.
  7. 암호화 구현 — 4절의 스펙대로, recipe의 키 공급과 모든 언어의 ChaCha20. 완료TCB v104에서 구현되었고, 형식의 자리가 이미 잡혀 있어 envelope 때문에 버전이 오르지는 않았습니다.

암호화를 뺀 전부가 들어갔고, 전체 스위트 520개가 통과합니다.