From b0e4c7a99e4eec21c68e16576341f70d070c700b Mon Sep 17 00:00:00 2001 From: Jayson Reis Date: Wed, 9 Sep 2026 01:29:52 +0200 Subject: [PATCH 1/2] fix: remove the per-call struct format strings from the response parse path Commit 72d2aaf precompiled the request packers. That work covered the send path only. The receive path still built a format string on every call. get built '!L%ds' % (bodylen - 4). get_multi built '!L%ds%ds' % (keylen, bodylen - keylen - 4). CPython caches compiled struct formats in an LRU cache with 100 entries by default. A workload with more than 100 distinct value lengths evicts entries and recompiles on every call. A Struct for '!L%ds' is specific to one length, so precompiling one per length does not help. Both sites now read the fixed 4 byte flags field with a module-level struct.Struct('!L') and slice the rest. A slice compiles nothing. Measured on Python 3.12, parse step only, no socket: get, 500 distinct value lengths: 0.418 us -> 0.137 us (3.05x) get, 20 distinct value lengths: 0.227 us -> 0.129 us (1.77x) get, one repeated length: 0.227 us -> 0.121 us (1.87x) get_multi, 500 distinct value lengths: 0.495 us -> 0.187 us (2.65x) get_multi, one repeated length: 0.292 us -> 0.173 us (1.69x) The bytes() copy in _read_socket stays. The issue asked for a measurement and a decision. The copy is real. bytes(bytearray) costs 0.073 us at 64 bytes, 0.111 us at 1 KB, 1.80 us at 16 KB, and 17.4 us at 256 KB. Removing it changes public return types, because a bytearray slice is a bytearray: - deserialize returns the raw buffer for a value with the binary flag. get would return a bytearray in place of bytes. - get_multi uses the response key as a dict key. A bytearray is unhashable. - stats() tests "isinstance(key, bytes)" and would stop decoding the key. It would then use an unhashable bytearray as a dict key. - The error paths format extra_content into an exception message. The text changes from b'...' to bytearray(b'...'). Each of those needs its own bytes() call, which puts the copy back for the binary path and widens the change well past this issue. The acceptance criteria allow the copy to stay with a stated reason. This is the reason. Refs #276 --- bmemcached/protocol.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/bmemcached/protocol.py b/bmemcached/protocol.py index 1d3c45c..a9b1873 100644 --- a/bmemcached/protocol.py +++ b/bmemcached/protocol.py @@ -15,6 +15,11 @@ logger = logging.getLogger(__name__) +# The value body of a get response starts with a 4 byte flags field. A Struct +# for '!L%ds' would be specific to one value length, so it is not reusable. +# Read the fixed part with this, then slice the rest. A slice compiles nothing. +FLAGS_UNPACKER = struct.Struct('!L') + class Protocol(threading.local): """ @@ -475,7 +480,8 @@ def get(self, key): raise MemcachedException(f"Code: {status:d} Message: {extra_content}", status) - flags, value = struct.unpack(f"!L{bodylen - 4}s", extra_content) + flags, = FLAGS_UNPACKER.unpack_from(extra_content) + value = extra_content[4:] return self.deserialize(value, flags), cas @@ -549,7 +555,9 @@ def get_multi(self, keys): ) if status == SUCCESS: - flags, key, value = struct.unpack(f"!L{keylen}s{bodylen - keylen - 4}s", extra_content) + flags, = FLAGS_UNPACKER.unpack_from(extra_content) + key = extra_content[4:4 + keylen] + value = extra_content[4 + keylen:] d[key] = self.deserialize(value, flags), cas elif status == DISCONNECTED: From fd8178d3ef2ecfa857d18b3bd19355994c7843e1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 19 Sep 2026 19:50:40 +0000 Subject: [PATCH 2/2] style: apply ruff format to the response unpack path Match main's ruff quote and slice style on FLAGS_UNPACKER. Co-authored-by: Jayson Reis --- bmemcached/protocol.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bmemcached/protocol.py b/bmemcached/protocol.py index a9b1873..9a3b266 100644 --- a/bmemcached/protocol.py +++ b/bmemcached/protocol.py @@ -18,7 +18,7 @@ # The value body of a get response starts with a 4 byte flags field. A Struct # for '!L%ds' would be specific to one value length, so it is not reusable. # Read the fixed part with this, then slice the rest. A slice compiles nothing. -FLAGS_UNPACKER = struct.Struct('!L') +FLAGS_UNPACKER = struct.Struct("!L") class Protocol(threading.local): @@ -480,7 +480,7 @@ def get(self, key): raise MemcachedException(f"Code: {status:d} Message: {extra_content}", status) - flags, = FLAGS_UNPACKER.unpack_from(extra_content) + (flags,) = FLAGS_UNPACKER.unpack_from(extra_content) value = extra_content[4:] return self.deserialize(value, flags), cas @@ -555,9 +555,9 @@ def get_multi(self, keys): ) if status == SUCCESS: - flags, = FLAGS_UNPACKER.unpack_from(extra_content) - key = extra_content[4:4 + keylen] - value = extra_content[4 + keylen:] + (flags,) = FLAGS_UNPACKER.unpack_from(extra_content) + key = extra_content[4 : 4 + keylen] + value = extra_content[4 + keylen :] d[key] = self.deserialize(value, flags), cas elif status == DISCONNECTED: