You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@thrift.apache.org by "markjm (via GitHub)" <gi...@apache.org> on 2026/06/13 07:16:18 UTC
[PR] python: avoid utf8 copy during fastbinary string encode [thrift]
markjm opened a new pull request, #3595:
URL: https://github.com/apache/thrift/pull/3595
Hi - I figured I'd share a few perf optimizations we are using internally. We are still on an older thrift version (đą ), so I did use some agent magic to port these to the head of this repo. My testing was primarily on my branch - in this case, for example, we only care about p3.12+, so i added some compat for when they added PyUnicode_AsUTF8AndSize in py3.3 (impressed the repo still suports py2!)
This one is 1 of 3 PRs
---
Use PyUnicode_AsUTF8AndSize when available, with a fallback for older Python 3 releases, so UTF-8 strings can be encoded without allocating an intermediate PyBytes object.
Performance (50k iterations, warmed)
| Workload | Baseline | This commit | Speedup |
|----------|----------|-------------|---------|
| encode simple (30B, 1 string) | 0.60 us | 0.55 us | 1.09x | | encode 10-string (182B) | 1.44 us | 1.01 us | 1.43x | | encode complex (395B) | 3.02 us | 2.56 us | 1.18x |
The more string fields a struct has, the larger the gain. Decode is unchanged.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@thrift.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] python: avoid utf8 copy during fastbinary string encode [thrift]
Posted by "markjm (via GitHub)" <gi...@apache.org>.
markjm commented on code in PR #3595:
URL: https://github.com/apache/thrift/pull/3595#discussion_r3407597543
##########
lib/py/src/ext/protocol.tcc:
##########
@@ -456,18 +456,32 @@ bool ProtocolBase<Impl>::encodeValue(PyObject* value, TType type, PyObject* type
case T_STRING: {
ScopedPyObject nval;
+ Py_ssize_t len;
if (PyUnicode_Check(value)) {
+#if PY_VERSION_HEX >= 0x03030000
+ const char* str = PyUnicode_AsUTF8AndSize(value, &len);
+ if (!str) {
+ return false;
+ }
+ if (!detail::check_ssize_t_32(len)) {
+ return false;
+ }
+
+ impl()->writeI32(static_cast<int32_t>(len));
+ return writeBuffer(const_cast<char*>(str), static_cast<size_t>(len));
Review Comment:
thanks bot
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@thrift.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] THRIFT-6069: python: avoid utf8 copy during fastbinary string encode [thrift]
Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #3595:
URL: https://github.com/apache/thrift/pull/3595#discussion_r3469348936
##########
lib/py/test/thrift_TSerializer.py:
##########
@@ -70,6 +70,17 @@ def test_TCompactProtocolAccelerated(self):
factory = TCompactProtocolAcceleratedFactory()
self.verify(self.compact_serialized, factory)
+ def test_TCompactProtocolAccelerated_unicode_matches_python(self):
+ message = Message("Ă©", 42)
Review Comment:
The new test uses a non-ASCII literal ("Ă©") without an encoding cookie. This can make the test file fail to parse under Python 2 (default ASCII source encoding) and also makes it ambiguous whether `message.body` is intended to be `unicode` vs `bytes` on Py2. Prefer using an explicit unicode escape literal so the test is source-encoding-independent and always exercises the unicode path.
##########
lib/py/test/thrift_TBinaryProtocol.py:
##########
@@ -280,6 +296,28 @@ def test_TBinaryProtocol_write_read(self):
print("Assertion fail")
raise e
+ def test_accelerated_utf8_roundtrip_on_application_exception(self):
+ if not self._has_fastbinary:
+ self.skipTest("C extension not available")
+
+ original = TApplicationException(
+ type=TApplicationException.PROTOCOL_ERROR,
+ message=("snowman-\u2603-rocket-\U0001F680-" * 32),
Review Comment:
`message` is currently a plain string literal containing `\u`/`\U` escapes. On Python 2 this may fail to parse (or not produce a `unicode` object), and it also wonât reliably exercise the unicode encoding path. Make this an explicit unicode literal so the test is version-independent and consistently tests UTF-8 encoding of unicode.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@thrift.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] THRIFT-6069: python: avoid utf8 copy during fastbinary string encode [thrift]
Posted by "Jens-G (via GitHub)" <gi...@apache.org>.
Jens-G merged PR #3595:
URL: https://github.com/apache/thrift/pull/3595
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@thrift.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] THRIFT-6069: python: avoid utf8 copy during fastbinary string encode [thrift]
Posted by "Jens-G (via GitHub)" <gi...@apache.org>.
Jens-G commented on PR #3595:
URL: https://github.com/apache/thrift/pull/3595#issuecomment-4698131382
### Code review
Found 2 issues:
1. The fast path calls `impl()->writeI32(len)` to write the string length, which is correct for `BinaryProtocol` but wrong for `CompactProtocol`. `CompactProtocol::writeString` encodes the length with `writeVarint(len)` (plain unsigned varint), while `writeI32` encodes with `writeVarint(toZigZag(val))` (zigzag). The fast path will produce a malformed Compact frame for any unicode string field, and a decoder will read the wrong number of bytes.
https://github.com/apache/thrift/blob/0691570336b46b4eb5f83bc24c57012de9c68f0e/lib/py/src/ext/protocol.tcc#L469-L473
2. AI tool use is acknowledged in the PR body but neither commit contains the required `Co-Authored-By:` or `Generated-by:` label ([AGENTS.md §4](https://github.com/apache/thrift/blob/35c1a53dd6de205a4a7c23fc356f1fba490116c4/AGENTS.md#L57-L71) says "**Always** label AI-assisted commits and PRs ⊠Apply this label even when AI only generated a portion of the change").
https://github.com/apache/thrift/blob/35c1a53dd6de205a4a7c23fc356f1fba490116c4/AGENTS.md#L57-L71
đ€ Generated with [Claude Code](https://claude.ai/code)
<sub>- If this code review was useful, please react with đ. Otherwise, react with đ.</sub>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@thrift.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] THRIFT-6069: python: avoid utf8 copy during fastbinary string encode [thrift]
Posted by "Jens-G (via GitHub)" <gi...@apache.org>.
Jens-G commented on PR #3595:
URL: https://github.com/apache/thrift/pull/3595#issuecomment-4745932561
Hi, any plans to continue here?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@thrift.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] python: avoid utf8 copy during fastbinary string encode [thrift]
Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #3595:
URL: https://github.com/apache/thrift/pull/3595#discussion_r3407591011
##########
lib/py/test/thrift_TBinaryProtocol.py:
##########
@@ -22,7 +22,9 @@
import uuid
import _import_local_thrift # noqa
+from thrift.Thrift import TApplicationException
from thrift.protocol.TBinaryProtocol import TBinaryProtocol
+from thrift.protocol.TBinaryProtocol import TBinaryProtocolAcceleratedFactory
Review Comment:
Importing `TBinaryProtocolAcceleratedFactory` at module import time can make the entire test module fail to import in environments where that symbol isnât present, even though the test itself is guarded by `fastbinary` availability. Consider importing `TBinaryProtocolAcceleratedFactory` inside `setUp()` or the specific test with a `try/except ImportError` and skipping the test (or the module) when unavailable.
##########
lib/py/test/thrift_TBinaryProtocol.py:
##########
@@ -280,6 +299,26 @@ def test_TBinaryProtocol_write_read(self):
print("Assertion fail")
raise e
+ def test_accelerated_utf8_roundtrip_on_application_exception(self):
+ if not self._has_fastbinary:
+ self.skipTest("C extension not available")
+
+ original = TApplicationException(
+ type=TApplicationException.PROTOCOL_ERROR,
+ message=("snowman-\u2603-rocket-\U0001F680-" * 32),
+ )
+
+ otrans = TTransport.TMemoryBuffer()
+ oproto = TBinaryProtocolAcceleratedFactory(fallback=False).getProtocol(otrans)
+ oproto.trans.write(oproto._fast_encode(original, APPLICATION_EXCEPTION_TYPEARGS))
+
+ itrans = TTransport.TMemoryBuffer(otrans.getvalue())
+ iproto = TBinaryProtocolAcceleratedFactory(fallback=False).getProtocol(itrans)
+ decoded = iproto._fast_decode(None, iproto, APPLICATION_EXCEPTION_TYPEARGS)
Review Comment:
This test calls private APIs (`_fast_encode` / `_fast_decode`) directly. Thatâs brittle across Thrift versions and can fail even when `fastbinary` is present but the protocol instance doesnât expose these methods the same way. Consider guarding with `hasattr(oproto, \"_fast_encode\")` / `hasattr(iproto, \"_fast_decode\")` and skipping if absent, or using a public accelerated encode/decode entrypoint if the library provides one.
##########
lib/py/test/thrift_TBinaryProtocol.py:
##########
@@ -280,6 +299,26 @@ def test_TBinaryProtocol_write_read(self):
print("Assertion fail")
raise e
+ def test_accelerated_utf8_roundtrip_on_application_exception(self):
+ if not self._has_fastbinary:
+ self.skipTest("C extension not available")
+
+ original = TApplicationException(
+ type=TApplicationException.PROTOCOL_ERROR,
+ message=("snowman-\u2603-rocket-\U0001F680-" * 32),
+ )
+
+ otrans = TTransport.TMemoryBuffer()
+ oproto = TBinaryProtocolAcceleratedFactory(fallback=False).getProtocol(otrans)
+ oproto.trans.write(oproto._fast_encode(original, APPLICATION_EXCEPTION_TYPEARGS))
Review Comment:
This test calls private APIs (`_fast_encode` / `_fast_decode`) directly. Thatâs brittle across Thrift versions and can fail even when `fastbinary` is present but the protocol instance doesnât expose these methods the same way. Consider guarding with `hasattr(oproto, \"_fast_encode\")` / `hasattr(iproto, \"_fast_decode\")` and skipping if absent, or using a public accelerated encode/decode entrypoint if the library provides one.
##########
lib/py/src/ext/protocol.tcc:
##########
@@ -456,18 +456,32 @@ bool ProtocolBase<Impl>::encodeValue(PyObject* value, TType type, PyObject* type
case T_STRING: {
ScopedPyObject nval;
+ Py_ssize_t len;
if (PyUnicode_Check(value)) {
+#if PY_VERSION_HEX >= 0x03030000
+ const char* str = PyUnicode_AsUTF8AndSize(value, &len);
+ if (!str) {
+ return false;
+ }
+ if (!detail::check_ssize_t_32(len)) {
+ return false;
+ }
+
+ impl()->writeI32(static_cast<int32_t>(len));
+ return writeBuffer(const_cast<char*>(str), static_cast<size_t>(len));
Review Comment:
Using `const_cast<char*>` here is avoidable and potentially unsafe if `writeBuffer` ever writes into the buffer (even accidentally). Prefer changing `writeBuffer` to accept a `const char*` (or adding an overload) and pass `str` through without casting. If changing the signature isnât feasible, a safe alternative is to copy into a mutable buffer before calling `writeBuffer` (with the tradeoff of allocating).
##########
lib/py/test/thrift_TBinaryProtocol.py:
##########
@@ -167,6 +169,16 @@ def testField(type, data):
protocol.readStructEnd()
+APPLICATION_EXCEPTION_TYPEARGS = [
+ TApplicationException,
+ (
+ None,
+ (1, 11, "message", "UTF8", None),
+ (2, 8, "type", None, None),
+ ),
+]
Review Comment:
`*_TYPEARGS` in Thriftâs accelerated paths are typically tuples (and may be used as cache keys or assumed immutable by downstream helpers). Using a list here is more error-prone and can break if internals assume tuple semantics. Consider making this a tuple (e.g., `(..., (...))`) to match common Thrift fastbinary conventions and avoid accidental mutation between tests.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@thrift.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org