Commit 72d2aaf
authored
fix: Minor performance improvements in hot paths (#263)
* fix: size _incr_decr key in bytes, not codepoints.
The struct format spec for incr/decr was sized as
COMMANDS[command]['struct'] % len(key), but the value packed into
that slot is keybytes (UTF-8). For a non-ASCII key the byte length
exceeds the codepoint count, the format spec under-sizes the field,
and struct.pack silently truncates the encoded key. The header
keylen advertises the full byte length, so the resulting wire packet
is shorter than the server expects -- the server blocks reading the
"missing" key bytes while the client blocks waiting for a response,
deadlocking the connection.
* fix: ensure serialize() always returns bytes.
For int/long values serialize() did value = str(value), producing a
Python 3 str. When that string is large enough to cross the
COMPRESSION_THRESHOLD it falls into self.compression.compress(value),
which rejects str with "TypeError: a bytes-like object is required".
Encode to bytes inline in the int/long branches so every assignment
to value in the type-dispatch already produces bytes; the binary,
text, and pickler branches all already do.
Reachable today via e.g. set('k', 10 ** 200): str(10 ** 200) is 201
chars, exceeds the 128-byte threshold, hits the compress branch, and
raises before any caller-side coercion runs. The downstream
text_type-to-bytes guards in _set_add_replace, set_multi, and
set_multi_cas only cover the small-value path where compression is
skipped; they cannot save the compress branch because the crash is
inside serialize() itself. Drop the now-redundant guard in
_set_add_replace.
The function's :rtype: str was already inaccurate -- the text, binary,
and pickler paths returned bytes; only int/long returned str. Update
it to bytes now that the contract actually holds across every branch.
* perf: replace COMMANDS struct format strings with precompiled packers.
Each entry in the COMMANDS table now carries a precompiled
struct.Struct for the fixed-size prefix of its wire format
(HEADER_STRUCT plus any leading "extras" bytes); variable-length
tails (key, value, auth payloads) are concatenated as bytes after
packer.pack(...).
Previously each call site built a fresh format string from
HEADER_STRUCT + the per-command 'struct' suffix and substituted in
the per-call lengths via "%". This costs a string concat plus a %
format on every call, and -- because the resulting format string
embeds the per-call lengths ('17s', '23s', ...) -- defeats the LRU
cache that the struct module keeps over compiled formats. Once the
working set of distinct lengths exceeds that cache (100 entries by
default in CPython), every call recompiles its format from scratch.
The hot build loops in get_multi, set_multi, and set_multi_cas paid
this on every key; additionally bind packer.pack and the relevant
COMMANDS / MAGIC / STATUS lookups to locals.
Microbench (500-key get_multi request build, no network): 1181us
-> 441us, ~2.7x.
* perf: use pickle.loads / pickle.dumps when self.(un)pickler is the default.
The serialize / deserialize paths construct a BytesIO and a Pickler
or Unpickler instance per call to support a user-overridable pickler
class. When self.(un)pickler is pickle.(Un)Pickler -- the default --
this is equivalent to pickle.dumps / pickle.loads, which are
implemented in C (_pickle) and skip the Python-level allocation.
Microbench (round-trip a small dict): BytesIO+Unpickler.load 3.08us
vs pickle.loads 1.59us, ~1.94x.1 parent 1d980c9 commit 72d2aaf
2 files changed
Lines changed: 220 additions & 137 deletions
0 commit comments