背景
機械学習やレイトレーシングなどで, Python と C/C++ でバイナリデータのやりとりをしたい.
Python の標準機能だけで完結したい. text なら JSON や numpy text 形式(csv)などあるが, バイナリは C++ 側でお手軽に使えるのは無い.
Pickle シリアリゼーションを検討する.
endianness も考慮されているようです.
情報
Pickle のシリアリゼーションフォーマット自体をお手軽に解説したサイトは英語でもありませんでした.
(わかってしまえば, そんなに複雑なフォーマットではないので, 解説するほどでもないのかもしれませんが...)
しかし, ありがたいことに PyTorch JIT が, TorchScript(Python ライクなスクリプト言語)を実装するうえで, 自前の C++ Pickle ローダとシリアリゼーション対応をしていて, コードが参考になります.
また, Python の Pickletools でデータの解析ができます.
フォーマット
Protocol バージョン
Pickle にはいくつか Protocol バージョンがあります. Python3 では 3 がデフォルトですが, Python3 で proto 3 でシリアライズすると, Python2 で読むことができません.
数値データメインで, あんまりへんてこでないデータを扱わないのであれば, proto 2 が推奨でしょうか.
(TorchScript では proto 2 しかサポートしていない)
ヘッダは, 0x80
(PROTO, 1 byte), バージョン番号(1 byte)の 2 bytes になります.
ためしに 1 をシリアライズしてみます.
import pickle
import io
a = 1
f = io.BytesIO()
b = pickle.dump(a, f)
w = open("bora.p", "wb")
w.write(f.getbuffer())
$ od -tx1c bora.p
0000000 80 03 4b 01 2e
200 003 K 001 .
0000005
'K' は BININT1
.
(2e) は STOP
です. データの終わりです.
pytorch jit の unpicker.cpp を見ると,
case PickleOpCode::BININT1: {
uint8_t value = read<uint8_t>();
stack_.emplace_back(int64_t(value));
} break;
とありますので, BININT1
は 1 byte でシリアライズできる int 型の値であることがわかります.
配列データを試してみます.
import pickle
import io
a = [1, 2]
f = io.BytesIO()
b = pickle.dump(a, f, protocol=2)
w = open("bora.p", "wb")
w.write(f.getbuffer())
こんどは pickletools でダンプしてみます.
$ python -m pickletools bora.p
0: \x80 PROTO 2
2: ] EMPTY_LIST
3: q BINPUT 0
5: ( MARK
6: K BININT1 1
8: K BININT1 2
10: e APPENDS (MARK at 5)
11: . STOP
highest protocol among opcodes = 2
基本的には, prefix + 実際のデータの組み合わせというかたちなので, あとは pytorch jit の pickler.cpp, unpickler.cpp や, pickletools.py を参考にいろいろ試していって解析すればよさそうです!
numpy array
numpy array(ndarray) をシリアライズしてみます.
a = numpy.array([1.0, 2.2, 3.3, 4, 5, 6, 7, 8, 9, 10], dtype=numpy.float32)
f = io.BytesIO()
b = pickle.dump(a, f, protocol=2)
w = open("bora.p", "wb")
w.write(f.getbuffer())
0: \x80 PROTO 2
2: c GLOBAL 'numpy.core.multiarray _reconstruct'
38: q BINPUT 0
40: c GLOBAL 'numpy ndarray'
55: q BINPUT 1
57: K BININT1 0
59: \x85 TUPLE1
60: q BINPUT 2
62: c GLOBAL '_codecs encode'
78: q BINPUT 3
80: X BINUNICODE 'b'
86: q BINPUT 4
88: X BINUNICODE 'latin1'
99: q BINPUT 5
101: \x86 TUPLE2
102: q BINPUT 6
104: R REDUCE
105: q BINPUT 7
107: \x87 TUPLE3
108: q BINPUT 8
110: R REDUCE
111: q BINPUT 9
113: ( MARK
114: K BININT1 1
116: K BININT1 10
118: \x85 TUPLE1
119: q BINPUT 10
121: c GLOBAL 'numpy dtype'
134: q BINPUT 11
136: X BINUNICODE 'f4'
143: q BINPUT 12
145: K BININT1 0
147: K BININT1 1
149: \x87 TUPLE3
150: q BINPUT 13
152: R REDUCE
153: q BINPUT 14
155: ( MARK
156: K BININT1 3
158: X BINUNICODE '<'
164: q BINPUT 15
166: N NONE
167: N NONE
168: N NONE
169: J BININT -1
174: J BININT -1
179: K BININT1 0
181: t TUPLE (MARK at 155)
182: q BINPUT 16
184: b BUILD
185: \x89 NEWFALSE
186: h BINGET 3
188: X BINUNICODE '\x00\x00\x80?ÍÌ\x0c@33S@\x00\x00\x80@\x00\x00\xa0@\x00\x00À@\x00\x00à@\x00\x00\x00A\x00\x00\x10A\x00\x00 A'
240: q BINPUT 17
242: h BINGET 5
244: \x86 TUPLE2
245: q BINPUT 18
247: R REDUCE
248: q BINPUT 19
250: t TUPLE (MARK at 113)
251: q BINPUT 20
253: b BUILD
254: . STOP
highest protocol among opcodes = 2
BINUNICODE あたりにバイト列で配列データが格納されているのがわかります.
numpy のソースコードあたりを解析したら自前 C++ ローダで pickle 版の numpy 配列や pytorch tensor(numpy と似た構造を持っていると想像できる)を読み込めそうですね!
(numpy ネイティブ? の NPY/NPZ はフォーマットがいくらか簡潔で, たとえば cnpy で読み書きできる https://github.com/rogersce/cnpy )
TODO
- ついでなので, 自前で Python っぽいインタプリタも実装してみたい.
- 優秀な Python 若人さまが, Pickle フォーマットを極めることにより, 人類史上最速で優秀な C++ で Picke データローダ若人さまへと昇華なされるスキーム を確立する旅に出たい.