Author: Armin Rigo <[email protected]>
Branch: 
Changeset: r44278:a9d654880b5b
Date: 2011-05-18 13:01 +0200
http://bitbucket.org/pypy/pypy/changeset/a9d654880b5b/

Log:    hg merge 0eedad4896ba (branch mapdict-interp):

         Reintroduce the fast paths killed in 8624cde59095, to avoid
        slowing down interpreted execution too much.

diff --git a/pypy/config/pypyoption.py b/pypy/config/pypyoption.py
--- a/pypy/config/pypyoption.py
+++ b/pypy/config/pypyoption.py
@@ -269,7 +269,7 @@
                    "make instances really small but slow without the JIT",
                    default=False,
                    requires=[("objspace.std.getattributeshortcut", True),
-                             ("objspace.std.withtypeversion", True),
+                             ("objspace.std.withmethodcache", True),
                        ]),
 
         BoolOption("withrangelist",
diff --git a/pypy/interpreter/pycode.py b/pypy/interpreter/pycode.py
--- a/pypy/interpreter/pycode.py
+++ b/pypy/interpreter/pycode.py
@@ -116,6 +116,10 @@
 
         self._compute_flatcall()
 
+        if self.space.config.objspace.std.withmapdict:
+            from pypy.objspace.std.mapdict import init_mapdict_cache
+            init_mapdict_cache(self)
+
     def _freeze_(self):
         if (self.magic == cpython_magic and
             '__pypy__' not in sys.builtin_module_names):
diff --git a/pypy/interpreter/pyopcode.py b/pypy/interpreter/pyopcode.py
--- a/pypy/interpreter/pyopcode.py
+++ b/pypy/interpreter/pyopcode.py
@@ -723,8 +723,13 @@
     def LOAD_ATTR(self, nameindex, next_instr):
         "obj.attributename"
         w_obj = self.popvalue()
-        w_attributename = self.getname_w(nameindex)
-        w_value = self.space.getattr(w_obj, w_attributename)
+        if (self.space.config.objspace.std.withmapdict
+            and not jit.we_are_jitted()):
+            from pypy.objspace.std.mapdict import LOAD_ATTR_caching
+            w_value = LOAD_ATTR_caching(self.getcode(), w_obj, nameindex)
+        else:
+            w_attributename = self.getname_w(nameindex)
+            w_value = self.space.getattr(w_obj, w_attributename)
         self.pushvalue(w_value)
     LOAD_ATTR._always_inline_ = True
 
diff --git a/pypy/objspace/std/callmethod.py b/pypy/objspace/std/callmethod.py
--- a/pypy/objspace/std/callmethod.py
+++ b/pypy/objspace/std/callmethod.py
@@ -13,6 +13,8 @@
 from pypy.interpreter import function
 from pypy.objspace.descroperation import object_getattribute
 from pypy.rlib import jit, rstack # for resume points
+from pypy.objspace.std.mapdict import LOOKUP_METHOD_mapdict, \
+    LOOKUP_METHOD_mapdict_fill_cache_method
 
 
 # This module exports two extra methods for StdObjSpaceFrame implementing
@@ -31,6 +33,13 @@
     #
     space = f.space
     w_obj = f.popvalue()
+
+    if space.config.objspace.std.withmapdict and not jit.we_are_jitted():
+        # mapdict has an extra-fast version of this function
+        from pypy.objspace.std.mapdict import LOOKUP_METHOD_mapdict
+        if LOOKUP_METHOD_mapdict(f, nameindex, w_obj):
+            return
+
     w_name = f.getname_w(nameindex)
     w_value = None
 
@@ -42,6 +51,7 @@
             # this handles directly the common case
             #   module.function(args..)
             w_value = w_obj.getdictvalue(space, name)
+            # xxx we could also use the mapdict cache in that case, probably
         else:
             typ = type(w_descr)
             if typ is function.Function or typ is 
function.FunctionWithFixedCode:
@@ -51,6 +61,11 @@
                     # nothing in the instance
                     f.pushvalue(w_descr)
                     f.pushvalue(w_obj)
+                    if (space.config.objspace.std.withmapdict and
+                            not jit.we_are_jitted()):
+                        # let mapdict cache stuff
+                        LOOKUP_METHOD_mapdict_fill_cache_method(
+                            space, f.getcode(), name, nameindex, w_obj, w_type)
                     return
     if w_value is None:
         w_value = space.getattr(w_obj, w_name)
diff --git a/pypy/objspace/std/mapdict.py b/pypy/objspace/std/mapdict.py
--- a/pypy/objspace/std/mapdict.py
+++ b/pypy/objspace/std/mapdict.py
@@ -8,6 +8,7 @@
 from pypy.objspace.std.dictmultiobject import IteratorImplementation
 from pypy.objspace.std.dictmultiobject import _is_sane_hash
 from pypy.objspace.std.objectobject import W_ObjectObject
+from pypy.objspace.std.typeobject import TypeCell
 
 # ____________________________________________________________
 # attribute shapes
@@ -683,3 +684,149 @@
                 w_attr = self.space.wrap(attr)
                 return w_attr, self.w_obj.getdictvalue(self.space, attr)
         return None, None
+
+# ____________________________________________________________
+# Magic caching
+
+class CacheEntry(object):
+    version_tag = None
+    index = 0
+    w_method = None # for callmethod
+    success_counter = 0
+    failure_counter = 0
+
+    def is_valid_for_obj(self, w_obj):
+        map = w_obj._get_mapdict_map()
+        return self.is_valid_for_map(map)
+
+    @jit.dont_look_inside
+    def is_valid_for_map(self, map):
+        # note that 'map' can be None here
+        mymap = self.map_wref()
+        if mymap is not None and mymap is map:
+            version_tag = map.terminator.w_cls.version_tag()
+            if version_tag is self.version_tag:
+                # everything matches, it's incredibly fast
+                if map.space.config.objspace.std.withmethodcachecounter:
+                    self.success_counter += 1
+                return True
+        return False
+
+_invalid_cache_entry_map = objectmodel.instantiate(AbstractAttribute)
+_invalid_cache_entry_map.terminator = None
+INVALID_CACHE_ENTRY = CacheEntry()
+INVALID_CACHE_ENTRY.map_wref = weakref.ref(_invalid_cache_entry_map)
+                                 # different from any real map ^^^
+
+def init_mapdict_cache(pycode):
+    num_entries = len(pycode.co_names_w)
+    pycode._mapdict_caches = [INVALID_CACHE_ENTRY] * num_entries
+
[email protected]_look_inside
+def _fill_cache(pycode, nameindex, map, version_tag, index, w_method=None):
+    entry = pycode._mapdict_caches[nameindex]
+    if entry is INVALID_CACHE_ENTRY:
+        entry = CacheEntry()
+        pycode._mapdict_caches[nameindex] = entry
+    entry.map_wref = weakref.ref(map)
+    entry.version_tag = version_tag
+    entry.index = index
+    entry.w_method = w_method
+    if pycode.space.config.objspace.std.withmethodcachecounter:
+        entry.failure_counter += 1
+
+def LOAD_ATTR_caching(pycode, w_obj, nameindex):
+    # this whole mess is to make the interpreter quite a bit faster; it's not
+    # used if we_are_jitted().
+    entry = pycode._mapdict_caches[nameindex]
+    map = w_obj._get_mapdict_map()
+    if entry.is_valid_for_map(map) and entry.w_method is None:
+        # everything matches, it's incredibly fast
+        return w_obj._mapdict_read_storage(entry.index)
+    return LOAD_ATTR_slowpath(pycode, w_obj, nameindex, map)
+LOAD_ATTR_caching._always_inline_ = True
+
+def LOAD_ATTR_slowpath(pycode, w_obj, nameindex, map):
+    space = pycode.space
+    w_name = pycode.co_names_w[nameindex]
+    if map is not None:
+        w_type = map.terminator.w_cls
+        w_descr = w_type.getattribute_if_not_from_object()
+        if w_descr is not None:
+            return space._handle_getattribute(w_descr, w_obj, w_name)
+        version_tag = w_type.version_tag()
+        if version_tag is not None:
+            name = space.str_w(w_name)
+            # We need to care for obscure cases in which the w_descr is
+            # a TypeCell, which may change without changing the version_tag
+            assert space.config.objspace.std.withmethodcache
+            _, w_descr = w_type._pure_lookup_where_with_method_cache(
+                name, version_tag)
+            #
+            selector = ("", INVALID)
+            if w_descr is None:
+                selector = (name, DICT) #common case: no such attr in the class
+            elif isinstance(w_descr, TypeCell):
+                pass              # we have a TypeCell in the class: give up
+            elif space.is_data_descr(w_descr):
+                # we have a data descriptor, which means the dictionary value
+                # (if any) has no relevance.
+                from pypy.interpreter.typedef import Member
+                descr = space.interpclass_w(w_descr)
+                if isinstance(descr, Member):    # it is a slot -- easy case
+                    selector = ("slot", SLOTS_STARTING_FROM + descr.index)
+            else:
+                # There is a non-data descriptor in the class.  If there is
+                # also a dict attribute, use the latter, caching its position.
+                # If not, we loose.  We could do better in this case too,
+                # but we don't care too much; the common case of a method
+                # invocation is handled by LOOKUP_METHOD_xxx below.
+                selector = (name, DICT)
+            #
+            if selector[1] != INVALID:
+                index = map.index(selector)
+                if index >= 0:
+                    # Note that if map.terminator is a DevolvedDictTerminator,
+                    # map.index() will always return -1 if selector[1]==DICT.
+                    _fill_cache(pycode, nameindex, map, version_tag, index)
+                    return w_obj._mapdict_read_storage(index)
+    if space.config.objspace.std.withmethodcachecounter:
+        INVALID_CACHE_ENTRY.failure_counter += 1
+    return space.getattr(w_obj, w_name)
+LOAD_ATTR_slowpath._dont_inline_ = True
+
+def LOOKUP_METHOD_mapdict(f, nameindex, w_obj):
+    space = f.space
+    pycode = f.getcode()
+    entry = pycode._mapdict_caches[nameindex]
+    if entry.is_valid_for_obj(w_obj):
+        w_method = entry.w_method
+        if w_method is not None:
+            f.pushvalue(w_method)
+            f.pushvalue(w_obj)
+            return True
+    return False
+
+def LOOKUP_METHOD_mapdict_fill_cache_method(space, pycode, name, nameindex,
+                                            w_obj, w_type):
+    version_tag = w_type.version_tag()
+    if version_tag is None:
+        return
+    map = w_obj._get_mapdict_map()
+    if map is None or isinstance(map.terminator, DevolvedDictTerminator):
+        return
+    # We know here that w_obj.getdictvalue(space, name) just returned None,
+    # so the 'name' is not in the instance.  We repeat the lookup to find it
+    # in the class, this time taking care of the result: it can be either a
+    # quasi-constant class attribute, or actually a TypeCell --- which we
+    # must not cache.  (It should not be None here, but you never know...)
+    assert space.config.objspace.std.withmethodcache
+    _, w_method = w_type._pure_lookup_where_with_method_cache(name,
+                                                              version_tag)
+    if w_method is None or isinstance(w_method, TypeCell):
+        return
+    _fill_cache(pycode, nameindex, map, version_tag, -1, w_method)
+
+# XXX fix me: if a function contains a loop with both LOAD_ATTR and
+# XXX LOOKUP_METHOD on the same attribute name, it keeps trashing and
+# XXX rebuilding the cache
diff --git a/pypy/objspace/std/test/test_mapdict.py 
b/pypy/objspace/std/test/test_mapdict.py
--- a/pypy/objspace/std/test/test_mapdict.py
+++ b/pypy/objspace/std/test/test_mapdict.py
@@ -604,6 +604,274 @@
         assert a.__dict__ is d
         assert isinstance(a, B)
 
+
+class AppTestWithMapDictAndCounters(object):
+    def setup_class(cls):
+        from pypy.interpreter import gateway
+        cls.space = gettestobjspace(
+            **{"objspace.std.withmapdict": True,
+               "objspace.std.withmethodcachecounter": True,
+               "objspace.opcodes.CALL_METHOD": True})
+        #
+        def check(space, w_func, name):
+            w_code = space.getattr(w_func, space.wrap('func_code'))
+            nameindex = map(space.str_w, w_code.co_names_w).index(name)
+            entry = w_code._mapdict_caches[nameindex]
+            entry.failure_counter = 0
+            entry.success_counter = 0
+            INVALID_CACHE_ENTRY.failure_counter = 0
+            #
+            w_res = space.call_function(w_func)
+            assert space.eq_w(w_res, space.wrap(42))
+            #
+            entry = w_code._mapdict_caches[nameindex]
+            if entry is INVALID_CACHE_ENTRY:
+                failures = successes = 0
+            else:
+                failures = entry.failure_counter
+                successes = entry.success_counter
+            globalfailures = INVALID_CACHE_ENTRY.failure_counter
+            return space.wrap((failures, successes, globalfailures))
+        check.unwrap_spec = [gateway.ObjSpace, gateway.W_Root, str]
+        cls.w_check = cls.space.wrap(gateway.interp2app(check))
+
+    def test_simple(self):
+        class A(object):
+            pass
+        a = A()
+        a.x = 42
+        def f():
+            return a.x
+        #
+        res = self.check(f, 'x')
+        assert res == (1, 0, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+        #
+        A.y = 5     # unrelated, but changes the version_tag
+        res = self.check(f, 'x')
+        assert res == (1, 0, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+        #
+        A.x = 8     # but shadowed by 'a.x'
+        res = self.check(f, 'x')
+        assert res == (1, 0, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+
+    def test_property(self):
+        class A(object):
+            x = property(lambda self: 42)
+        a = A()
+        def f():
+            return a.x
+        #
+        res = self.check(f, 'x')
+        assert res == (0, 0, 1)
+        res = self.check(f, 'x')
+        assert res == (0, 0, 1)
+        res = self.check(f, 'x')
+        assert res == (0, 0, 1)
+
+    def test_slots(self):
+        class A(object):
+            __slots__ = ['x']
+        a = A()
+        a.x = 42
+        def f():
+            return a.x
+        #
+        res = self.check(f, 'x')
+        assert res == (1, 0, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+
+    def test_two_attributes(self):
+        class A(object):
+            pass
+        a = A()
+        a.x = 40
+        a.y = -2
+        def f():
+            return a.x - a.y
+        #
+        res = self.check(f, 'x')
+        assert res == (1, 0, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+        #
+        res = self.check(f, 'y')
+        assert res == (0, 1, 0)
+        res = self.check(f, 'y')
+        assert res == (0, 1, 0)
+        res = self.check(f, 'y')
+        assert res == (0, 1, 0)
+
+    def test_two_maps(self):
+        class A(object):
+            pass
+        a = A()
+        a.x = 42
+        def f():
+            return a.x
+        #
+        res = self.check(f, 'x')
+        assert res == (1, 0, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+        #
+        a.y = "foo"      # changes the map
+        res = self.check(f, 'x')
+        assert res == (1, 0, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+        #
+        a.y = "bar"      # does not change the map any more
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+
+    def test_custom_metaclass(self):
+        class A(object):
+            class __metaclass__(type):
+                pass
+        a = A()
+        a.x = 42
+        def f():
+            return a.x
+        #
+        res = self.check(f, 'x')
+        assert res == (1, 0, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+        res = self.check(f, 'x')
+        assert res == (0, 1, 0)
+
+    def test_old_style_base(self):
+        class B:
+            pass
+        class C(object):
+            pass
+        class A(C, B):
+            pass
+        a = A()
+        a.x = 42
+        def f():
+            return a.x
+        #
+        res = self.check(f, 'x')
+        assert res == (0, 0, 1)
+        res = self.check(f, 'x')
+        assert res == (0, 0, 1)
+        res = self.check(f, 'x')
+        assert res == (0, 0, 1)
+
+    def test_call_method_uses_cache(self):
+        # bit sucky
+        global C
+
+        class C(object):
+            def m(*args):
+                return args
+        C.sm = staticmethod(C.m.im_func)
+        C.cm = classmethod(C.m.im_func)
+
+        exec """if 1:
+
+            def f():
+                c = C()
+                res = c.m(1)
+                assert res == (c, 1)
+                return 42
+
+            def g():
+                c = C()
+                res = c.sm(1)
+                assert res == (1, )
+                return 42
+
+            def h():
+                c = C()
+                res = c.cm(1)
+                assert res == (C, 1)
+                return 42
+        """
+        res = self.check(f, 'm')
+        assert res == (1, 0, 0)
+        res = self.check(f, 'm')
+        assert res == (0, 1, 0)
+        res = self.check(f, 'm')
+        assert res == (0, 1, 0)
+        res = self.check(f, 'm')
+        assert res == (0, 1, 0)
+
+        # static methods are not cached
+        res = self.check(g, 'sm')
+        assert res == (0, 0, 0)
+        res = self.check(g, 'sm')
+        assert res == (0, 0, 0)
+
+        # neither are class methods
+        res = self.check(h, 'cm')
+        assert res == (0, 0, 0)
+        res = self.check(h, 'cm')
+        assert res == (0, 0, 0)
+
+    def test_mix_cache_bug(self):
+        # bit sucky
+        global C
+
+        class C(object):
+            def m(*args):
+                return args
+
+        exec """if 1:
+
+            def f():
+                c = C()
+                res = c.m(1)
+                assert res == (c, 1)
+                bm = c.m
+                res = bm(1)
+                assert res == (c, 1)
+                return 42
+
+        """
+        res = self.check(f, 'm')
+        assert res == (1, 1, 1)
+        res = self.check(f, 'm')
+        assert res == (0, 2, 1)
+        res = self.check(f, 'm')
+        assert res == (0, 2, 1)
+        res = self.check(f, 'm')
+        assert res == (0, 2, 1)
+
     def test_dont_keep_class_alive(self):
         import weakref
         import gc
@@ -648,6 +916,40 @@
         got = a.method()
         assert got == 43
 
+    def test_bug_method_change(self):
+        class A(object):
+            def method(self):
+                return 42
+        a = A()
+        got = a.method()
+        assert got == 42
+        A.method = lambda self: 43
+        got = a.method()
+        assert got == 43
+        A.method = lambda self: 44
+        got = a.method()
+        assert got == 44
+
+    def test_bug_slot_via_changing_member_descr(self):
+        class A(object):
+            __slots__ = ['a', 'b', 'c', 'd']
+        x = A()
+        x.a = 'a'
+        x.b = 'b'
+        x.c = 'c'
+        x.d = 'd'
+        got = x.a
+        assert got == 'a'
+        A.a = A.b
+        got = x.a
+        assert got == 'b'
+        A.a = A.c
+        got = x.a
+        assert got == 'c'
+        A.a = A.d
+        got = x.a
+        assert got == 'd'
+
 class AppTestGlobalCaching(AppTestWithMapDict):
     def setup_class(cls):
         cls.space = gettestobjspace(
_______________________________________________
pypy-commit mailing list
[email protected]
http://mail.python.org/mailman/listinfo/pypy-commit

Reply via email to