[pypy-commit] lang-smalltalk storage-refactoring-virtual-pc: Implemented refactoring to handle the pc in a virtualized way, instead of maintining it inside the context object.

anton_gulenko noreply at buildbot.pypy.org
Wed May 14 14:51:15 CEST 2014


Author: Anton Gulenko <anton.gulenko at googlemail.com>
Branch: storage-refactoring-virtual-pc
Changeset: r827:1900ca207c39
Date: 2014-05-14 14:36 +0200
http://bitbucket.org/pypy/lang-smalltalk/changeset/1900ca207c39/

Log:	Implemented refactoring to handle the pc in a virtualized way,
	instead of maintining it inside the context object. See changes
	Interpreter.loop_bytecodes() for the main impact of this
	refactoring. Everything else is just to make that work.

	bytecode implementations get the pc and return the modified pc. A
	ContextSwitchException causes the pc to be stored into the context
	object, so it can be resumed later. This might break Smalltalk
	semantics at some places, but we hope this improves virtualizability
	of frame objects.

diff --git a/spyvm/interpreter.py b/spyvm/interpreter.py
--- a/spyvm/interpreter.py
+++ b/spyvm/interpreter.py
@@ -77,64 +77,59 @@
                 s_new_context = s_sender
                 while s_new_context is not nlr.s_target_context:
                     s_sender = s_new_context.s_sender()
-                    s_new_context._activate_unwind_context(self)
+                    s_new_context._activate_unwind_context(self, s_new_context.pc())
                     s_new_context = s_sender
                 s_new_context.push(nlr.value)
             except ProcessSwitch, p:
                 if self.trace:
                     print "====== Switched process from: %s" % s_new_context.short_str()
                     print "====== to: %s " % p.s_new_context.short_str()
+                # TODO Idea: check how process switches affect performance. The tracing should not
+                # continue when the process is changed (it's probably rare to have the exact same interleaving
+                # of multiple processes). How do we make sure that a bridge is created here?
                 s_new_context = p.s_new_context
 
-    def loop_bytecodes(self, s_context, may_context_switch=True):
-        old_pc = 0
+    def loop_bytecodes(self, s_context, fresh_context=False, may_context_switch=True):
         if not jit.we_are_jitted() and may_context_switch:
             self.quick_check_for_interrupt(s_context)
         method = s_context.w_method()
+        if jit.promote(fresh_context):
+            pc = 0
+        else:
+            pc = s_context.pc()
         while True:
-            pc = s_context.pc()
+            self.jit_driver.jit_merge_point(pc=pc, self=self, method=method, s_context=s_context)
+            old_pc = pc
+            pc = self.step(s_context, pc)
             if pc < old_pc:
                 if jit.we_are_jitted():
                     self.jitted_check_for_interrupt(s_context)
-                self.jit_driver.can_enter_jit(
-                    pc=pc, self=self, method=method,
-                    s_context=s_context)
-            old_pc = pc
-            self.jit_driver.jit_merge_point(
-                pc=pc, self=self, method=method,
-                s_context=s_context)
-            try:
-                self.step(s_context)
-            except Return, nlr:
-                if nlr.s_target_context is not s_context:
-                    s_context._activate_unwind_context(self)
-                    raise nlr
-                else:
-                    s_context.push(nlr.value)
+                self.jit_driver.can_enter_jit(pc=pc, self=self, method=method, s_context=s_context)
     
     # This is just a wrapper around loop_bytecodes that handles the remaining_stack_depth mechanism
-    def stack_frame(self, s_new_frame, may_context_switch=True):
+    def stack_frame(self, s_new_frame, may_context_switch=True, fresh_context=False):
         if self.remaining_stack_depth <= 1:
             raise StackOverflow(s_new_frame)
 
         self.remaining_stack_depth -= 1
         try:
-            self.loop_bytecodes(s_new_frame, may_context_switch)
+            self.loop_bytecodes(s_new_frame, may_context_switch=may_context_switch, fresh_context=fresh_context)
         finally:
             self.remaining_stack_depth += 1
     
-    def step(self, context):
-        bytecode = context.fetch_next_bytecode()
+    def step(self, context, pc):
+        bytecode = context.fetch_bytecode(pc)
+        pc += 1
         for entry in UNROLLING_BYTECODE_RANGES:
             if len(entry) == 2:
                 bc, methname = entry
                 if bytecode == bc:
-                    return getattr(context, methname)(self, bytecode)
+                    return getattr(context, methname)(self, bytecode, pc)
             else:
                 start, stop, methname = entry
                 if start <= bytecode <= stop:
-                    return getattr(context, methname)(self, bytecode)
-        assert 0, "unreachable"
+                    return getattr(context, methname)(self, bytecode, pc)
+        assert False, "unreachable"
     
     # ============== Methods for handling user interrupts ==============
     
@@ -241,27 +236,52 @@
 
 # This is a decorator for bytecode implementation methods.
 # parameter_bytes=N means N additional bytes are fetched as parameters.
-def bytecode_implementation(parameter_bytes=0):
+# jump=True means the pc is changed in an unpredictable way.
+#           The implementation method must additionally handle the pc.
+# needs_pc=True means the bytecode implementation required the pc, but will not change it.
+def bytecode_implementation(parameter_bytes=0, jump=False, needs_pc=False):
     def bytecode_implementation_decorator(actual_implementation_method):
         from rpython.rlib.unroll import unrolling_zero
         @jit.unroll_safe
-        def bytecode_implementation_wrapper(self, interp, current_bytecode):
+        def bytecode_implementation_wrapper(self, interp, current_bytecode, pc):
             parameters = ()
             i = unrolling_zero
             while i < parameter_bytes:
-                parameters += (self.fetch_next_bytecode(), )
+                parameters += (self.fetch_bytecode(pc), )
+                pc += 1
                 i = i + 1
+            if jump or needs_pc:
+                parameters += (pc, )
             # This is a good place to step through bytecodes.
             # import pdb; pdb.set_trace()
-            return actual_implementation_method(self, interp, current_bytecode, *parameters)
+            try:
+                jumped_pc = actual_implementation_method(self, interp, current_bytecode, *parameters)
+                if jump:
+                    return jumped_pc
+                else:
+                    return pc
+            except ContextSwitchException:
+                # We are returning to the loop() method, so the virtualized pc variable must be written to the context
+                # objects.
+                # This can be very bad for performance since it forces the jit to heap-allocate virtual objects.
+                # Bytecodes with jump=True cannot cause any Exception, so we can safely store pc (and not jumped_pc).
+                self.store_pc(pc)
+                raise
+            except Return, ret:
+                if ret.s_target_context is not self:
+                    self._activate_unwind_context(interp, pc)
+                    raise ret
+                else:
+                    self.push(ret.value)
+                    return pc
         bytecode_implementation_wrapper.func_name = actual_implementation_method.func_name
         return bytecode_implementation_wrapper
     return bytecode_implementation_decorator
 
 def make_call_primitive_bytecode(primitive, selector, argcount, store_pc=False):
     func = primitives.prim_table[primitive]
-    @bytecode_implementation()
-    def callPrimitive(self, interp, current_bytecode):
+    @bytecode_implementation(needs_pc=True)
+    def callPrimitive(self, interp, current_bytecode, pc):
         # WARNING: this is used for bytecodes for which it is safe to
         # directly call the primitive.  In general, it is not safe: for
         # example, depending on the type of the receiver, bytecodePrimAt
@@ -270,28 +290,32 @@
         # The rule of thumb is that primitives with only int and float
         # in their unwrap_spec are safe.
         try:
-            return func(interp, self, argcount)
+            if store_pc:
+                # The pc is stored because some primitives read the pc from the frame object.
+                # Only do this selectively to avoid forcing virtual frame object to the heap.
+                self.store_pc(pc)
+            func(interp, self, argcount)
         except primitives.PrimitiveFailedError:
-            pass
-        return self._sendSelfSelectorSpecial(selector, argcount, interp)
+            self._sendSelfSelectorSpecial(interp, selector, argcount)
     callPrimitive.func_name = "callPrimitive_%s" % func.func_name
     return callPrimitive
 
 def make_call_primitive_bytecode_classbased(a_class_name, a_primitive, alternative_class_name, alternative_primitive, selector, argcount):
