와이어와 생성 표면
6. 와이어 — 형식 무변경
이 문서의 핵심입니다. Luban 대조 §5.2는 다형 레코드에 「형식 개정 1회」를 산정하고 「컬럼 지향 인코딩에서 행마다 컬럼 집합이 달라지는 것을 어떻게 담을지가 가장 어려운 자리」라고 기재하였는데, 그 질문은 presence 비트맵이 이미 정하고 있습니다.
| 다형 레코드가 싣는 것 | 와이어 |
|---|---|
| 판별자 | enum 컬럼 하나. 멀티 로우면 KindArray |
| 기반 필드 | 평범한 컬럼. 모든 행에 present입니다 |
| 변종 멤버 | 옵셔널 컬럼 — v103 비트 6(로우) · v106 비트 7(원소) |
v103이 「값은 모든 로우에 대해 기록하고, 없는 로우는 타입의 빈 값을 차지한다」로 배치를 정해 두었으므로, 변종 멤버 컬럼은 대부분이 같은 빈 값입니다. v104의 RLE 계열이 그것을 압축합니다 — 다만 얼마나 압축하는지는 데이터에 달려 있고, 그것이 아래 실측의 결론입니다.
새 kind도, 새 와이어 비트도, 새 블록 배치도 없습니다. 다형 참조는 키 하나이므로 더욱 그렇습니다 — 식별자조차 파일에 실리지 않고 링킹이 산출합니다.
6.1 실측 — 형식의 수용과 행 순서의 비용
구현 없이 쟀습니다. 이 절이 제안하는 형태 — 판별자 컬럼, 기반 필드, 그리고 모든 변종 멤버를 옵셔널 컬럼으로 펼친 합집합 — 는 지금 표기로 그대로 적을 수 있는 시트입니다. 그래서 실제 인코더가 실제 제안을 상대로 낸 수입니다.
같은 데이터 2,000행, 변종당 멤버 6개입니다. 멤버의 값은 흩어진 값입니다 — 근거는 아래 「측정을 한 번 다시 한 이유」에 있습니다.
| 형태 | .tcb | 참조 대비 |
|---|---|---|
| 참조 경로 — 변종 5개가 각자 카탈로그, 그것을 가리키는 테이블 하나 | 24,997 B | 1.00배 |
| 값 임베딩 5 — 변종별로 행 이 묶여 있음 | 36,713 B | 1.47배 |
| 값 임베딩 16 — 변종별로 묶여 있음 | 42,700 B | 1.71배 |
| 값 임베딩 5 — 행마다 변종이 번갈아 나옴 | 70,155 B | 2.81배 |
| 값 임베딩 16 — 행마다 번갈아 | 86,566 B | 3.46배 |
변종의 개수보다 행의 순서가 크게 작용합니다. 변종을 5개에서 16개로 넓히면 묶인 쪽이 1.16배가 되는데, 같은 5개를 섞어 놓으면 1.91배가 됩니다.
이유는 v103의 배치입니다. 변종 멤버 컬럼은 없는 행에도 타입의 빈 값을 싣고, 그 빈 값이 이어져 있을 때만 런이 됩니다. 인코딩 보고서가 i32 멤버 컬럼 하나에 대해 그 셋을 나란히 적습니다.
| 그 컬럼이 있는 곳 | 고른 인코딩 | 컬럼 하나 |
|---|---|---|
| 참조 카탈로그 — 400행, 빈 칸 없음 | BITPACK | 854 B |
| 값 임베딩, 변종별로 묶임 — 2,000행 중 400행이 present | DELTA_RLE | 1,486 B |
| 값 임베딩, 행마다 번갈아 — 같은 400행 | RLE | 2,619 B |
묶여 있으면 없는 행 1,600개가 런 몇 개로 접히고, 번갈아 나오면 5행마다 런이 끊깁니다.
측정을 한 번 다시 한 이유
처음 잰 수는 틀렸습니다. 멤버의 정숫값 을 등차수열로 만들었더니, 카탈로그에서는 그것이
완전한 등차라 DELTA_RLE가 컬럼 하나를 4바이트로 접었고, 같은 값이 다른 변종의 행 사이에
놓인 값 임베딩에서는 등차가 아니었습니다. 두 레이아웃을 서로 다른 데이터로 비교한 것이고,
참조 쪽이 실제보다 좋게 나왔습니다(참조 10,850B · 번갈아 6.06배).
흩어진 값으로 다시 재면 어느 쪽도 그 런을 얻지 못합니다. 방향은 그대로이고 배수가 줄었습니다 — 행 순서가 4.4배가 아니라 1.9배입니다.
6.2 이 실측이 4단계에 남기는 것
| 무엇 | 결론 |
|---|---|
| 형식 개정 | 필요 없습니다. 위 넷은 전부 지금 인코더가 쓴 평범한 .tcb입니다 |
| 변종의 폭 | 16개까지 견딥니다. 상위 문서가 판정 자리로 지목한 곳이고, 묶인 데이터에서 1.71배입니다 — 5개에서 16개로 넓히는 값이 1.16배입니다 |
| 행 순서 | 판별자로 정렬합니다. 6.3절 |
| 범용 압축 | 순서를 뒤집습니다. Deflate 뒤에는 묶인 값 임베딩이 5,082B로 참조 경로의 15,169B보다 작습니다 — 없는 행이 만드는 긴 런이 압축이 가장 잘 먹는 것이기 때문입니다. 다만 그것은 모든 런타임에 압축 해제기를 들이는 별건입니다 |
값 임베딩이 확보하는 것은 크기가 아닙니다. 압축하지 않은 파일에서는 묶인 데이터에서도 참조 경로보다 크고, 그것이 사는 것은 id를 부여하지 않는 것입니다(9절).
6.3 결정 — 판별자로 정렬
다형 그룹을 가진 테이블의 행은 판별자 순으로 놓입니다. 시트에 어떤 차례로 적혀 있든 같은 파일이 나오고, 6.1절의 1.9배가 없어집니다.
| 무엇 | 결정 | 근거 |
|---|---|---|
| 어디서 | 쿠킹에서. 익스포터가 아닙니다 | 바이너리만 정렬하면 JSON과 행의 차례가 어긋납니다. 적합성 비교는 생성된 리더가 읽은 것과 JSON 익스포터가 쓴 것을 행마다 맞춰 보므로, 둘이 다른 차례를 가질 수 없습니다. 모델이 하나이면 차례도 하나입니다 |
| 무엇을 기준으로 | 판별자 값. 안정 정렬입니다 | 같은 변종 안에서는 아무것도 움직이지 않습니다 — 작성자의 차례가 그대로 남고, 변종만 모입니다. 런을 얻는 데 필요한 최소가 그것입니다 |
| 어느 테이블 | 다형 그룹을 가진 테이블만 | 지금 그런 테이블은 하나도 없으므로 골든이 한 바이트도 움직이지 않습니다. 뒤에 움직일 수 있는 것은 $type을 적어 스스로 들어온 테이블뿐입니다 |
| 행 세트 | 세트마다 따로 | 세트는 각자 자기 파일이고 자기 행을 가집니다 (행 세트) |
보이게 되는 것과 그대로인 것
| 보는 쪽 | 바뀌는가 |
|---|---|
조회 — FindByIndex 등 | 아닙니다. 키로 찾고, 키는 그대로입니다 |
| JSON 배열의 차례 | 바뀝니다 |
생성 코드의 Records 목록 | 바뀝니다 |
| 히스토리 diff | 아닙니다. 스냅샷이 행을 RowKey로 식별합니다 — 자리가 아니라 키입니다 |
작성자의 차례가 산출물의 차례가 아니게 되는 것이 이 결정이 파는 것이고, 그 대가로 시트를 어떻게 적든 같은 파일이 나옵니다. 파는 쪽이 좁은 것은 다형 그룹을 적은 테이블에 한정되기 때문입니다 — 그 표기를 쓰지 않은 테이블은 지금과 완전히 같습니다.
채택하지 않은 것 — present만 싣는 인코딩
없는 행의 값을 아예 싣지 않는 인코딩이 같은 문제를 다른 데서 풉니다. presence 비트맵이 어느 행인지 이미 말하므로, 값 블록이 present인 행만 담으면 됩니다. 6.1절의 i32 멤버 컬럼으로 재면 2,619바이트가 카탈로그와 같은 854바이트에 비트맵 250바이트가 되고, 행 순서가 무관해집니다 — 정렬보다 나은 결과입니다.
채택하지 않는 것은 비용의 등급이 다르기 때문입니다. 정렬은 쿠커의 패스 하나이고, 이것은 14번째 인코딩을 형식에 더해 15개 리더에 디코드 경로를 하나씩 늘리는 일입니다. v103이 「값은 모든 로우에 대해 기록한다」를 고른 근거가 정확히 그것이었습니다 — 「9종 × 3종의 디코드를 전부 present인 로우만 세면서 다시 쓰게 된다」.
둘은 배타적이지 않습니다. 정렬이 뒤에 이 인코딩이 필요로 할 것을 쓰지 않으므로, 값 임베딩이 실제로 쓰이고 나서 그 비트맵이 무거운 것으로 측정되면 그때 더하면 됩니다.
7. 생성 표면
원칙 하나입니다 — 판별자 하나와, 그 언어가 이미 사용하는 합 타입 표현. 언어마다 새 개념을 도입하지 않습니다.
구현이 이 표를 셋에서 다섯으로 넓혔습니다. 아래가 실제로 선 것입니다.
| 표현 | 언어 | 무엇으로 좁히는가 |
|---|---|---|
| 기반 타입 + 변종 타 입 | C# · Java · PHP · Python · Ruby · C++ | is · instanceof · isinstance · is_a? · dynamic_cast |
| 전수 검사되는 합 타입 | Kotlin(sealed class) · Swift(enum) · Dart(sealed) · Rust(enum) | when · switch · match. 빠뜨리면 컴파일되지 않습니다 |
| 구조적 합집합 | TypeScript | kind 리터럴 |
| 봉인 인터페이스 | Go | 타입 스위치. 비공개 메서드가 집합을 닫습니다 |
| 판별자 + 변종별 접근자 | C · Unreal · Lua | 좁힐 것이 없으므로 판별자를 읽고 그것이 지목한 변종을 요청합니다 |
| 초판이 틀린 곳 |
|---|
| Python · Ruby · C++를 셋째 갈래에 넣었습니다. 셋 다 상속이 있으므로 첫째입니다 |
| TypeScript와 Go를 첫째로 뭉갰습니다. 앞은 구조적 타이핑이라 클래스를 쓸 이유가 없고, 뒤는 상속이 없어 봉인 인터페이스가 됩니다 |
| 전수성을 얻는 넷을 「합 타입이 있는 언어(Rust)」 한 줄로 적었습니다. Kotlin · Swift · Dart도 같은 것을 줍니다 — 변종이 하나 늘면 모든 소비자에서 컴파일 오류가 되는 것이 이 넷의 값입니다 |
언어별 배정은 각 생성기의 기존 형태를 따라 구현 단계에서 확정했습니다 — 레코드 안의 참조가 언어를 통일하지 않기로 한 것과 같은 자리입니다.