Skip to content

perf(stream): multi-output handles answer with a plain tuple - #762

Merged
mario4tier merged 2 commits into
devfrom
fix/stream-plain-tuple
Sep 9, 2026
Merged

mario4tier merged 2 commits into
devfrom
fix/stream-plain-tuple

Conversation

@mario4tier

@mario4tier mario4tier commented Sep 9, 2026

Copy link
Copy Markdown
Member

Multi-output streaming handles now answer with the same plain tuple the Function
API returns, instead of a per-function NamedTuple. #757 issue examples have been updated
to reflect the new plain tuple API.

Why

update() on a multi-output handle spent ~125 ns/bar inside
namedtuple.__new__, against a 6 ns C call. Measured with every candidate
compiled into one binary and interleaved, so no cross-build layout scatter:

what update() returns ns/bar x C
NamedTuple (before) 181.6 31.6x
tuple.__new__(X_Value, ...) 131.2 21.9x
cdef class 83.1 13.9x
plain tuple (this PR) 56.5 9.4x

End to end on the real builds: MACD 169.7 -> 36.7 ns/bar, BBANDS -73.4%,
STOCH -73.9%, AROON -74.6%, MINMAXINDEX -74.9%. open_and_fill is unchanged
beyond noise, correctly: one saved construction against a 700 us call.

Two reasons beyond speed. The Function API already returns a plain tuple, so
the two tiers now agree rather than differing on the same function's outputs.

Scope

23 of 201 functions are multi-output. Single-output handles are untouched
by construction: of the 201 generated stream classes, the 178 single-output
ones are byte-identical before and after, and all 23 changed classes are
multi-output.

What to review

Five hand-written files, 34 insertions and 30 deletions:

  • tools/generate_stream.py -- four edits: emit (a, b, c) at the
    update/peek/value site and at open_and_fill; stop emitting the per-function
    namedtuple; stubs emit Tuple[...] instead of a NamedTuple class
  • talib/stream.py -- drop the now-dead X_Value export loop; docstring
  • tests/test_stream.py, README.md, CHANGELOG

talib/_stream.pxi, talib/stream.pyi and talib/_ta_lib.c are generated.

Testing

1697 passed, on both commits independently.

test_open_and_fill_matches_batch loses its _fields check and gains a type
and arity check.

SUPERTREND's streaming outputs are outSupertrend/outTrend in the C header.
The committed _stream.pxi predates that rename and names them real/integer,
so its namedtuple fields disagree with abstract's output_names and
test_open_and_fill_matches_batch[SUPERTREND] fails on dev.

_ta_lib.c regenerated with Cython 3.2.8, the version that produced the
committed file.

Claude-Session: https://claude.ai/code/session_01RQsaLFjtMXaVE4UHQKVaeu
update() on a multi-output handle spent ~125 ns/bar in namedtuple.__new__
against a 6 ns C call. MACD update() goes from 182 to 57 ns/bar with every
candidate measured in one binary, and from 170 to 37 ns/bar end to end;
BBANDS, STOCH, AROON and MINMAXINDEX all land in the -73% to -75% band.

The Function API already returns a plain tuple, so the two tiers now agree.
tuple -> NamedTuple stays available later as a non-breaking upgrade, since
every tuple idiom keeps working; the reverse would not be.

Single-output handles are untouched: 178 of 178 single-output classes are
byte-identical before and after.

test_open_and_fill_matches_batch loses its _fields check and gains a type
and arity check that reaches all 23 multi-output and 178 single-output
functions; order stays pinned by the element-wise comparison above it.

Claude-Session: https://claude.ai/code/session_01RQsaLFjtMXaVE4UHQKVaeu
@mario4tier
mario4tier merged commit d55e027 into dev Sep 9, 2026
6 checks passed
@mario4tier
mario4tier deleted the fix/stream-plain-tuple branch September 9, 2026 23:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant