Skip to content

[Python] Optimize TupleCoderImpl, SequenceCoderImpl, and component size estimation - #41217

Draft
damccorm wants to merge 3 commits into
apache:masterfrom
damccorm:perf/py-tuple-and-sequence-coder-impl
Draft

damccorm wants to merge 3 commits into
apache:masterfrom
damccorm:perf/py-tuple-and-sequence-coder-impl

Conversation

@damccorm

Copy link
Copy Markdown
Contributor

Description

Optimizes AbstractComponentCoderImpl.estimate_size, TupleCoderImpl, and SequenceCoderImpl in sdks/python/apache_beam/coders/coder_impl.py and sdks/python/apache_beam/coders/coder_impl.pxd:

  1. AbstractComponentCoderImpl.get_estimated_size_and_observables: Types loop variables (i=int, last=int, num_coders=int) in coder_impl.pxd, guards if child_observables: observables += child_observables to avoid extending an empty list on every component, and forwards nested=nested from estimate_size.
  2. TupleCoderImpl._extract_components and _construct_from_components: Avoids copying tuple(value) / tuple(components) when the input is already a tuple (or list for extraction).
  3. SequenceCoderImpl.encode_to_stream: Short-circuits type(value) is tuple or type(value) is list before calling hasattr(value, "__len__").

Benchmarks & Performance Impact

  • Isolated Microbenchmark Rerun (upstream/master vs upstream/master + this PR alone): bench_18_tuple_and_sequence_coder_impl (c2feb961eef) (on branch perf/py-benchmarks-suite) — 300,000 SequenceCoderImpl.encode and TupleCoderImpl.estimate_size calls improved from 209.90 ms -> 148.70 ms (1.41x faster, -29.2%, -61.20 ms) in compiled Cython.
  • Per-Change Attribution (Most to Least Impact, 300,000 calls each in compiled Cython):
    1. Most impact (79.4% of savings, -41.63 ms): Optimizing AbstractComponentCoderImpl.get_estimated_size_and_observables (if child_observables: guard, typed loop locals, and TupleCoderImpl._extract_components tuple pass-through) — 126.00 ms -> 84.37 ms (1.49x faster, -33.0%).
    2. Least impact (20.6% of savings, -10.81 ms): Short-circuiting type(value) is tuple or type(value) is list before hasattr(value, "__len__") in SequenceCoderImpl.encode_to_stream — 63.62 ms -> 52.81 ms (1.20x faster, -17.0%).
  • End-to-End Cloud Dataflow Benchmark (core_window_row_combine): Included in run_core_window_row_combine_pipeline (c2feb961eef) (3,000,000 generated events -> 9,000,000 sliding-windowed elements on a single n2-standard-4 worker), which improved end-to-end worker stage execution time by 1.80x (-44.5%, 599.6s -> 332.9s) and reduced total billable Dataflow vCPU/Memory/PD usage by -26.1% (1,134 -> 838 vCPU-s).

Testing

  • Verified with pytest sdks/python/apache_beam/coders/coders_test.py sdks/python/apache_beam/coders/fast_coders_test.py.

@damccorm

Copy link
Copy Markdown
Contributor Author

/gemini review

@github-actions github-actions Bot added python core size/s S: 10-49 lines changed labels Nov 11, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 Review Summary

This PR introduces targeted Cython and Python fast-path optimizations for AbstractComponentCoderImpl, TupleCoderImpl, and SequenceCoderImpl, along with fixing AbstractComponentCoderImpl.estimate_size to forward the nested parameter. The changes are clean, preserve coder semantics, and are well-supported by microbenchmark and end-to-end Dataflow benchmark results.

🔍 General Feedback

  • The fast paths for tuple/list checks and the if child_observables: guard in AbstractComponentCoderImpl.get_estimated_size_and_observables translate directly into fast C-level checks in Cython without unnecessary allocations or attribute lookups.
  • One minor accidental change in coder_impl.pxd on WindowedValueCoderImpl.get_estimated_size_and_observables is noted inline.

💡 Tip: Comment /gemini review on this pull request to trigger a new Gemini code review.

cdef libc.stdint.int64_t _from_normal_time(self, libc.stdint.uint64_t value)

@cython.locals(c=CoderImpl)
@cython.locals(c=CoderImpl, i=int, last=int, num_coders=int)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low: WindowedValueCoderImpl.get_estimated_size_and_observables does not use i, last, or num_coders. It looks like this @cython.locals decorator was unintentionally updated alongside AbstractComponentCoderImpl.get_estimated_size_and_observables.

Suggested change
@cython.locals(c=CoderImpl, i=int, last=int, num_coders=int)
@cython.locals(c=CoderImpl)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 91f182a: reverted @cython.locals on WindowedValueCoderImpl.get_estimated_size_and_observables back to @cython.locals(c=CoderImpl).

@damccorm

Copy link
Copy Markdown
Contributor Author

/gemini review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 Review Summary

This PR introduces targeted Cython and hot-path optimizations to AbstractComponentCoderImpl, TupleCoderImpl, and SequenceCoderImpl in coder_impl.py and coder_impl.pxd. The changes are clean, preserve existing semantics (while fixing nested forwarding in AbstractComponentCoderImpl.estimate_size), and address the previous review feedback.

🔍 General Feedback

  • Typing the loop variables (i, last, num_coders) in AbstractComponentCoderImpl.get_estimated_size_and_observables and guarding observables += child_observables cleanly avoids unnecessary Cython object overhead and empty list extensions.
  • Fast-pathing tuple and list checks in TupleCoderImpl and SequenceCoderImpl.encode_to_stream avoids redundant allocations and attribute lookups in tight encoding/estimation loops.
  • Forwarding nested=nested in AbstractComponentCoderImpl.estimate_size ensures accurate size estimation for nested variable-length last components.

💡 Tip: Comment /gemini review on this pull request to trigger a new Gemini code review.

@codecov

codecov Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 57.63%. Comparing base (d3078c1) to head (91f182a).
⚠️ Report is 83 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master   #41217      +/-   ##
============================================
+ Coverage     57.48%   57.63%   +0.15%     
  Complexity     2492     2492              
============================================
  Files          1204     1198       -6     
  Lines        171265   171203      -62     
  Branches       2103     2103              
============================================
+ Hits          98456    98680     +224     
+ Misses        70157    69871     -286     
  Partials       2652     2652              
Flag Coverage Δ
python 80.54% <100.00%> (+0.32%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@damccorm damccorm added the ready-for-human Draft PR with all checks passing and all review comments resolved, ready for human review label Nov 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core performance python ready-for-human Draft PR with all checks passing and all review comments resolved, ready for human review size/s S: 10-49 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant