직렬화
글 23개
MemoryPack 의 byte[] 직렬화는 GC 가 치울 배열을 매번 남기고, 스레드마다 256KB 를 쥔다
MemoryPack 의 byte[] 직렬화는 호출마다 결과 배열을 새로 만들고, 스레드마다 256KB 작업 공간을 둡니다. 큰 결과 배열은 gen2 GC 를 부르고, 작업 공간은 스레드가 사는 동안 남으니 자주 도는 경로는 재사용 버퍼로 보냅니다.
MemoryPack 은 압축 부품만 주고, 압축 경로는 개발자가 직접 짠다
MemoryPack 에는 압축 옵션이 없고 압축기와 멤버 포매터만 있습니다. 압축 경로는 개발자가 짜야 하고, 그때 압축 표시, 압축할 크기, 푸는 크기 상한, 멤버 포매터의 null 을 직접 챙겨야 합니다.
MemoryPack 의 Union 은 받는 쪽이 모르는 구현 타입을 받으면 예외를 던진다
MemoryPack 의 Union 은 인터페이스 필드에 든 구현 타입을 태그 1 B 로 알립니다. 받는 쪽이 모르는 태그와 등록하지 않은 자식 클래스는 예외이고, 양쪽 선언 타입이 어긋나면 예외 없이 빈 객체가 나옵니다.
MemoryPack 은 기본 모드에서 멤버를 더하면 옛 클라이언트가 응답을 읽지 못한다
MemoryPack 기본 모드는 멤버 수만 적어, 멤버를 더한 새 서버의 응답을 옛 클라이언트가 읽지 못합니다. VersionTolerant 로 막지만 타입 변경은 못 잡고, 순환이 생기면 스택 넘침으로 프로세스가 끝납니다.
MemoryPack 은 멤버 순서를 바꾸면 옛 클라이언트와 값이 예외 없이 섞인다
MemoryPack 의 생성 코드는 멤버를 부모부터 선언 순서대로 값만 씁니다. 서버만 순서를 바꾸거나 부모에 멤버를 더하면 옛 클라이언트와 값이 예외 없이 섞이니, 멤버는 끝에만 추가하고 순서 목록 파일을 리뷰합니다.
MemoryPack 의 포매터는 타입마다 static 필드 하나에 등록된다
MemoryPack 은 타입마다 쓸 포매터를 static 필드 하나에 둡니다. 다시 등록하면 기존 포매터를 덮어쓰고, 한 번 실패한 타입은 IsRegistered 가 true 라 등록이 건너뛰어집니다. 유니티와 함께 쓰는 모델은 두 대상으로 빌드합니다.
MemoryPack 의 리더는 조작한 패킷 하나로 서버 프로세스를 끝낼 수 있다
서버가 클라이언트 패킷을 MemoryPack 으로 바로 풀 때 생기는 네 가지 문제와 예방책입니다. 그중 둘은 try/catch 로 잡을 수 없이 프로세스를 끝내므로, 멤버 포매터나 형식 분리로 리더에 닿기 전에 막아야 합니다.
MemoryPackWriter 는 숫자를 바꾸지 않고 메모리 모양 그대로 쓴다
MemoryPackWriter 는 int 200 을 글자로 바꾸거나 줄이지 않고, C# 이 메모리에 들고 있는 C8 00 00 00 을 그대로 옮겨 적습니다. 버퍼에는 모자랄 때와 Flush 때만 알려 빠르지만, writer 를 직접 쓰면 Flush 전까지 버퍼는 0 B 입니다.
MemoryPackSerializer 는 직렬화 도중 다시 부르면 앞부분을 덮어쓴다
패킷을 직렬화하는 도중 포매터 안에서 MemoryPackSerializer.Serialize 를 또 부르면, 스레드마다 하나인 버퍼를 0번부터 다시 써서 패킷 앞부분이 덮입니다. StringBuilder 하나를 두 함수가 같이 쓰는 것과 같고, 받은 writer 에 바로 써야 합니다.
MemoryPack 은 메모리 모양을 그대로 보내서 받는 쪽 타입대로 읽힌다
.NET 서버와 유니티 클라이언트가 같은 메시지를 주고받는데 시각이 어긋나고 튜플 값이 뒤바뀝니다. MemoryPack 바이트에는 타입 표시가 거의 없고, 값이 메모리 모양 그대로 나가서 받는 쪽 타입의 메모리 모양대로 읽히기 때문입니다.
MemoryPack 은 C# 객체를 메모리 그대로 복사해 바이트로 만든다
서버와 클라이언트가 메시지를 주고받으려면 객체를 바이트로 바꿔야 합니다. MemoryPack 은 그 일을 하는 C# 과 Unity 용 라이브러리로, 코드를 소스 생성기가 만들고 값은 변환 없이 메모리 그대로 복사해 JSON 보다 몇 배 빠릅니다.
MessagePack-CSharp 는 워밍업 뒤 결과물만 할당한다
직렬화 한 번이 힙에 남기는 것은 결과 배열 하나, 역직렬화는 만들어지는 객체뿐입니다. 재사용 버퍼면 0 B 입니다. 예외는 첫 호출과, 한 번에 128 KB 넘게 요청하는 큰 원시 배열입니다.
MessagePack-CSharp 의 스트림 리더는 메시지 끝을 건너뛰어 보며 찾는다
한 스트림에 이어 붙은 메시지 사이에는 길이도 구분자도 없습니다. MessagePackStreamReader 는 TrySkip 으로 값 하나의 끝을 할당 없이 찾지만, 조각나 도착하면 매번 처음부터 다시 훑고 크기 한도도 없습니다.
MessagePack-CSharp 의 UntrustedData 는 해시 비교자와 압축 해제 한도만 바꾼다
기본값 TrustedData 에서는 240 KB 요청이 1초를 먹고 4 KB 압축 블록이 100 MB 를 할당시킵니다. UntrustedData 는 이 둘만 막고, 깊이 한도 500 은 기본값에서도 이미 켜져 있습니다.
MessagePack-CSharp 의 Union 봉투는 선언 타입이 인터페이스일 때만 붙는다
MessagePack 바이트에는 타입 이름이 없어 인터페이스 필드를 되읽을 수 없습니다. Union 은 선언 타입에만 두 칸 봉투를 씌우고, 등록 안 된 타입은 예외 없이 nil 이 됩니다. Typeless 는 스트림이 타입을 고릅니다.
MessagePack-CSharp v3 는 포매터를 컴파일할 때 C# 으로 만든다
런타임 IL 생성은 첫 호출이 느리고 NativeAOT 에서 쓸 수 없습니다. v3 는 소스 생성기가 컴파일 때 포매터를 만들지만, List<내 타입> 멤버는 여전히 동적 코드로 떨어져 NativeAOT 에서 실패합니다.
MessagePack-CSharp 는 문자열 키를 문자열로 만들지 않고 8바이트 정수로 비교한다
정수 키는 배열 칸 번호라 키가 바이트에 실리지 않고, 문자열 키는 이름이 값마다 실립니다. 읽을 때 문자열 키는 string 을 만들지 않고 8바이트씩 ulong 으로 잘라 상수와 비교해 할당이 0 입니다.
MessagePack-CSharp 는 타입마다 포매터를 정적 필드에 한 번만 정한다
직렬화마다 멤버 타입의 포매터를 묻는데도 조회는 7 ns 안팎입니다. 리졸버 체인을 타입마다 한 번 돌아 정적 제네릭 필드에 박아 두기 때문이고, 아무도 모르면 런타임에 IL 로 만듭니다.
MessagePack-CSharp 의 리더는 정수를 쓴 폭이 아니라 값으로 읽는다
1바이트로 쓴 200 을 int 로 읽으면 되고 sbyte 로 읽으면 OverflowException 입니다. MessagePackReader 는 64비트로 넓혔다가 checked 로 좁히고, 거짓 헤더와 잘린 글자도 할당 전에 막습니다.
MessagePackWriter 는 최악 크기를 먼저 잡고 쓴 만큼만 전진한다
값마다 IBufferWriter 를 부르면 가상 호출이 쌓이고, 문자열은 헤더 폭을 인코딩 전에 모릅니다. MessagePackWriter 는 최악 크기를 먼저 잡고 Span 을 쥔 채 쓴 만큼만 전진해 할당 0 으로 씁니다.
MessagePack-CSharp 의 Serialize 는 스레드마다 둔 64 KB 배열에 먼저 쓴다
byte[] 를 돌려주는 Serialize 는 결과 크기를 모르면서도 결과 배열 하나만 할당합니다. 스레드마다 둔 64 KB 스크래치에 먼저 쓰고 복사하기 때문이고, 포매터 안에서 다시 부르면 그 배열이 덮입니다.
MessagePack 포맷은 첫 바이트 하나로 종류와 길이를 말한다
MessagePack 바이트에는 전체 길이도 필드 이름도 없습니다. 첫 바이트 하나가 종류와 길이를 함께 말하고, 작은 정수와 짧은 문자열은 그 한 바이트로 끝납니다. DateTime 은 Kind 를 잃습니다.
MessagePack-CSharp 는 필드 이름 대신 번호로 객체를 바이트로 바꾼다
JSON 은 객체마다 필드 이름을 싣고 숫자를 글자로 바꿉니다. MessagePack-CSharp 는 [Key] 번호로 칸을 정하고 타입마다 전용 포매터를 두어, 같은 객체를 23 B 대신 8 B 로 두 배 안팎 빠르게 씁니다.