From 9db60366d53ebc5e87fd36e09613d93aabb3f20b Mon Sep 17 00:00:00 2001 From: stepan Date: Mon, 21 Sep 2026 22:02:39 +0200 Subject: [PATCH 1/2] Quicken GetMethod attribute loads --- .../src/tests/test_graalpy_opts.py | 40 ++++- .../bytecode_dsl/PBytecodeDSLRootNode.java | 146 +++++++++++++----- 2 files changed, 144 insertions(+), 42 deletions(-) diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_graalpy_opts.py b/graalpython/com.oracle.graal.python.test/src/tests/test_graalpy_opts.py index 599005d4b1..14fc6229cf 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_graalpy_opts.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_graalpy_opts.py @@ -38,6 +38,7 @@ # SOFTWARE. import sys +import types import unittest if sys.implementation.name == "graalpy" and not __graalpython__.is_forced_uncached_interpreter: @@ -140,6 +141,41 @@ def tester(d): assert_contains_bytecode(tester, "GetMethod$FastPath") + def test_get_method_module_instance_quickening(): + module = types.ModuleType("test_module") + module.func = lambda: "instance" + + def tester(mod): + return mod.func() + + for _ in range(5): + assert tester(module) == "instance" + assert_contains_bytecode(tester, "GetMethod$InstanceFastPath") + + del module.func + module.__getattr__ = lambda name: lambda: "fallback: " + name + assert tester(module) == "fallback: func" + + + @skipUnlessSingleContext + def test_get_method_instance_shadows_non_descriptor_quickening(): + class K: + func = 42 + + obj = K() + obj.func = lambda: "instance" + + def tester(o): + return o.func() + + for _ in range(5): + assert tester(obj) == "instance" + assert_contains_bytecode(tester, "GetMethod$InstanceFastPath") + + del obj.func + assert obj.func == 42 + + @skipUnlessSingleContext def test_call_nilary_method_quickening_python_function(): def callee(): @@ -304,7 +340,7 @@ def tester(o): @skipUnlessSingleContext - def test_get_method_builtin_and_pyclass_quickening(): + def test_get_method_builtin_and_pyclass(): class MyDict: def popitem(self): return 42 @@ -317,8 +353,6 @@ def tester(d): assert tester(d)[0] == i assert tester(MyDict()) == 42 - assert_contains_bytecode(tester, "GetMethod$FastPath") - if __name__ == '__main__': unittest.main() diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/bytecode_dsl/PBytecodeDSLRootNode.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/bytecode_dsl/PBytecodeDSLRootNode.java index c52729feae..85592d9b80 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/bytecode_dsl/PBytecodeDSLRootNode.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/bytecode_dsl/PBytecodeDSLRootNode.java @@ -122,6 +122,7 @@ import com.oracle.graal.python.builtins.objects.type.TpSlots; import com.oracle.graal.python.builtins.objects.type.TpSlots.GetObjectSlotsNode; import com.oracle.graal.python.builtins.objects.type.slots.TpSlotDescrGet.CallSlotDescrGet; +import com.oracle.graal.python.builtins.objects.type.slots.TpSlotDescrSet; import com.oracle.graal.python.builtins.objects.type.slots.TpSlotIterNext.CallSlotTpIterNextNode; import com.oracle.graal.python.builtins.objects.typing.PTypeAliasType; import com.oracle.graal.python.compiler.MakeTypeParamKind; @@ -265,7 +266,6 @@ import com.oracle.graal.python.runtime.sequence.storage.ObjectSequenceStorage; import com.oracle.graal.python.runtime.sequence.storage.SequenceStorage; import com.oracle.graal.python.util.ArrayBuilder; -import com.oracle.graal.python.util.InlineWeakValueProfile; import com.oracle.graal.python.util.PythonUtils; import com.oracle.truffle.api.Assumption; import com.oracle.truffle.api.CompilerAsserts; @@ -316,6 +316,7 @@ import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.DirectCallNode; import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.nodes.InvalidAssumptionException; import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.nodes.RootNode; import com.oracle.truffle.api.nodes.SlowPathException; @@ -1934,7 +1935,7 @@ public static boolean doIterator(VirtualFrame frame, LocalAccessor output, Objec } } - @Operation(storeBytecodeIndex = true) + @Operation(storeBytecodeIndex = false) @ConstantOperand(type = TruffleString.class) @ImportStatic({PythonUtils.class, PGuards.class, GetAttribute.class}) public static final class GetMethod { @@ -1955,61 +1956,128 @@ public static Object doStringFastPath(VirtualFrame frame, TruffleString name, Tr return result; } - private static boolean hasObjectOrModuleGetattro(Node inliningTarget, PythonManagedClass klass, InlineWeakValueProfile slotsValueProfile) { - TpSlots slots = slotsValueProfile.execute(inliningTarget, klass.getTpSlots()); - return GetAttribute.hasObjectOrModuleGetattro(slots); + @ForceQuickening + @Specialization(guards = { + /* static checks: */ "!hasMaterializedDict(cachedShape)", "!isBuiltin(cachedShape)", "noInstanceAttribute", "!isNoValue(result)", // + /* dynamic checks: */ "cachedShape.check(obj)"}, // + assumptions = "typeStableAssumption", // + limit = "2", excludeForUncached = true) + public static Object doFastPath(VirtualFrame frame, TruffleString name, PythonObject obj, + @Cached("obj.getShape()") Shape cachedShape, + @Cached("cachedShape.getProperty(name) == null") boolean noInstanceAttribute, + @Cached(value = "loadCacheableAttr(obj, cachedShape, name)", weak = true) Object result, + @Cached("getTypeStableAssumption(cachedShape)") Assumption typeStableAssumption) { + assert typeStableAssumption != null; + assert PythonLanguage.get(null).isSingleContext(); // implied by type stable assumption being single ctx only + assert obj.checkDictFlags(); + return result; } - @Idempotent - public static boolean isBuiltinWithObjectOrModuleGetattro(Shape cachedShape) { - return cachedShape.getDynamicType() instanceof PythonBuiltinClassType type && GetAttribute.hasObjectOrModuleGetattro(type.getSlots()); + public static Assumption getTypeStableAssumption(Shape cachedShape) { + Object type = cachedShape.getDynamicType(); + if (type instanceof PythonBuiltinClassType || type instanceof PythonBuiltinClass) { + return null; // meaning: nothing to check + } else if (type instanceof PythonClass pythonClass) { + return pythonClass.getTypeStableAssumption(); + } + // earlier guards should have ensured this + throw CompilerDirectives.shouldNotReachHere(); } - public static PythonManagedClass getManagedClassOrNull(Shape cachedShape) { - return cachedShape.getDynamicType() instanceof PythonManagedClass managedClass ? managedClass : null; + @Idempotent + public static boolean isBuiltin(Shape cachedShape) { + return cachedShape.getDynamicType() instanceof PythonBuiltinClassType || + cachedShape.getDynamicType() instanceof PythonBuiltinClass; } @ForceQuickening @Specialization(guards = { - "!hasMaterializedDict(cachedShape)", "managedClass != null || isBuiltinWithObjectOrModuleGetattro(cachedShape)", // - "cachedShape.check(obj)", "result != null"}, limit = "2", excludeForUncached = true) - public static Object doFastPath(VirtualFrame frame, - TruffleString name, PythonObject obj, - @Bind Node inliningTarget, + /* static checks: */ "!hasMaterializedDict(cachedShape)", "isBuiltin(cachedShape)", "noInstanceAttribute", "!isNoValue(result)", // + /* dynamic checks: */ "cachedShape.check(obj)"}, // + limit = "2", excludeForUncached = true) + public static Object doFastPathBuiltin(VirtualFrame frame, TruffleString name, PythonObject obj, @Cached("obj.getShape()") Shape cachedShape, - @Cached("getManagedClassOrNull(cachedShape)") PythonManagedClass managedClass, - @Cached("getPropertyGetterWithFinalAssumption(cachedShape, name)") PropertyGetter cachedPropertyGetter, - @Cached InlineWeakValueProfile slotsValueProfile, - @Cached InlinedBranchProfile hasInstanceValueBranchProfile, - @Cached LookupAttributeInMRONode.CachedKeyFastPath getMethod, - @Bind("getMethodFastPath(obj, name, inliningTarget, managedClass, cachedShape, cachedPropertyGetter, slotsValueProfile, hasInstanceValueBranchProfile, getMethod)") Object result) { + @Cached("cachedShape.getProperty(name) == null") boolean noInstanceAttribute, + @Cached(value = "loadCacheableAttr(obj, cachedShape, name)", weak = true) Object result) { assert obj.checkDictFlags(); return result; } - static Object getMethodFastPath(PythonObject obj, TruffleString name, Node inliningTarget, PythonManagedClass managedClass, Shape cachedShape, PropertyGetter cachedPropertyGetter, - InlineWeakValueProfile slotsValueProfile, InlinedBranchProfile hasInstanceValueBranchProfile, LookupAttributeInMRONode.CachedKeyFastPath getMethod) { - if (managedClass != null) { - if (!hasObjectOrModuleGetattro(inliningTarget, managedClass, slotsValueProfile)) { - return null; - } + @ForceQuickening + @Specialization(guards = { + /* static checks: */ "!hasMaterializedDict(cachedShape)", "canLoadInstanceAttr", "getter != null", // + /* dynamic checks: */ "getter.accepts(obj)"}, // + rewriteOn = {GetAttribute.FastPathBailoutException.class, InvalidAssumptionException.class}, // + limit = "2", excludeForUncached = true) + public static Object doInstanceFastPath(VirtualFrame frame, TruffleString name, PythonObject obj, + @Cached("obj.getShape()") Shape cachedShape, + @Cached("canLoadInstanceAttr(cachedShape, name)") boolean canLoadInstanceAttr, + @Cached("getTypeStableAssumption(cachedShape)") Assumption typeStableAssumption, + @Cached("getPropertyGetterWithFinalAssumption(cachedShape, name)") PropertyGetter getter) throws GetAttribute.FastPathBailoutException, InvalidAssumptionException { + assert typeStableAssumption == null || PythonLanguage.get(null).isSingleContext(); + if (typeStableAssumption != null) { + typeStableAssumption.check(); } - Object descr = getMethod.execute(inliningTarget, cachedShape.getDynamicType(), name); - if (descr == null || (descr != PNone.NO_VALUE && !MaybeBindDescriptorNode.isMethodDescriptor(descr))) { - return null; + return new BoundDescriptor(GetAttribute.getValue(getter, obj)); + } + + public static boolean canLoadInstanceAttr(Shape cachedShape, TruffleString key) { + Object klass = cachedShape.getDynamicType(); + TpSlots klassSlots = null; + if (klass instanceof PythonBuiltinClassType type) { + klassSlots = type.getSlots(); + } else if (klass instanceof PythonClass pyClass) { + klassSlots = pyClass.getTpSlots(); } - if (cachedPropertyGetter != null) { - assert obj.checkDictFlags(); - Object instanceValue = cachedPropertyGetter.get(obj); - if (instanceValue != PNone.NO_VALUE) { - hasInstanceValueBranchProfile.enter(inliningTarget); - return new BoundDescriptor(instanceValue); - } + if (klassSlots == null || !GetAttribute.hasObjectOrModuleGetattro(klassSlots)) { + return false; + } + + Object descr; + if (klass instanceof PythonBuiltinClassType type) { + descr = LookupAttributeInMRONode.findAttr(type, key); + } else { + descr = LookupAttributeInMRONode.lookupSlowPathNoSideEffects(klass, key); + } + if (descr == PNone.NO_VALUE) { + return true; } - return descr != PNone.NO_VALUE ? descr : null; + Object descrClass = GetClassNode.executeUncached(descr); + return descrClass instanceof PythonBuiltinClassType descrType && !TpSlotDescrSet.PyDescr_IsData(descrType.getSlots()); } - @Specialization(replaces = {"doStringFastPath", "doFastPath"}) + public static Object loadCacheableAttr(PythonObject object, Shape cachedShape, TruffleString key) { + assert object.checkDictFlags(); + Object klass = cachedShape.getDynamicType(); + TpSlots klassSlots = null; + if (klass instanceof PythonBuiltinClassType type) { + klassSlots = type.getSlots(); + } else if (klass instanceof PythonClass pyClass) { + klassSlots = pyClass.getTpSlots(); + } + if (klassSlots == null || !GetAttribute.hasObjectOrModuleGetattro(klassSlots)) { + return PNone.NO_VALUE; + } + + // guard should have already checked that the shape doesn't have that key + assert DynamicObject.GetNode.getUncached().execute(object, key, null) == null; + + Object descr; + if (klass instanceof PythonBuiltinClassType type) { + descr = LookupAttributeInMRONode.findAttr(type, key); + } else { + descr = LookupAttributeInMRONode.lookupSlowPathNoSideEffects(klass, key); + } + + if (MaybeBindDescriptorNode.isMethodDescriptor(descr)) { + return descr; + } + return PNone.NO_VALUE; + } + + @Specialization(replaces = {"doStringFastPath", "doFastPath", "doFastPathBuiltin", "doInstanceFastPath"}) + @ForceQuickening + @StoreBytecodeIndex public static Object doIt(VirtualFrame frame, TruffleString name, Object obj, @Bind Node inliningTarget, From 3e7d38123f26319f0a079a4842dabb398de035cd Mon Sep 17 00:00:00 2001 From: stepan Date: Tue, 22 Sep 2026 22:19:29 +0200 Subject: [PATCH 2/2] Specialize the GetMethod instrance attribute quickening to Python modules --- .../src/tests/test_graalpy_opts.py | 27 +++++++++--- .../bytecode_dsl/PBytecodeDSLRootNode.java | 44 +++---------------- 2 files changed, 27 insertions(+), 44 deletions(-) diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_graalpy_opts.py b/graalpython/com.oracle.graal.python.test/src/tests/test_graalpy_opts.py index 14fc6229cf..fca6a2660d 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_graalpy_opts.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_graalpy_opts.py @@ -47,7 +47,7 @@ def assert_contains_bytecode(fun, bytecode_str): bytecode = __graalpython__.dis(fun) - assert bytecode_str in __graalpython__.dis(fun), bytecode + assert bytecode_str in bytecode, bytecode def test_read_name_quickening_local(): @@ -141,7 +141,7 @@ def tester(d): assert_contains_bytecode(tester, "GetMethod$FastPath") - def test_get_method_module_instance_quickening(): + def test_get_method_module_quickening(): module = types.ModuleType("test_module") module.func = lambda: "instance" @@ -150,15 +150,31 @@ def tester(mod): for _ in range(5): assert tester(module) == "instance" - assert_contains_bytecode(tester, "GetMethod$InstanceFastPath") + assert_contains_bytecode(tester, "GetMethod$ModuleFastPath") del module.func module.__getattr__ = lambda name: lambda: "fallback: " + name assert tester(module) == "fallback: func" - @skipUnlessSingleContext - def test_get_method_instance_shadows_non_descriptor_quickening(): + def test_get_method_module_subclass_not_quickened(): + class ModuleSubclass(types.ModuleType): + def __getattribute__(self, name): + if name == "func": + return lambda: "override" + return super().__getattribute__(name) + + module = ModuleSubclass("test_module_subclass") + module.func = lambda: "instance" + + def tester(mod): + return mod.func() + + for _ in range(5): + assert tester(module) == "override" + + + def test_get_method_instance_not_quickened(): class K: func = 42 @@ -170,7 +186,6 @@ def tester(o): for _ in range(5): assert tester(obj) == "instance" - assert_contains_bytecode(tester, "GetMethod$InstanceFastPath") del obj.func assert obj.func == 42 diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/bytecode_dsl/PBytecodeDSLRootNode.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/bytecode_dsl/PBytecodeDSLRootNode.java index 85592d9b80..5e59c32c42 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/bytecode_dsl/PBytecodeDSLRootNode.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/bytecode_dsl/PBytecodeDSLRootNode.java @@ -122,7 +122,6 @@ import com.oracle.graal.python.builtins.objects.type.TpSlots; import com.oracle.graal.python.builtins.objects.type.TpSlots.GetObjectSlotsNode; import com.oracle.graal.python.builtins.objects.type.slots.TpSlotDescrGet.CallSlotDescrGet; -import com.oracle.graal.python.builtins.objects.type.slots.TpSlotDescrSet; import com.oracle.graal.python.builtins.objects.type.slots.TpSlotIterNext.CallSlotTpIterNextNode; import com.oracle.graal.python.builtins.objects.typing.PTypeAliasType; import com.oracle.graal.python.compiler.MakeTypeParamKind; @@ -316,7 +315,6 @@ import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.DirectCallNode; import com.oracle.truffle.api.nodes.ExplodeLoop; -import com.oracle.truffle.api.nodes.InvalidAssumptionException; import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.nodes.RootNode; import com.oracle.truffle.api.nodes.SlowPathException; @@ -2005,47 +2003,17 @@ public static Object doFastPathBuiltin(VirtualFrame frame, TruffleString name, P @ForceQuickening @Specialization(guards = { - /* static checks: */ "!hasMaterializedDict(cachedShape)", "canLoadInstanceAttr", "getter != null", // + /* static checks: */ "!canBeSpecial", "!hasMaterializedDict(cachedShape)", "isBuiltinModule(cachedShape)", "getter != null", // /* dynamic checks: */ "getter.accepts(obj)"}, // - rewriteOn = {GetAttribute.FastPathBailoutException.class, InvalidAssumptionException.class}, // + rewriteOn = GetAttribute.FastPathBailoutException.class, // limit = "2", excludeForUncached = true) - public static Object doInstanceFastPath(VirtualFrame frame, TruffleString name, PythonObject obj, + public static Object doModuleFastPath(VirtualFrame frame, TruffleString name, PythonModule obj, @Cached("obj.getShape()") Shape cachedShape, - @Cached("canLoadInstanceAttr(cachedShape, name)") boolean canLoadInstanceAttr, - @Cached("getTypeStableAssumption(cachedShape)") Assumption typeStableAssumption, - @Cached("getPropertyGetterWithFinalAssumption(cachedShape, name)") PropertyGetter getter) throws GetAttribute.FastPathBailoutException, InvalidAssumptionException { - assert typeStableAssumption == null || PythonLanguage.get(null).isSingleContext(); - if (typeStableAssumption != null) { - typeStableAssumption.check(); - } + @Cached("canBeSpecialMethod(name)") boolean canBeSpecial, + @Cached("getPropertyGetterWithFinalAssumption(cachedShape, name)") PropertyGetter getter) throws GetAttribute.FastPathBailoutException { return new BoundDescriptor(GetAttribute.getValue(getter, obj)); } - public static boolean canLoadInstanceAttr(Shape cachedShape, TruffleString key) { - Object klass = cachedShape.getDynamicType(); - TpSlots klassSlots = null; - if (klass instanceof PythonBuiltinClassType type) { - klassSlots = type.getSlots(); - } else if (klass instanceof PythonClass pyClass) { - klassSlots = pyClass.getTpSlots(); - } - if (klassSlots == null || !GetAttribute.hasObjectOrModuleGetattro(klassSlots)) { - return false; - } - - Object descr; - if (klass instanceof PythonBuiltinClassType type) { - descr = LookupAttributeInMRONode.findAttr(type, key); - } else { - descr = LookupAttributeInMRONode.lookupSlowPathNoSideEffects(klass, key); - } - if (descr == PNone.NO_VALUE) { - return true; - } - Object descrClass = GetClassNode.executeUncached(descr); - return descrClass instanceof PythonBuiltinClassType descrType && !TpSlotDescrSet.PyDescr_IsData(descrType.getSlots()); - } - public static Object loadCacheableAttr(PythonObject object, Shape cachedShape, TruffleString key) { assert object.checkDictFlags(); Object klass = cachedShape.getDynamicType(); @@ -2075,7 +2043,7 @@ public static Object loadCacheableAttr(PythonObject object, Shape cachedShape, T return PNone.NO_VALUE; } - @Specialization(replaces = {"doStringFastPath", "doFastPath", "doFastPathBuiltin", "doInstanceFastPath"}) + @Specialization(replaces = {"doStringFastPath", "doFastPath", "doFastPathBuiltin", "doModuleFastPath"}) @ForceQuickening @StoreBytecodeIndex public static Object doIt(VirtualFrame frame,