-    @bytecode_implementation()
-    def callClassbasedPrimitive(self, interp, current_bytecode):
+    @bytecode_implementation(needs_pc=True)
+    def callClassbasedPrimitive(self, interp, current_bytecode, pc):
         rcvr = self.peek(argcount)
         receiver_class = rcvr.getclass(self.space)
         try:
             if receiver_class is getattr(self.space, a_class_name):
                 func = primitives.prim_table[a_primitive]
-                return func(interp, self, argcount)
+                func(interp, self, argcount)
             elif receiver_class is getattr(self.space, alternative_class_name):
                 func = primitives.prim_table[alternative_primitive]
-                return func(interp, self, argcount)
+                func(interp, self, argcount)
+            else:
+                self._sendSelfSelectorSpecial(interp, selector, argcount)
         except primitives.PrimitiveFailedError:
-            pass
-        return self._sendSelfSelectorSpecial(selector, argcount, interp)
+            self._sendSelfSelectorSpecial(interp, selector, argcount)
     callClassbasedPrimitive.func_name = "callClassbasedPrimitive_%s" % selector
     return callClassbasedPrimitive
 
@@ -300,7 +324,7 @@
     func = primitives.prim_table[primitive_index]
     @bytecode_implementation()
     def quick_call_primitive_bytecode(self, interp, current_bytecode):
-        return func(interp, self, argcount)
+        func(interp, self, argcount)
     return quick_call_primitive_bytecode
 
 # This is for bytecodes that actually implement a simple message-send.
@@ -308,7 +332,7 @@
 def make_send_selector_bytecode(selector, argcount):
     @bytecode_implementation()
     def selector_bytecode(self, interp, current_bytecode):
-        return self._sendSelfSelectorSpecial(selector, argcount, interp)
+        self._sendSelfSelectorSpecial(interp, selector, argcount)
     selector_bytecode.func_name = "selector_bytecode_%s" % selector
     return selector_bytecode
 
@@ -473,8 +497,8 @@
         index_in_array, w_indirectTemps = self._extract_index_and_temps(index_in_array, index_of_array)
         w_indirectTemps.atput0(self.space, index_in_array, self.pop())
 
