본문으로 건너뛰기

TCB v106 — 원소 presence 비트맵

문서 목록으로

상태: 구현 완료 — 변환기와 모든 런타임 · 대상 버전 106 (105를 대체)

배열의 어느 원소에 값이 있는지를 파일에 담습니다. 와이어 바이트의 비트 7이 그 사실을 선언하고, 컬럼 블록에 원소당 1비트의 비트맵이 하나 더 붙습니다. 값 블록과 인코딩 13종은 그대로입니다.

이 문서의 범위는 형식으로 한정합니다. 시트 표기(T?[] · T[]? · T?[]?)와 모델, 그리고 모든 언어가 내는 원소별 답은 원소가 없을 수 있는 배열에 있습니다.


1. 변경 근거 — 없는 원소와 빈 값의 구별

v103이 로우에 대해 답한 질문이 원소에 대해서는 답되지 않고 있었습니다. 배열의 원소는 전부 있거나 전부 없거나라고 가정되어 있었고, 실제 시트는 그렇게 쓰이지 않습니다 — 가운데가 빈 배열이 있고, 그것을 표현할 수 없어서 생긴 우회가 셋이었습니다 (그 셋).

string[]이 이 형식이 무엇을 사는지 가장 분명하게 보여줍니다. 값이 없는 원소는 빈 문자열을 차지하고, 그것은 작성자가 빈 문자열을 적었을 때와 같은 바이트입니다. 4절의 파일에서 그 두 경우가 나란히 나옵니다.

2. 와이어 바이트 — 비트 7

7 6 5 4 3 2 1 0
[ ?[] ] [?] [kind] [ element ]

v106이 쓰기 시작한 비트
마스크의미
0x0F엘리먼트 타입
0x30kind (스칼라 · 고정 배열 · 가변 배열)
0x40로우 presence — 블록 앞에 로우당 1비트 (TcbFormat.WireNullable, v103)
0x80원소 presence — 그 뒤에 원소당 1비트 (TcbFormat.WireElementNullable)

두 비트는 직교합니다. int?[]?는 둘을 함께 켜고 비트맵을 둘 갖습니다. kind 값을 쓰지 않은 이유는 v103과 같습니다 — 원소 nullability는 kind와 직교하고, 남은 kind 값 하나는 진짜 새로운 종류를 위한 것입니다. 이제 이 바이트에 예약 비트가 없습니다.

연번 컬럼이 접힌 배열은 비트 7만 켭니다. 그 그룹에는 배열을 가리키는 셀이 없어서 배열 자체의 없음이라는 것이 없기 때문입니다 — 그 이유. 4절의 컬럼이 그 경우입니다.

3. 블록 배치 — 비트맵 둘, 값은 그대로

블록: [비트 6이면 로우 presence: 인코딩 1바이트 + 비트맵]
[비트 7이면 원소 presence: counter32 원소 총수 + 인코딩 1바이트 + 비트맵]
encoding이 정한 배치로 담긴 값들
규칙근거
원소 총수를 비트맵 앞에 counter32로 적습니다가변 배열의 총수는 로우 길이의 합이고, 그 길이들은 값 블록 안에 있습니다. 비트맵을 먼저 만난 리더에게는 크기를 잴 것이 없습니다. 컬럼당 최대 5바이트
원소 비트맵은 로우 비트맵 , 값 리더가 만나는 순서가 그 순서입니다 — 로우가 배열을 갖는지, 그 배열의 어느 자리가 값을 갖는지, 그리고 값
비트맵은 자기 인코딩 바이트를 답니다로우 비트맵이 v105에서 그렇게 되었고, 같은 선택을 받습니다
값은 모든 원소에 대해 기록합니다. 없는 원소는 타입의 빈 값을 차지합니다v103이 로우에 대해 내린 것과 같은 결정입니다. 그래서 인코딩 13종의 디코드 경로가 하나도 바뀌지 않았습니다 — 배열은 길이 스트림과 원소 스트림을 따로 인코딩하는데, 그 둘 중 어느 것도 비트맵을 보지 않습니다
byteLength는 비트맵 둘을 포함한 블록의 총 바이트입니다모르는 컬럼 건너뛰기가 advance(byteLength) 한 번이라는 불변식이 유지됩니다
원소가 필수인 컬럼에는 비트맵이 없습니다전부 1인 비트맵은 원소당 1비트를 내고 아무것도 나타내지 않습니다

비트맵의 길이는 실제로 기록된 원소의 총수입니다 — 고정 배열이면 rowCount × count, 가변 배열이면 로우 길이의 합입니다. 배열이 없는 로우는 원소를 하나도 쓰지 않으므로 비트도 내지 않습니다. 리더는 이미 길이를 순서대로 읽으므로, 늘어나는 것은 모든 로우의 모든 원소마다 한 번 증가하는 카운터 하나입니다.

4. 실제 파일 한 개 — nullable-elements 골든의 Folded.tcb

4행 2컬럼, 88바이트. Tag1·Tag2·Tag3가 접힌 string?[] 하나이고, 로우마다 다른 자리가 비어 있습니다.

로우시트읽히는 것
1a b c["a","b","c"]
2a - c["a", null, "c"]
3- b c[null, "b", "c"]
4a (빈 칸) c["a", "", "c"]
54 43 42 00 magic "TCB\0" ── 헤더 42바이트 ──
6a 00 00 00 version 106
00 flags
00 cipher
00 × 12 nonce
00 × 16 mac
54 43 42 00 keyCheck
08 rowCount counter32, 지그재그 8 → 4
04 columnCount counter32, 지그재그 4 → 2

