[interop] Cache types resolved through the typeof trampoline - #117
Open
guitargeek wants to merge 1 commit into
Open
guitargeek wants to merge 1 commit into
guitargeek wants to merge 1 commit into
Conversation
GetType() resolves names that Cpp::GetType() cannot handle by declaring
`using <unique id> = __typeof__(<name>);` in the interpreter. Without a
cache, every call parses and compiles code and adds one more alias to
the AST, growing the interpreter state unboundedly for repeated names.
Memoize the resolved types by name. A name that fails to resolve may
succeed once more declarations are available, so failures are retried.
Upstream motivation: in ROOT, reading a TTree array branch creates the
converter for the branch's element pointer type (e.g. "Double_t*") on
every access, so an event loop went through the trampoline for every
entry. The original reproducer:
import array
import time
import ROOT
n = array.array("i", [3])
e = array.array("d", [1.0, 2.0, 3.0])
tree = ROOT.TTree("tree", "")
tree.Branch("n", n, "n/I")
tree.Branch("e", e, "e[n]/D")
for _ in range(10000):
tree.Fill()
start = time.perf_counter()
for event in tree:
event.e # array branch: the converter for "Double_t*" is created on each access
print(f"loop over 10000 entries reading an array branch: {time.perf_counter() - start:.2f} s")
This takes 0.11 s instead of 1.66 s with this change.
On performance in cppjit proper, the honest result of validating this is
that no equivalent hot path exists here: with an instrumented build, the
full test suite (559 tests) and targeted scenarios (STL container
template instantiation, low-level views, callbacks through
std::function, operator dispatch, exception translation) produce exactly
one trampoline invocation per process, the one-time resolution of
std::exception. Spellings like "double*" are resolved by Cpp::GetType
directly. So there is no cppjit-side number to quote; the cache prevents
AST alias growth for consumers that do drive this path repeatedly, at
the cost of one map lookup per call.
vgvassilev
reviewed
Sep 27, 2026
| // new alias to the AST, so memoize the resolved types by name. A name that | ||
| // fails to resolve may succeed once more declarations are available, so | ||
| // failures are retried. | ||
| static std::map<std::string, TCppType_t> s_slow_type_cache; |
Contributor
There was a problem hiding this comment.
We can probably put as a key the const char* pointer to "__cppjit_interop_GetType_" + std::to_string(var_count++); which should exist somewhere in the AST (or even the Decl*).
aaronj0
pushed a commit
to aaronj0/root
that referenced
this pull request
Oct 1, 2026
GetType() resolves names that Cpp::GetType() cannot handle by declaring
`using <unique id> = __typeof__(<name>);` in the interpreter. Without a
cache, every call parses and compiles code and adds one more alias to
the AST, growing the interpreter state unboundedly for repeated names.
Memoize the resolved types by name. A name that fails to resolve may
succeed once more declarations are available, so failures are retried.
Upstream motivation: in ROOT, reading a TTree array branch creates the
converter for the branch's element pointer type (e.g. "Double_t*") on
every access, so an event loop went through the trampoline for every
entry. The original reproducer:
import array
import time
import ROOT
n = array.array("i", [3])
e = array.array("d", [1.0, 2.0, 3.0])
tree = ROOT.TTree("tree", "")
tree.Branch("n", n, "n/I")
tree.Branch("e", e, "e[n]/D")
for _ in range(10000):
tree.Fill()
start = time.perf_counter()
for event in tree:
event.e # array branch: the converter for "Double_t*" is created on each access
print(f"loop over 10000 entries reading an array branch: {time.perf_counter() - start:.2f} s")
This takes 0.11 s instead of 1.66 s with this change.
On performance in cppjit proper, the honest result of validating this is
that no equivalent hot path exists here: with an instrumented build, the
full test suite (559 tests) and targeted scenarios (STL container
template instantiation, low-level views, callbacks through
std::function, operator dispatch, exception translation) produce exactly
one trampoline invocation per process, the one-time resolution of
std::exception. Spellings like "double*" are resolved by Cpp::GetType
directly. So there is no cppjit-side number to quote; the cache prevents
AST alias growth for consumers that do drive this path repeatedly, at
the cost of one map lookup per call.
Matches compiler-research/cppjit#117.
aaronj0
pushed a commit
to aaronj0/root
that referenced
this pull request
Oct 1, 2026
GetType() resolves names that Cpp::GetType() cannot handle by declaring
`using <unique id> = __typeof__(<name>);` in the interpreter. Without a
cache, every call parses and compiles code and adds one more alias to
the AST, growing the interpreter state unboundedly for repeated names.
Memoize the resolved types by name. A name that fails to resolve may
succeed once more declarations are available, so failures are retried.
Upstream motivation: in ROOT, reading a TTree array branch creates the
converter for the branch's element pointer type (e.g. "Double_t*") on
every access, so an event loop went through the trampoline for every
entry. The original reproducer:
import array
import time
import ROOT
n = array.array("i", [3])
e = array.array("d", [1.0, 2.0, 3.0])
tree = ROOT.TTree("tree", "")
tree.Branch("n", n, "n/I")
tree.Branch("e", e, "e[n]/D")
for _ in range(10000):
tree.Fill()
start = time.perf_counter()
for event in tree:
event.e # array branch: the converter for "Double_t*" is created on each access
print(f"loop over 10000 entries reading an array branch: {time.perf_counter() - start:.2f} s")
This takes 0.11 s instead of 1.66 s with this change.
On performance in cppjit proper, the honest result of validating this is
that no equivalent hot path exists here: with an instrumented build, the
full test suite (559 tests) and targeted scenarios (STL container
template instantiation, low-level views, callbacks through
std::function, operator dispatch, exception translation) produce exactly
one trampoline invocation per process, the one-time resolution of
std::exception. Spellings like "double*" are resolved by Cpp::GetType
directly. So there is no cppjit-side number to quote; the cache prevents
AST alias growth for consumers that do drive this path repeatedly, at
the cost of one map lookup per call.
Matches compiler-research/cppjit#117.
aaronj0
pushed a commit
to aaronj0/root
that referenced
this pull request
Oct 1, 2026
GetType() resolves names that Cpp::GetType() cannot handle by declaring
`using <unique id> = __typeof__(<name>);` in the interpreter. Without a
cache, every call parses and compiles code and adds one more alias to
the AST, growing the interpreter state unboundedly for repeated names.
Memoize the resolved types by name. A name that fails to resolve may
succeed once more declarations are available, so failures are retried.
Upstream motivation: in ROOT, reading a TTree array branch creates the
converter for the branch's element pointer type (e.g. "Double_t*") on
every access, so an event loop went through the trampoline for every
entry. The original reproducer:
import array
import time
import ROOT
n = array.array("i", [3])
e = array.array("d", [1.0, 2.0, 3.0])
tree = ROOT.TTree("tree", "")
tree.Branch("n", n, "n/I")
tree.Branch("e", e, "e[n]/D")
for _ in range(10000):
tree.Fill()
start = time.perf_counter()
for event in tree:
event.e # array branch: the converter for "Double_t*" is created on each access
print(f"loop over 10000 entries reading an array branch: {time.perf_counter() - start:.2f} s")
This takes 0.11 s instead of 1.66 s with this change.
On performance in cppjit proper, the honest result of validating this is
that no equivalent hot path exists here: with an instrumented build, the
full test suite (559 tests) and targeted scenarios (STL container
template instantiation, low-level views, callbacks through
std::function, operator dispatch, exception translation) produce exactly
one trampoline invocation per process, the one-time resolution of
std::exception. Spellings like "double*" are resolved by Cpp::GetType
directly. So there is no cppjit-side number to quote; the cache prevents
AST alias growth for consumers that do drive this path repeatedly, at
the cost of one map lookup per call.
Matches compiler-research/cppjit#117.
aaronj0
pushed a commit
to aaronj0/root
that referenced
this pull request
Oct 2, 2026
GetType() resolves names that Cpp::GetType() cannot handle by declaring
`using <unique id> = __typeof__(<name>);` in the interpreter. Without a
cache, every call parses and compiles code and adds one more alias to
the AST, growing the interpreter state unboundedly for repeated names.
Memoize the resolved types by name. A name that fails to resolve may
succeed once more declarations are available, so failures are retried.
Upstream motivation: in ROOT, reading a TTree array branch creates the
converter for the branch's element pointer type (e.g. "Double_t*") on
every access, so an event loop went through the trampoline for every
entry. The original reproducer:
import array
import time
import ROOT
n = array.array("i", [3])
e = array.array("d", [1.0, 2.0, 3.0])
tree = ROOT.TTree("tree", "")
tree.Branch("n", n, "n/I")
tree.Branch("e", e, "e[n]/D")
for _ in range(10000):
tree.Fill()
start = time.perf_counter()
for event in tree:
event.e # array branch: the converter for "Double_t*" is created on each access
print(f"loop over 10000 entries reading an array branch: {time.perf_counter() - start:.2f} s")
This takes 0.11 s instead of 1.66 s with this change.
On performance in cppjit proper, the honest result of validating this is
that no equivalent hot path exists here: with an instrumented build, the
full test suite (559 tests) and targeted scenarios (STL container
template instantiation, low-level views, callbacks through
std::function, operator dispatch, exception translation) produce exactly
one trampoline invocation per process, the one-time resolution of
std::exception. Spellings like "double*" are resolved by Cpp::GetType
directly. So there is no cppjit-side number to quote; the cache prevents
AST alias growth for consumers that do drive this path repeatedly, at
the cost of one map lookup per call.
Matches compiler-research/cppjit#117.
aaronj0
pushed a commit
that referenced
this pull request
Oct 3, 2026
GetType() resolves names that Cpp::GetType() cannot handle by declaring
`using <unique id> = __typeof__(<name>);` in the interpreter. Without a
cache, every call parses and compiles code and adds one more alias to
the AST, growing the interpreter state unboundedly for repeated names.
Memoize the resolved types by name. A name that fails to resolve may
succeed once more declarations are available, so failures are retried.
Upstream motivation: in ROOT, reading a TTree array branch creates the
converter for the branch's element pointer type (e.g. "Double_t*") on
every access, so an event loop went through the trampoline for every
entry. The original reproducer:
import array
import time
import ROOT
n = array.array("i", [3])
e = array.array("d", [1.0, 2.0, 3.0])
tree = ROOT.TTree("tree", "")
tree.Branch("n", n, "n/I")
tree.Branch("e", e, "e[n]/D")
for _ in range(10000):
tree.Fill()
start = time.perf_counter()
for event in tree:
event.e # array branch: the converter for "Double_t*" is created on each access
print(f"loop over 10000 entries reading an array branch: {time.perf_counter() - start:.2f} s")
This takes 0.11 s instead of 1.66 s with this change.
On performance in cppjit proper, the honest result of validating this is
that no equivalent hot path exists here: with an instrumented build, the
full test suite (559 tests) and targeted scenarios (STL container
template instantiation, low-level views, callbacks through
std::function, operator dispatch, exception translation) produce exactly
one trampoline invocation per process, the one-time resolution of
std::exception. Spellings like "double*" are resolved by Cpp::GetType
directly. So there is no cppjit-side number to quote; the cache prevents
AST alias growth for consumers that do drive this path repeatedly, at
the cost of one map lookup per call.
Matches #117.
aaronj0
pushed a commit
to root-project/root
that referenced
this pull request
Oct 3, 2026
GetType() resolves names that Cpp::GetType() cannot handle by declaring
`using <unique id> = __typeof__(<name>);` in the interpreter. Without a
cache, every call parses and compiles code and adds one more alias to
the AST, growing the interpreter state unboundedly for repeated names.
Memoize the resolved types by name. A name that fails to resolve may
succeed once more declarations are available, so failures are retried.
Upstream motivation: in ROOT, reading a TTree array branch creates the
converter for the branch's element pointer type (e.g. "Double_t*") on
every access, so an event loop went through the trampoline for every
entry. The original reproducer:
import array
import time
import ROOT
n = array.array("i", [3])
e = array.array("d", [1.0, 2.0, 3.0])
tree = ROOT.TTree("tree", "")
tree.Branch("n", n, "n/I")
tree.Branch("e", e, "e[n]/D")
for _ in range(10000):
tree.Fill()
start = time.perf_counter()
for event in tree:
event.e # array branch: the converter for "Double_t*" is created on each access
print(f"loop over 10000 entries reading an array branch: {time.perf_counter() - start:.2f} s")
This takes 0.11 s instead of 1.66 s with this change.
On performance in cppjit proper, the honest result of validating this is
that no equivalent hot path exists here: with an instrumented build, the
full test suite (559 tests) and targeted scenarios (STL container
template instantiation, low-level views, callbacks through
std::function, operator dispatch, exception translation) produce exactly
one trampoline invocation per process, the one-time resolution of
std::exception. Spellings like "double*" are resolved by Cpp::GetType
directly. So there is no cppjit-side number to quote; the cache prevents
AST alias growth for consumers that do drive this path repeatedly, at
the cost of one map lookup per call.
Matches compiler-research/cppjit#117.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
GetType() resolves names that Cpp::GetType() cannot handle by declaring
using <unique id> = __typeof__(<name>);in the interpreter. Without a cache, every call parses and compiles code and adds one more alias to the AST, growing the interpreter state unboundedly for repeated names.Memoize the resolved types by name. A name that fails to resolve may succeed once more declarations are available, so failures are retried.
Upstream motivation: in ROOT, reading a TTree array branch creates the converter for the branch's element pointer type (e.g. "Double_t*") on every access, so an event loop went through the trampoline for every entry. The original reproducer:
This takes 0.11 s instead of 1.66 s with this change.
On performance in cppjit proper, the honest result of validating this is that no equivalent hot path exists here: with an instrumented build, the full test suite (559 tests) and targeted scenarios (STL container template instantiation, low-level views, callbacks through std::function, operator dispatch, exception translation) produce exactly one trampoline invocation per process, the one-time resolution of std::exception. Spellings like "double*" are resolved by Cpp::GetType directly. So there is no cppjit-side number to quote; the cache prevents AST alias growth for consumers that do drive this path repeatedly, at the cost of one map lookup per call.