Skip to content

THRIFT-5943: optimize python C extension readStruct for nested structs#3377

Open
markjm wants to merge 1 commit intoapache:masterfrom
markjm:py-readstruct-optimization
Open

THRIFT-5943: optimize python C extension readStruct for nested structs#3377
markjm wants to merge 1 commit intoapache:masterfrom
markjm:py-readstruct-optimization

Conversation

@markjm
Copy link
Copy Markdown
Contributor

@markjm markjm commented Apr 2, 2026

In this area of code, there already exists an optimization where we skip calling klass(**kwargs) on the top-level struct and instead init an empty object then PyObject_SetAttr the properties onto it.

This was done because Python's keyword argument matching is O(n) string comparisons per argument, making this expensive for structs with many fields.

However, nested structs which are decoded within the C extension did not get this same optimization, causing them to go through the slow class initialization path.

In this change, for mutable nested structs, we create the instance up front with a no-arg constructor (klass()) and set decoded fields directly via PyObject_SetAttr (the same way we fast-path the top-level struct).

Immutable structs (TFrozenBase subclasses) continue to use the kwargs path since their generated __setattr__ blocks attribute mutation.

Immutability is detected via (1) PyObject_IsSubclass against TFrozenBase with the class reference cached in a function-local static or (2) and overridden __setattr__.

Benchmarks show up to 3.6x speedup for deeply nested struct hierarchies, with no impact on flat structs. I had Claude generate me some benchmarks to showcase performance in different nested scenarios:

https://gist.github.com/markjm/e8610298d30d32869f46cf521272c85c

Checklist

  • Did you create an Apache Jira ticket? (Request account here, not required for trivial changes)
  • If a ticket exists: Does your pull request title follow the pattern "THRIFT-NNNN: describe my issue"?
  • Did you squash your changes to a single commit? (not required, but preferred)
  • Did you do your best to avoid breaking changes? If one was needed, did you label the Jira ticket with "Breaking-Change"?
  • [n/a] If your change does not involve any code, include [skip ci] anywhere in the commit message to free up build resources.

@mergeable mergeable bot added the python label Apr 2, 2026
@markjm markjm force-pushed the py-readstruct-optimization branch 4 times, most recently from 171c028 to 4d0914c Compare April 2, 2026 17:37
In this area of code, there already exists an optimization where we skip calling `klass(**kwargs)` on the top-level struct and instead init an empty object then PyObject_SetAttr the properties onto it.

This was done because Python's keyword
argument matching is O(n) string comparisons per argument, making this expensive for structs with many fields.

However, nested structs which are decoded within the C extension did not get this same optimization, causing them to go through the slow class initialization path.

In this change, for mutable nested structs, we create the instance up front with a no-arg constructor (klass()) and set decoded fields directly via PyObject_SetAttr (the same way we fast-path the top-level struct).

Immutable structs (TFrozenBase subclasses) continue to use the kwargs
path since their generated __setattr__ blocks attribute mutation.

Immutability is detected via PyObject_IsSubclass against TFrozenBase,
with the class reference cached in a function-local static.

Benchmarks show up to 3.6x speedup for deeply nested struct hierarchies, with no impact on flat structs. I had Claude generate me some benchmarks to showcase performance in different nested scenarios.

Note the test change. I asked about potentially changing this to look more like what the codegen produces here https://github.com/apache/thrift/pull/3349/changes#r3026288637. Currently, this fails because its not marked as frozen but has a erroring setattr
@markjm markjm force-pushed the py-readstruct-optimization branch from 4d0914c to e45a8f7 Compare April 2, 2026 17:40
@markjm
Copy link
Copy Markdown
Contributor Author

markjm commented Apr 2, 2026

Regarding:

Note the test change. I asked about potentially changing this to look more like what the codegen produces here https://github.com/apache/thrift/pull/3349/changes#r3026288637. Currently, this fails because its not marked as frozen but has a erroring setattr

the test suite alerted me to another mechanism which something could look frozen which i needed to handle by checking 2 options. with that I dont need to modify the tests

   // Immutable structs are produced by two codegen paths:
    //   1. "frozen2" mode: classes inherit from TFrozenBase
    //   2. "python.immutable" annotation: classes get a __setattr__ that raises TypeError

@markjm markjm changed the title python: optimize Python C extension readStruct for nested structs THRIFT-5943: optimize Python python C extension readStruct for nested structs Apr 2, 2026
@markjm
Copy link
Copy Markdown
Contributor Author

markjm commented Apr 2, 2026

thanks for kicking the builds @fishy - all green 🕺

@markjm markjm changed the title THRIFT-5943: optimize Python python C extension readStruct for nested structs THRIFT-5943: optimize python C extension readStruct for nested structs Apr 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant