Skip to content

Commit 5996eeb

Browse files
committed
py/persistentcode: Add a qstr window to save mpy files more efficiently.
This is an implementation of a sliding qstr window used to reduce the number of qstrs stored in a .mpy file. The window size is configured to 32 entries which takes a fixed 64 bytes (16-bits each) on the C stack when loading/saving a .mpy file. It allows to remember the most recent 32 qstrs so they don't need to be stored again in the .mpy file. The qstr window uses a simple least-recently-used mechanism to discard the least recently used qstr when the window overflows (similar to dictionary compression). This scheme only needs a single pass to save/load the .mpy file. Reduces mpy file size by about 25% with a window size of 32.
1 parent 5a2599d commit 5996eeb

4 files changed

Lines changed: 140 additions & 40 deletions

File tree

py/persistentcode.c

Lines changed: 103 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@
4141
// The current version of .mpy files
4242
#define MPY_VERSION (3)
4343

44-
// The feature flags byte encodes the compile-time config options that
44+
// Macros to encode/decode flags to/from the feature byte
45+
#define MPY_FEATURE_ENCODE_FLAGS(flags) (flags)
46+
#define MPY_FEATURE_DECODE_FLAGS(feat) ((feat) & 3)
47+
48+
// The feature flag bits encode the compile-time config options that
4549
// affect the generate bytecode.
4650
#define MPY_FEATURE_FLAGS ( \
4751
((MICROPY_OPT_CACHE_MAP_LOOKUP_IN_BYTECODE) << 0) \
@@ -68,6 +72,57 @@ STATIC int mp_small_int_bits(void) {
6872
}
6973
#endif
7074

