From e8ebdbb63949eda115377114806d872133fd02a1 Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Sun, 27 Sep 2026 08:26:38 +0000 Subject: [PATCH] [interop] Cache subclass checks Passing an object of a derived class to a formal base-class argument, as well as binding a method call whose 'self' is an instance of a class derived from the method's declaring class, makes the bindings check interop::IsSubclass() on every call, and the clang derivation check is not free. Memoize IsSubclass() per class pair in the interop wrapper, where the calls are already serialized by the interop lock. A class that is still incomplete can gain bases once its definition is loaded, so a negative answer is only cached when both classes are complete. For example, calling a C++ function that takes a base-class reference with a derived instance: import time import cppjit cppjit.cppdef(r""" struct SubBase { virtual ~SubBase() = default; virtual int value() const { return 1; } }; struct SubDerived : SubBase { int value() const override { return 2; } }; int take_base(const SubBase& b) { return b.value(); } """) d = cppjit.gbl.SubDerived() cppjit.gbl.take_base(d) # warm-up start = time.perf_counter() for _ in range(200000): cppjit.gbl.take_base(d) print(f"200k calls: {time.perf_counter() - start:.2f} s") This takes 0.08 s instead of 0.22 s per 200k calls in a pristine build (0.18 s with only the parent commit's caches applied; ~2x further). Release build against the pinned CppInterOp, min of 7 repetitions, interleaved A/B. An instrumented build confirms the converter queries IsSubclass() once per call on this path, so the saved cost is the clang derivation check itself. --- src/interop/interop_wrapper.cxx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/interop/interop_wrapper.cxx b/src/interop/interop_wrapper.cxx index 735e207..6545001 100644 --- a/src/interop/interop_wrapper.cxx +++ b/src/interop/interop_wrapper.cxx @@ -1132,7 +1132,20 @@ interop::TCppScope_t interop::GetBaseScope(TCppScope_t klass, bool interop::IsSubclass(TCppScope_t derived, TCppScope_t base) { std::lock_guard Lock(InterOpMutex); - return Cpp::IsSubclass(derived, base); + // Checked on every method call that receives 'self' as its first argument + // (e.g. from pythonizations and protocol slots), so memoize per class + // pair. A class that is still incomplete can gain bases once its + // definition is loaded, so a negative answer is only cached for complete + // classes. + static std::map, bool> s_subclass_cache; + const auto cacheKey = std::make_pair(derived.data, base.data); + auto cached = s_subclass_cache.find(cacheKey); + if (cached != s_subclass_cache.end()) + return cached->second; + bool result = Cpp::IsSubclass(derived, base); + if (result || (Cpp::IsComplete(derived) && Cpp::IsComplete(base))) + s_subclass_cache.emplace(cacheKey, result); + return result; } static std::set gSmartPtrTypes = {