본문으로 건너뛰기

v107 — 동적 배열 단일화

문서 목록으로

상태: 구현 완료

고정 길이 배열 kind를 형식에서 제거하고, 모든 배열이 로우마다 자기 길이를 싣도록 한 개정입니다. 디스크립터의 원소 개수 필드도 함께 사라집니다.

핵심은 한 줄입니다. 길이가 파일에 한 번만 적히면 그 길이는 생성 코드에 굳고, 그러면 컬럼을 하나 더하는 것이 데이터 패치가 아니라 코드 배포가 됩니다.


1. 문제 — 상수로 굳는 길이

v106까지 배열 kind가 둘이었습니다.

kind길이가 어디에
KindFixedArray = 1디스크립터에 한 번. 모든 로우가 같은 길이
KindVarArray = 2로우마다 counter32 하나

고정 쪽이 생성 코드에 남긴 것이 상수입니다. C#이라면 이렇게 나왔고, 모든 언어가 같은 것을 냈습니다.

internal const int Slot_N = 2;

for (int j = 0; j < Record.Slot_N; ++j) // 읽기 루프가 그 상수를 돕니다

배포된 리더는 자기가 아는 개수까지만 읽습니다. 시트에 Slot3을 더하면 파일에는 원소가 셋 실리지만, 이미 나가 있는 클라이언트는 둘만 읽고 셋째를 조용히 버립니다. 이 도구가 매니페스트와 데이터 패치 업데이터를 갖추고 「데이터만 배포할지 코드까지 배포할지」를 판정하는 것과 어긋나는 자리이고, 와이어 태그가 존재하는 이유와 같은 종류의 문제입니다 — 이름이든 개수든 코드에 굳어 있으면 데이터만으로 바꿀 수 없습니다.

2. 결정 — 배열 kind는 하나

v106v107
kind 0KindScalarKindScalar
kind 1KindFixedArrayKindArray — 로우마다 길이를 싣습니다
kind 2KindVarArray없습니다
디스크립터의 원소 개수모든 컬럼에 counter32 하나없습니다

개수 필드가 함께 사라지는 이유는 그것이 kind를 다시 적는 것이 되기 때문입니다. 남는 값이 스칼라의 1과 배열의 0 둘뿐이고, 그 둘은 kind가 이미 구별합니다.

번호를 예약하지 않는 근거

지운 kind의 번호를 비워 두지 않고 KindVarArray를 1로 당겼습니다. 번호 예약은 옛 리더가 새 파일을 옛 뜻으로 읽고도 성공하는 것을 막는 장치인데, 이 형식에서는 버전 검사가 이미 차단합니다 — 모든 런타임이 헤더의 버전을 정확 일치로 검사하고 다른 값을 거부합니다.

형식의 버전 이력이 이 구분을 그대로 적어 두었습니다. 「거부하면 버전을 올리고, 잘못 읽으면 문제」이고, 103과 105가 움직인 것이 뒤쪽 때문이었습니다. 여기서는 옛 리더가 v107 파일을 열지 못하고 멈추므로, 남는 위험이 없습니다.

3. 비용 — 컬럼당 런 하나

가변 배열이 되면 길이 스트림이 붙습니다. 그런데 그 스트림도 다른 컬럼과 똑같이 인코딩됩니다 — raw varint와 RLE 중 작은 쪽이고, TcbColumnEncoder.EncodeArray의 주석이 답을 미리 적어 두었습니다.

A column whose rows are all the same length, which is most of them, becomes one run

로우 수와 무관하게 컬럼당 몇 바이트이고, 로우마다 붙는 비용이 아닙니다.

실측 — 골든의 .tcb 73개 전량

합계9,886 → 9,639 바이트, −247 (−2.50%)
줄어든 파일64개. 디스크립터에서 컬럼마다 개수 필드가 빠집니다
늘어난 파일8개, 각 +3~+5. 고정 배열이 있던 표에 길이 런이 하나 붙습니다
그대로1개

늘어난 것보다 줄어든 것이 큽니다. 컬럼 수가 많고 배열이 적은 표에서 개수 필드의 삭제가 크게 듣고, 배열이 있는 표에서만 런 하나를 되돌려 줍니다.

4. 생성 코드

읽기 경로가 하나로 줄었습니다. 언어마다 있던 「고정 길이용」 분기가 사라지고, 이미 있던 가변 경로가 모든 배열을 담당합니다.