75+
#define QSTR_WINDOW_SIZE (32)
76+
77+
typedef struct _qstr_window_t {
78+
uint16_t idx; // indexes the head of the window
79+
uint16_t window[QSTR_WINDOW_SIZE];
80+
} qstr_window_t;
81+
82+
// Push a qstr to the head of the window, and the tail qstr is overwritten
83+
STATIC void qstr_window_push(qstr_window_t *qw, qstr qst) {
84+
qw->idx = (qw->idx + 1) % QSTR_WINDOW_SIZE;
85+
qw->window[qw->idx] = qst;
86+
}
87+
88+
// Pull an existing qstr from within the window to the head of the window
89+
STATIC qstr qstr_window_pull(qstr_window_t *qw, size_t idx) {
90+
qstr qst = qw->window[idx];
91+
if (idx > qw->idx) {
92+
memmove(&qw->window[idx], &qw->window[idx + 1], (QSTR_WINDOW_SIZE - idx - 1) * sizeof(uint16_t));
93+
qw->window[QSTR_WINDOW_SIZE - 1] = qw->window[0];
94+
idx = 0;
95+
}
96+
memmove(&qw->window[idx], &qw->window[idx + 1], (qw->idx - idx) * sizeof(uint16_t));
97+
qw->window[qw->idx] = qst;
98+
return qst;
99+
}
100+
101+
#if MICROPY_PERSISTENT_CODE_LOAD
102+
103+
// Access a qstr at the given index, relative to the head of the window (0=head)
104+
STATIC qstr qstr_window_access(qstr_window_t *qw, size_t idx) {
105+
return qstr_window_pull(qw, (qw->idx + QSTR_WINDOW_SIZE - idx) % QSTR_WINDOW_SIZE);
106+
}
107+
108+
#endif
109+
110+
#if MICROPY_PERSISTENT_CODE_SAVE
111+
112+
// Insert a qstr at the head of the window, either by pulling an existing one or pushing a new one
113+
STATIC size_t qstr_window_insert(qstr_window_t *qw, qstr qst) {
114+
for (size_t idx = 0; idx < QSTR_WINDOW_SIZE; ++idx) {
115+
if (qw->window[idx] == qst) {
116+
qstr_window_pull(qw, idx);
117+
return (qw->idx + QSTR_WINDOW_SIZE - idx) % QSTR_WINDOW_SIZE;
118+
}
119+
}
120+
qstr_window_push(qw, qst);
121+
return QSTR_WINDOW_SIZE;
122+
}
123+
124+
#endif
125+
71126
typedef struct _bytecode_prelude_t {
72127
uint n_state;
73128
uint n_exc_stack;
@@ -122,12 +177,18 @@ STATIC size_t read_uint(mp_reader_t *reader) {
122177
return unum;
123178
}
124179

125-
STATIC qstr load_qstr(mp_reader_t *reader) {
180+
STATIC qstr load_qstr(mp_reader_t *reader, qstr_window_t *qw) {
126181
size_t len = read_uint(reader);
182+
if (len & 1) {
183+
// qstr in window
184+
return qstr_window_access(qw, len >> 1);
185+
}
186+
len >>= 1;
127187
char *str = m_new(char, len);
128188
read_bytes(reader, (byte*)str, len);
129189
qstr qst = qstr_from_strn(str, len);
130190
m_del(char, str, len);
191+
qstr_window_push(qw, qst);
131192
return qst;
132193
}
133194

@@ -151,20 +212,20 @@ STATIC mp_obj_t load_obj(mp_reader_t *reader) {
151212
}
152213
}
153214

154-
STATIC void load_bytecode_qstrs(mp_reader_t *reader, byte *ip, byte *ip_top) {
215+
STATIC void load_bytecode_qstrs(mp_reader_t *reader, qstr_window_t *qw, byte *ip, byte *ip_top) {
155216
while (ip < ip_top) {
156217
size_t sz;
157218
uint f = mp_opcode_format(ip, &sz);
158219
if (f == MP_OPCODE_QSTR) {
159-
qstr qst = load_qstr(reader);
220+
qstr qst = load_qstr(reader, qw);
160221
ip[1] = qst;
161222
ip[2] = qst >> 8;
162223
}
163224
ip += sz;
164225
}
165226
}
166227

167-
STATIC mp_raw_code_t *load_raw_code(mp_reader_t *reader) {
228+
STATIC mp_raw_code_t *load_raw_code(mp_reader_t *reader, qstr_window_t *qw) {
168229
// load bytecode
169230
size_t bc_len = read_uint(reader);
170231
byte *bytecode = m_new(byte, bc_len);
@@ -177,25 +238,25 @@ STATIC mp_raw_code_t *load_raw_code(mp_reader_t *reader) {
177238
extract_prelude(&ip, &ip2, &prelude);
178239

179240
// load qstrs and link global qstr ids into bytecode
180-
qstr simple_name = load_qstr(reader);
181-
qstr source_file = load_qstr(reader);
241+
qstr simple_name = load_qstr(reader, qw);
242+
qstr source_file = load_qstr(reader, qw);
182243
((byte*)ip2)[0] = simple_name; ((byte*)ip2)[1] = simple_name >> 8;
183244
((byte*)ip2)[2] = source_file; ((byte*)ip2)[3] = source_file >> 8;
184-
load_bytecode_qstrs(reader, (byte*)ip, bytecode + bc_len);
245+
load_bytecode_qstrs(reader, qw, (byte*)ip, bytecode + bc_len);
185246

186247
// load constant table
187248
size_t n_obj = read_uint(reader);
188249
size_t n_raw_code = read_uint(reader);
189250
mp_uint_t *const_table = m_new(mp_uint_t, prelude.n_pos_args + prelude.n_kwonly_args + n_obj + n_raw_code);
190251
mp_uint_t *ct = const_table;
191252
for (size_t i = 0; i < prelude.n_pos_args + prelude.n_kwonly_args; ++i) {
192-
*ct++ = (mp_uint_t)MP_OBJ_NEW_QSTR(load_qstr(reader));
253+
*ct++ = (mp_uint_t)MP_OBJ_NEW_QSTR(load_qstr(reader, qw));
193254
}
194255
for (size_t i = 0; i < n_obj; ++i) {
195256
*ct++ = (mp_uint_t)load_obj(reader);
196257
}
197258
for (size_t i = 0; i < n_raw_code; ++i) {
198-
*ct++ = (mp_uint_t)(uintptr_t)load_raw_code(reader);
259+
*ct++ = (mp_uint_t)(uintptr_t)load_raw_code(reader, qw);
199260
}
200261

201262
// create raw_code and return it
@@ -217,11 +278,14 @@ mp_raw_code_t *mp_raw_code_load(mp_reader_t *reader) {
217278
read_bytes(reader, header, sizeof(header));
218279
if (header[0] != 'M'
219280
|| header[1] != MPY_VERSION
220-
|| header[2] != MPY_FEATURE_FLAGS
221-
|| header[3] > mp_small_int_bits()) {
281+
|| MPY_FEATURE_DECODE_FLAGS(header[2]) != MPY_FEATURE_FLAGS
282+
|| header[3] > mp_small_int_bits()
283+
|| read_uint(reader, NULL) > QSTR_WINDOW_SIZE) {
222284
mp_raise_ValueError("incompatible .mpy file");
223285
}
224-
mp_raw_code_t *rc = load_raw_code(reader);
286+
qstr_window_t qw;
287+
qw.idx = 0;
288+
mp_raw_code_t *rc = load_raw_code(reader, &qw);
225289
reader->close(reader->data);
226290
return rc;
227291
}
@@ -264,10 +328,16 @@ STATIC void mp_print_uint(mp_print_t *print, size_t n) {
264328
print->print_strn(print->data, (char*)p, buf + sizeof(buf) - p);
265329
}
266330

267-
STATIC void save_qstr(mp_print_t *print, qstr qst) {
331+
STATIC void save_qstr(mp_print_t *print, qstr_window_t *qw, qstr qst) {
332+
size_t idx = qstr_window_insert(qw, qst);
333+
if (idx < QSTR_WINDOW_SIZE) {
334+
// qstr found in window, encode index to it
335+
mp_print_uint(print, idx << 1 | 1);
336+
return;
337+
}
268338
size_t len;
269339
const byte *str = qstr_data(qst, &len);
270-
mp_print_uint(print, len);
340+
mp_print_uint(print, len << 1);
271341
mp_print_bytes(print, str, len);
272342
}
273343

@@ -312,19 +382,19 @@ STATIC void save_obj(mp_print_t *print, mp_obj_t o) {
312382
}
313383
}
314384

315-
STATIC void save_bytecode_qstrs(mp_print_t *print, const byte *ip, const byte *ip_top) {
385+
STATIC void save_bytecode_qstrs(mp_print_t *print, qstr_window_t *qw, const byte *ip, const byte *ip_top) {
316386
while (ip < ip_top) {
317387
size_t sz;
318388
uint f = mp_opcode_format(ip, &sz);
319389
if (f == MP_OPCODE_QSTR) {
320390
qstr qst = ip[1] | (ip[2] << 8);
321-
save_qstr(print, qst);
391+
save_qstr(print, qw, qst);
322392
}
323393
ip += sz;
324394
}
325395
}
326396

327-
STATIC void save_raw_code(mp_print_t *print, mp_raw_code_t *rc) {
397+
STATIC void save_raw_code(mp_print_t *print, mp_raw_code_t *rc, qstr_window_t *qstr_window) {
328398
if (rc->kind != MP_CODE_BYTECODE) {
329399
mp_raise_ValueError("can only save bytecode");
330400
}
@@ -340,23 +410,23 @@ STATIC void save_raw_code(mp_print_t *print, mp_raw_code_t *rc) {
340410
extract_prelude(&ip, &ip2, &prelude);
341411

342412
// save qstrs
343-
save_qstr(print, ip2[0] | (ip2[1] << 8)); // simple_name
344-
save_qstr(print, ip2[2] | (ip2[3] << 8)); // source_file
345-
save_bytecode_qstrs(print, ip, rc->data.u_byte.bytecode + rc->data.u_byte.bc_len);
413+
save_qstr(print, qstr_window, ip2[0] | (ip2[1] << 8)); // simple_name
414+
save_qstr(print, qstr_window, ip2[2] | (ip2[3] << 8)); // source_file
415+
save_bytecode_qstrs(print, qstr_window, ip, rc->data.u_byte.bytecode + rc->data.u_byte.bc_len);
346416

347417
// save constant table
348418
mp_print_uint(print, rc->data.u_byte.n_obj);
349419
mp_print_uint(print, rc->data.u_byte.n_raw_code);
350420
const mp_uint_t *const_table = rc->data.u_byte.const_table;
351421
for (uint i = 0; i < prelude.n_pos_args + prelude.n_kwonly_args; ++i) {
352422
mp_obj_t o = (mp_obj_t)*const_table++;
353-
save_qstr(print, MP_OBJ_QSTR_VALUE(o));
423+
save_qstr(print, qstr_window, MP_OBJ_QSTR_VALUE(o));
354424
}
355425
for (uint i = 0; i < rc->data.u_byte.n_obj; ++i) {
356426
save_obj(print, (mp_obj_t)*const_table++);
357427
}
358428
for (uint i = 0; i < rc->data.u_byte.n_raw_code; ++i) {
359-
save_raw_code(print, (mp_raw_code_t*)(uintptr_t)*const_table++);
429+
save_raw_code(print, (mp_raw_code_t*)(uintptr_t)*const_table++, qstr_window);
360430
}
361431
}
362432

@@ -366,16 +436,24 @@ void mp_raw_code_save(mp_raw_code_t *rc, mp_print_t *print) {
366436
// byte version
367437
// byte feature flags
368438
// byte number of bits in a small int
369-
byte header[4] = {'M', MPY_VERSION, MPY_FEATURE_FLAGS_DYNAMIC,
439+
// uint size of qstr window
440+
byte header[4] = {
441+
'M',
442+
MPY_VERSION,
443+
MPY_FEATURE_ENCODE_FLAGS(MPY_FEATURE_FLAGS_DYNAMIC),
370444
#if MICROPY_DYNAMIC_COMPILER
371445
mp_dynamic_compiler.small_int_bits,
372446
#else
373447
mp_small_int_bits(),
374448
#endif
375449
};
376450
mp_print_bytes(print, header, sizeof(header));
451+
mp_print_uint(print, QSTR_WINDOW_SIZE);
377452

378-
save_raw_code(print, rc);
453+
qstr_window_t qw;
454+
qw.idx = 0;
455+
memset(qw.window, 0, sizeof(qw.window));
456+
save_raw_code(print, rc, &qw);
379457
}
380458

381459
// here we define mp_raw_code_save_file depending on the port

tests/import/mpy_invalid.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def open(self, path, mode):
4848
'/mod0.mpy': b'', # empty file
4949
'/mod1.mpy': b'M', # too short header
5050
'/mod2.mpy': b'M\x00\x00\x00', # bad version
51+
'/mod3.mpy': b'M\x00\x00\x00\x7f', # qstr window too large
5152
}
5253

5354
# create and mount a user filesystem

tests/import/mpy_invalid.py.exp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
mod0 ValueError incompatible .mpy file
22
mod1 ValueError incompatible .mpy file
33
mod2 ValueError incompatible .mpy file
4+
mod3 ValueError incompatible .mpy file

tools/mpy-tool.py

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,19 @@ class Config:
6363
MICROPY_LONGINT_IMPL_MPZ = 2
6464
config = Config()
6565

66+
class QStrWindow:
67+
def __init__(self, size_log2):
68+
self.window = []
69+
self.size = 1 << size_log2
70+
71+
def push(self, val):
72+
self.window = [val] + self.window[:self.size - 1]
73+
74+
def access(self, idx):
75+
val = self.window[idx]
76+
self.window = [val] + self.window[:idx] + self.window[idx + 1:]
77+
return val
78+
6679
MP_OPCODE_BYTE = 0
6780
MP_OPCODE_QSTR = 1
6881
MP_OPCODE_VAR_UINT = 2
@@ -391,11 +404,16 @@ def read_uint(f):
391404

392405
global_qstrs = []
393406
qstr_type = namedtuple('qstr', ('str', 'qstr_esc', 'qstr_id'))
394-
def read_qstr(f):
407+
def read_qstr(f, qstr_win):
395408
ln = read_uint(f)
409+
if ln & 1:
410+
# qstr in table
411+
return qstr_win.access(ln >> 1)
412+
ln >>= 1
396413
data = str_cons(f.read(ln), 'utf8')
397414
qstr_esc = qstrutil.qstr_escape(data)
398415
global_qstrs.append(qstr_type(data, qstr_esc, 'MP_QSTR_' + qstr_esc))
416+
qstr_win.push(len(global_qstrs) - 1)
399417
return len(global_qstrs) - 1
400418

401419
def read_obj(f):
@@ -417,30 +435,30 @@ def read_obj(f):
417435
else:
418436
assert 0
419437

420-
def read_qstr_and_pack(f, bytecode, ip):
421-
qst = read_qstr(f)
438+
def read_qstr_and_pack(f, bytecode, ip, qstr_win):
439+
qst = read_qstr(f, qstr_win)
422440
bytecode[ip] = qst & 0xff
423441
bytecode[ip + 1] = qst >> 8
424442

425-
def read_bytecode_qstrs(file, bytecode, ip):
443+
def read_bytecode_qstrs(file, bytecode, ip, qstr_win):
426444
while ip < len(bytecode):
427445
f, sz = mp_opcode_format(bytecode, ip)
428446
if f == 1:
429-
read_qstr_and_pack(file, bytecode, ip + 1)
447+
read_qstr_and_pack(file, bytecode, ip + 1, qstr_win)
430448
ip += sz
431449

432-
def read_raw_code(f):
450+
def read_raw_code(f, qstr_win):
433451
bc_len = read_uint(f)
434452
bytecode = bytearray(f.read(bc_len))
435453
ip, ip2, prelude = extract_prelude(bytecode)
436-
read_qstr_and_pack(f, bytecode, ip2) # simple_name
437-
read_qstr_and_pack(f, bytecode, ip2 + 2) # source_file
438-
read_bytecode_qstrs(f, bytecode, ip)
454+
read_qstr_and_pack(f, bytecode, ip2, qstr_win) # simple_name
455+
read_qstr_and_pack(f, bytecode, ip2 + 2, qstr_win) # source_file
456+
read_bytecode_qstrs(f, bytecode, ip, qstr_win)
439457
n_obj = read_uint(f)
440458
n_raw_code = read_uint(f)
441-
qstrs = [read_qstr(f) for _ in range(prelude[3] + prelude[4])]
459+
qstrs = [read_qstr(f, qstr_win) for _ in range(prelude[3] + prelude[4])]
442460
objs = [read_obj(f) for _ in range(n_obj)]
443-
raw_codes = [read_raw_code(f) for _ in range(n_raw_code)]
461+
raw_codes = [read_raw_code(f, qstr_win) for _ in range(n_raw_code)]
444462
return RawCode(bytecode, qstrs, objs, raw_codes)
445463

446464
def read_mpy(filename):
@@ -450,11 +468,13 @@ def read_mpy(filename):
450468
raise Exception('not a valid .mpy file')
451469
if header[1] != config.MPY_VERSION:
452470
raise Exception('incompatible .mpy version')
453-
feature_flags = header[2]
454-
config.MICROPY_OPT_CACHE_MAP_LOOKUP_IN_BYTECODE = (feature_flags & 1) != 0
455-
config.MICROPY_PY_BUILTINS_STR_UNICODE = (feature_flags & 2) != 0
471+
feature_byte = header[2]
472+
qw_size = read_uint(f)
473+
config.MICROPY_OPT_CACHE_MAP_LOOKUP_IN_BYTECODE = (feature_byte & 1) != 0
474+
config.MICROPY_PY_BUILTINS_STR_UNICODE = (feature_byte & 2) != 0
456475
config.mp_small_int_bits = header[3]
457-
return read_raw_code(f)
476+
qstr_win = QStrWindow(qw_size)
477+
return read_raw_code(f, qstr_win)
458478

459479
def dump_mpy(raw_codes):
460480
for rc in raw_codes:

0 commit comments

Comments
 (0)