-    @bytecode_implementation(parameter_bytes=3)
-    def pushClosureCopyCopiedValuesBytecode(self, interp, current_bytecode, descriptor, j, i):
+    @bytecode_implementation(parameter_bytes=3, jump=True)
+    def pushClosureCopyCopiedValuesBytecode(self, interp, current_bytecode, descriptor, j, i, pc):
         """ Copied from Blogpost: http://www.mirandabanda.org/cogblog/2008/07/22/closures-part-ii-the-bytecodes/
         ContextPart>>pushClosureCopyNumCopiedValues: numCopied numArgs: numArgs blockSize: blockSize
         "Simulate the action of a 'closure copy' bytecode whose result is the
@@ -500,10 +524,10 @@
         numArgs, numCopied = splitter[4, 4](descriptor)
         blockSize = (j << 8) | i
         # Create new instance of BlockClosure
-        w_closure = space.newClosure(self.w_self(), self.pc(), numArgs,
-                                            self.pop_and_return_n(numCopied))
+        w_closure = space.newClosure(self.w_self(), pc, numArgs, self.pop_and_return_n(numCopied))
         self.push(w_closure)
-        self._jump(blockSize)
+        assert blockSize >= 0
+        return self._jump(blockSize, pc)
         
     # ====== Helpers for send/return bytecodes ======
 
@@ -543,8 +567,8 @@
 
         return interp.stack_frame(s_frame)
 
-    @objectmodel.specialize.arg(1)
-    def _sendSelfSelectorSpecial(self, selector, numargs, interp):
+    @objectmodel.specialize.arg(2)
+    def _sendSelfSelectorSpecial(self, interp, selector, numargs):
         w_selector = self.space.get_special_selector(selector)
         return self._sendSelfSelector(w_selector, numargs, interp)
     
@@ -705,7 +729,7 @@
 
     # ====== Misc ======
     
-    def _activate_unwind_context(self, interp):
+    def _activate_unwind_context(self, interp, current_pc):
         # TODO put the constant somewhere else.
         # Primitive 198 is used in BlockClosure >> ensure:
         if self.is_closure_context() or self.w_method().primitive() != 198:
@@ -716,7 +740,7 @@
             self.settemp(1, self.space.w_true) # mark unwound
             self.push(self.gettemp(0)) # push the first argument
             try:
-                self.bytecodePrimValue(interp, 0)
+                self.bytecodePrimValue(interp, 0, current_pc)
             except Return, nlr:
                 if self is not nlr.s_target_context:
                     raise nlr
@@ -733,10 +757,10 @@
 
     # ====== Jump bytecodes ======
 
-    def _jump(self, offset):
-        self.store_pc(self.pc() + offset)
+    def _jump(self, offset, pc):
+        return pc + offset
 
-    def _jumpConditional(self, interp, expecting_true, position):
+    def _jumpConditional(self, interp, expecting_true, position, pc):
         if expecting_true:
             w_expected = interp.space.w_true
             w_alternative = interp.space.w_false
@@ -747,9 +771,10 @@
         # Don't check the class, just compare with only two Boolean instances.
         w_bool = self.pop()
         if w_expected.is_same_object(w_bool):
-            self._jump(position)
+            return self._jump(position, pc)
         elif not w_alternative.is_same_object(w_bool):
             self._mustBeBoolean(interp, w_bool)
+        return pc
 
     def _shortJumpOffset(self, current_bytecode):
         return (current_bytecode & 7) + 1
@@ -757,27 +782,27 @@
     def _longJumpOffset(self, current_bytecode, parameter):
         return ((current_bytecode & 3) << 8) + parameter
 
-    @bytecode_implementation()
-    def shortUnconditionalJumpBytecode(self, interp, current_bytecode):
-        self._jump(self._shortJumpOffset(current_bytecode))
+    @bytecode_implementation(jump=True)
+    def shortUnconditionalJumpBytecode(self, interp, current_bytecode, pc):
+        return self._jump(self._shortJumpOffset(current_bytecode), pc)
 
-    @bytecode_implementation()
-    def shortConditionalJumpBytecode(self, interp, current_bytecode):
+    @bytecode_implementation(jump=True)
+    def shortConditionalJumpBytecode(self, interp, current_bytecode, pc):
         # The conditional jump is "jump on false"
-        self._jumpConditional(interp, False, self._shortJumpOffset(current_bytecode))
+        return self._jumpConditional(interp, False, self._shortJumpOffset(current_bytecode), pc)
 
-    @bytecode_implementation(parameter_bytes=1)
-    def longUnconditionalJumpBytecode(self, interp, current_bytecode, parameter):
+    @bytecode_implementation(parameter_bytes=1, jump=True)
+    def longUnconditionalJumpBytecode(self, interp, current_bytecode, parameter, pc):
         offset = (((current_bytecode & 7) - 4) << 8) + parameter
-        self._jump(offset)
+        return self._jump(offset, pc)
 
-    @bytecode_implementation(parameter_bytes=1)
-    def longJumpIfTrueBytecode(self, interp, current_bytecode, parameter):
-        self._jumpConditional(interp, True, self._longJumpOffset(current_bytecode, parameter))
+    @bytecode_implementation(parameter_bytes=1, jump=True)
+    def longJumpIfTrueBytecode(self, interp, current_bytecode, parameter, pc):
+        return self._jumpConditional(interp, True, self._longJumpOffset(current_bytecode, parameter), pc)
 
-    @bytecode_implementation(parameter_bytes=1)
-    def longJumpIfFalseBytecode(self, interp, current_bytecode, parameter):
-        self._jumpConditional(interp, False, self._longJumpOffset(current_bytecode, parameter))
+    @bytecode_implementation(parameter_bytes=1, jump=True)
+    def longJumpIfFalseBytecode(self, interp, current_bytecode, parameter, pc):
+        return self._jumpConditional(interp, False, self._longJumpOffset(current_bytecode, parameter), pc)
 
     # ====== Bytecodes implemented with primitives and message sends ======
 
@@ -808,7 +833,7 @@
     bytecodePrimEquivalent = make_quick_call_primitive_bytecode(primitives.EQUIVALENT, 1)
     bytecodePrimClass = make_quick_call_primitive_bytecode(primitives.CLASS, 0)
 
-    bytecodePrimBlockCopy = make_call_primitive_bytecode(primitives.BLOCK_COPY, "blockCopy:", 1)
+    bytecodePrimBlockCopy = make_call_primitive_bytecode(primitives.BLOCK_COPY, "blockCopy:", 1, store_pc=True)
     bytecodePrimValue = make_call_primitive_bytecode_classbased("w_BlockContext", primitives.VALUE, "w_BlockClosure", primitives.CLOSURE_VALUE, "value", 0)
     bytecodePrimValueWithArg = make_call_primitive_bytecode_classbased("w_BlockContext", primitives.VALUE, "w_BlockClosure", primitives.CLOSURE_VALUE_, "value:", 1)
 
diff --git a/spyvm/test/test_interpreter.py b/spyvm/test/test_interpreter.py
--- a/spyvm/test/test_interpreter.py
+++ b/spyvm/test/test_interpreter.py
@@ -22,7 +22,7 @@
 def step_in_interp(ctxt): # due to missing resets in between tests
     interp._loop = False
     try:
-        retval = interp.step(ctxt)
+        retval = interp.step_context(ctxt)
         if retval is not None:
             return retval.w_self()
     except interpreter.Return, nlr:
@@ -1013,7 +1013,7 @@
         assert False
 
 class StackTestInterpreter(TestInterpreter):
-    def stack_frame(self, w_frame, may_interrupt=True):
+    def stack_frame(self, w_frame, may_interrupt=True, fresh_context=False):
         stack_depth = self.max_stack_depth - self.remaining_stack_depth
         for i in range(stack_depth + 1):
             assert sys._getframe(5 + i * 7).f_code.co_name == 'loop_bytecodes'
diff --git a/spyvm/test/test_miniimage.py b/spyvm/test/test_miniimage.py
--- a/spyvm/test/test_miniimage.py
+++ b/spyvm/test/test_miniimage.py
@@ -372,12 +372,12 @@
     
     assert isinstance(s_ctx, shadow.MethodContextShadow)
     assert s_ctx.top().is_same_object(space.w_true)
-    interp.step(s_ctx)
-    interp.step(s_ctx)
+    interp.step_context(s_ctx)
+    interp.step_context(s_ctx)
     assert s_ctx.top().value == 1
-    interp.step(s_ctx)
+    interp.step_context(s_ctx)
     assert s_ctx.top().value == 2
-    interp.step(s_ctx)
+    interp.step_context(s_ctx)
     assert s_ctx.top().value == 3
 
 def test_primitive_perform_with_args():
diff --git a/spyvm/test/test_primitives.py b/spyvm/test/test_primitives.py
--- a/spyvm/test/test_primitives.py
+++ b/spyvm/test/test_primitives.py
@@ -673,7 +673,7 @@
 
     def quick_check_for_interrupt(s_frame, dec=1):
         raise Context_switched
-    def step(s_frame):
+    def step(s_frame, pc):
         raise Stepping
 
     w_frame, s_initial_context = new_frame("<never called, but used for method generation>")
diff --git a/spyvm/test/util.py b/spyvm/test/util.py
--- a/spyvm/test/util.py
+++ b/spyvm/test/util.py
@@ -81,12 +81,20 @@
         self._loop = True
         return interpreter.Interpreter.loop(self, w_active_context)
     
-    def stack_frame(self, s_new_frame, may_context_switch=True):
+    def stack_frame(self, s_new_frame, may_context_switch=True, fresh_context=False):
         if not self._loop:
+            self.s_new_frame = s_new_frame
             return s_new_frame # this test is done to not loop in test,
                                # but rather step just once where wanted
-        return interpreter.Interpreter.stack_frame(self, s_new_frame, may_context_switch)
-
+        return interpreter.Interpreter.stack_frame(self, s_new_frame,
+                                may_context_switch=may_context_switch, fresh_context=fresh_context)
+    
+    def step_context(self, s_context):
+        self.s_new_frame = None
+        new_pc = self.step(s_context, s_context.pc())
+        s_context.store_pc(new_pc)
+        return self.s_new_frame # None, if we're still in the same frame.
+    
 class BootstrappedObjSpace(objspace.ObjSpace):
     
     def bootstrap(self):


More information about the pypy-commit mailing list