사라진 것남은 것
serial · serial_ref · record_serial · array · array_ref · record_arrayvar_array · var_array_ref · record_var
record_member_serialrecord_member_var — 멤버가 배열인 레코드. 멤버를 로우의 길이로 잡습니다
_N · _M 길이 상수없음 — 길이는 파일이 정합니다

남는 상수가 하나 있습니다. 배열의 배열에서 바깥 레벨의 개수입니다. 그것은 배열의 길이가 아니라 그 필드를 채우는 컬럼의 수이고, 레코드 그룹의 멤버 수가 생성 코드에 남는 것과 같은 성질입니다.

함정 하나 — 멤버가 배열인 레코드

레코드 배열과 「멤버가 배열인 레코드」를 같은 분기로 접으면, 레코드 하나인 필드에 레코드의 배열을 할당하는 코드가 나옵니다. 둘 다 「멤버이면서 배열」이지만 배열인 것이 다릅니다 — 앞은 그룹이고 뒤는 멤버입니다. C#에서 실제로 그렇게 접었고, 컴파일 게이트가 검출했습니다.

멤버가 사라진 파일 — 조용한 축소에서 소리 나는 거부로

레코드 배열은 첫 멤버가 배열을 잡고 나머지는 길이를 확인합니다. 그 「첫 멤버」는 생성 시점의 순서이므로, 파일이 그 멤버의 컬럼을 더 이상 담지 않으면 아무도 잡지 않은 배열을 다음 멤버가 확인하게 되고 그 자리에서 거부합니다.

v106까지 고정 그룹은 선언 시점에 컬럼 수만큼 잡아 두었으므로 같은 상황에서 조용히 넘어갔고, 값이 빠진 원소를 소비하는 쪽이 알 수 없었습니다. 바뀐 쪽이 낫다고 봅니다 — 멤버를 지우는 것은 스키마 스큐가 이미 재생성을 요구하는 변경이고, 리더의 다른 모든 불일치가 그렇듯 「코드를 다시 생성하거나 데이터를 다시 뽑으라」고 보고하는 편이 맞습니다. 멤버를 더하는 쪽은 그대로 안전합니다 — 모르는 태그는 건너뜁니다.

5. 게이트

형식이 바뀌므로 바이트 동일은 기준이 될 수 없습니다. 판정은 되읽기입니다.

무엇결과
변환 골든 22개통과 — 재기록했고, 그 diff가 이 개정의 검토 대상입니다
적합성 코퍼스 — 모든 언어가 같은 파일을 읽어 같은 값을 내는가통과
MAC 검사 — 값이 바뀐 사본을 각 언어가 거부하는가통과
인코딩 도달 — 코퍼스가 13개 인코딩을 전부 지나는가통과 — 디스크립터를 걷는 테스트가 개수 필드를 건너뛰고 있어 함께 고쳤습니다
C · C++ · C# · Lua · 언리얼 컴파일 게이트통과

폐기한 테스트가 하나 있습니다. FixedArrayLengthTests는 「고정 배열의 길이를 생성 코드가 정하지 않을 때 런타임이 무엇을 하는가」를 보던 것이고, 그 대상이 없어졌습니다. 그것이 지키던 성질 — 컬럼이 늘어난 파일을 거부하지 않고 읽는 것 — 은 이제 특례가 아니라 기본입니다.

6. 스키마 스큐

SchemaBaseline이 컬럼마다 기록하던 원소 개수를 함께 지웠습니다. 개수의 변화가 더는 스키마 변경이 아니기 때문입니다 — 그룹에 컬럼을 하나 더하는 것은 기존 리더가 그대로 읽는 변경이 되었고, 그것을 깨진 변경으로 보고하면 보고가 틀린 것이 됩니다. 남는 판정은 element와 kind입니다.

그리고 베이스라인 파일 자체에 버전을 세웠습니다. Kind는 와이어의 번호를 그대로 담고 있어서 그 뜻이 형식 버전에 묶여 있습니다 — v107이 배열 kind를 1로 당겼으므로, 옛 베이스라인과 대조하면 가변 배열이던 컬럼이 전부 「스칼라였는데 배열이 되었다」로 보고됩니다. 일어나지 않은 파괴적 변경입니다. 그래서 버전이 다른 베이스라인은 비교하지 않고 새로 씁니다. 잃는 것이 없습니다 — 이 파일이 보는 것은 「이미 배포된 리더가 깨지는가」인데, 형식 버전이 오르면 그 리더는 새 파일을 헤더에서 거부하므로 답이 이미 「그렇다」이고 피할 수 없습니다.