디스크립터 둘입니다.

tagwire해석encodingcountbyteLength
102i32 · 스칼라 · 필수DELTA_RLE13
296string · 고정 배열 · 로우 필수 · 원소 옵셔널RAW325

96이 이 문서의 바이트입니다 — 1001_0110이므로 엘리먼트 6(string), kind 1(고정 배열), 비트 6은 0, 비트 7은 1입니다. 접힌 배열이므로 로우 비트맵은 없고 원소 비트맵만 있습니다.

tag 2 (string[3], 25B)

18 원소 총수 counter32, 지그재그 24 → 12 (4행 × 3원소)
00 비트맵 인코딩 RAW
af 0f 비트맵 12비트, 하위 비트부터

af = 1010_1111 0f = 0000_1111
│││││││└ 원소 0 로우1 "a" 1
││││││└─ 원소 1 로우1 "b" 1
│││││└── 원소 2 로우1 "c" 1
││││└─── 원소 3 로우2 "a" 1
│││└──── 원소 4 로우2 - 0 ← 없음
││└───── 원소 5 로우2 "c" 1
│└────── 원소 6 로우3 - 0 ← 없음
└─────── 원소 7 로우3 "b" 1
원소 8 로우3 "c" 1
원소 9 로우4 "a" 1
원소 10 로우4 "" 1 ← 빈 칸은 값
원소 11 로우4 "c" 1

02 61 02 62 02 63 로우 1: "a" "b" "c"
02 61 00 02 63 로우 2: "a" "" "c"
00 02 62 02 63 로우 3: "" "b" "c"
02 61 00 02 63 로우 4: "a" "" "c"

로우 2·3·4의 00이 이 형식이 산 것입니다. 없는 원소(로우 2의 원소 4, 로우 3의 원소 6)와 작성자가 빈 문자열을 적은 원소(로우 4의 원소 10)가 값 블록에서 같은 한 바이트입니다. 그 셋을 가르는 것은 비트맵뿐이고, 비트맵이 없으면 파일에 그 구별이 없습니다.

로우 2와 로우 3도 값만으로는 같습니다 — 둘 다 "a"·""·"c"가 아니라, 한쪽은 "a" "" "c"이고 다른 쪽은 "" "b" "c"이므로 여기서는 값이 다릅니다. 그러나 비트맵이 하는 일은 그 차이가 어느 자리의 없음인지를 나타내는 것입니다: 로우 3은 원소 0이 없고, 그것이 "b"·"c"에 대해 아무 말도 하지 않는다는 사실이 이 비트맵으로 처음 표현됩니다.

5. 버전 — 105 파일의 거부

버전배포대체 사유
103미배포인코딩 4종 추가와 flags → 104
104미배포인코딩 1종 추가와 로우 비트맵의 인코딩 바이트 → 105
105미배포컬럼이 원소별 값 유무를 담게 됨 → 106

비트 7을 검사하지 않는 105 리더는 원소 비트맵을 값 블록의 앞부분으로 읽습니다. 감지되지 않는 읽기 오류이므로, v103이 그랬던 것처럼 버전으로 거부합니다.

table format version 105 is not supported (expected 106)

원소 옵셔널 컬럼이 없는 파일은 버전 4바이트만 달라집니다 — 비트 7이 꺼진 컬럼의 블록은 v105와 한 바이트도 다르지 않습니다.

6. 담지 않는 것

무엇
레코드 멤버의 원소별 presence레코드 배열이 표현하는 없음은 원소 개수이고, 그것은 비트맵이 아니라 길이입니다
값의 압축없는 원소도 빈 값을 차지합니다. 압축하면 인코딩 13종 × kind 3종의 디코드를 「present인 원소만 세면서」 다시 쓰게 되고, 그것을 모든 언어에서 반복합니다. 원소당 1비트를 내고 그 작업을 사지 않았습니다
바깥 레벨의 길이배열의 배열에서 바깥 길이는 「컬럼이 몇 개인가」이고, 파일은 그것을 숫자가 아니라 별개의 태그들로 표현합니다

7. 검증 게이트

무엇어떻게
비트 6·7이 따로 켜지는 것NullableArrayElementTests.The_file_declares_the_two_bitmaps_separately — 파일에서 디스크립터를 직접 읽습니다
C#·TypeScript의 왕복파일을 읽어 익스포터가 같은 시트에서 쓴 JSON과 대조합니다 (cs-check-nullable-elements · ts-check-nullable-elements)
Python·Ruby·PHP읽어서 값과 존재 여부를 직접 확인합니다
나머지 언어생성 코드의 컴파일. 언리얼은 UHT가 헤더를 받는지까지
nullable-elements 골든모든 언어와 json·binary. 표기 4종과 string?[]이 한 테이블에 있습니다

8. 비용

무엇얼마
원소 옵셔널 컬럼원소당 1비트 + 총수 counter32(최대 5바이트) + 인코딩 1바이트
그 밖의 컬럼0 — 비트 7이 꺼져 있으면 블록은 v105와 같습니다
파일 전체버전 4바이트
모든 런타임컬럼 구조체에 bool 한 칸, ReadElementPresence 하나, CheckColumn의 비교 한 줄