Commit add499ab authored by Jason Madden's avatar Jason Madden

Substantial performance gains for PyPy.

First, eliminate the use of the RingWrapper object and always delete
by index. It was only necessary to allow use of ring.remove(). This
had some performance impact, but mostly saves memory.

Second, eliminate the use of `enumerate` in the hot mru() path. This was the big performance win.

Current results:

** concurrency=2 **
"Transaction",                mysql     before
"Add 3000 Objects",             7424     5486
"Update 3000 Objects",          5699     4141
"Read 3000 Warm Objects",       4571     4003
"Read 3000 Cold Objects",       4932     4204
"Read 3000 Hot Objects",       17295     10416
"Read 3000 Steamin' Objects", 346331     168983
parent 2b4dce7f
...@@ -56,24 +56,6 @@ else: ...@@ -56,24 +56,6 @@ else:
_OGA = object.__getattribute__ _OGA = object.__getattribute__
class _RingWrapper(object):
"""
A wrapper for objects in the cache's MRU deque that has value-based
identity semantics (to avoid activating ghosted objects or otherwise
finding the wrong object).
"""
__slots__ = ('object',)
def __init__(self, o):
self.object = o
def __eq__(self, other):
try:
return self.object is other.object
except AttributeError:
return NotImplemented
def _sweeping_ring(f): def _sweeping_ring(f):
# A decorator for functions in the PickleCache # A decorator for functions in the PickleCache
# that are sweeping the entire ring (mutating it); # that are sweeping the entire ring (mutating it);
...@@ -118,8 +100,10 @@ class PickleCache(object): ...@@ -118,8 +100,10 @@ class PickleCache(object):
self.non_ghost_count = 0 self.non_ghost_count = 0
self.persistent_classes = {} self.persistent_classes = {}
self.data = weakref.WeakValueDictionary() self.data = weakref.WeakValueDictionary()
# oldest is on the left, newest on the right so that default iteration order # oldest is on the left, newest on the right so that default
# is maintained from oldest to newest # iteration order is maintained from oldest to newest.
# Note that the remove() method is verboten: it uses equality
# comparisons, but we must use identity comparisons
self.ring = deque() self.ring = deque()
self.cache_size_bytes = cache_size_bytes self.cache_size_bytes = cache_size_bytes
...@@ -191,13 +175,7 @@ class PickleCache(object): ...@@ -191,13 +175,7 @@ class PickleCache(object):
del self.persistent_classes[oid] del self.persistent_classes[oid]
else: else:
value = self.data.pop(oid) value = self.data.pop(oid)
wrapper = _RingWrapper(value) self._remove_from_ring(value)
try:
self.ring.remove(wrapper)
except ValueError:
pass
else:
self.non_ghost_count -= 1
def get(self, oid, default=None): def get(self, oid, default=None):
""" See IPickleCache. """ See IPickleCache.
...@@ -218,16 +196,16 @@ class PickleCache(object): ...@@ -218,16 +196,16 @@ class PickleCache(object):
return False # marker return for tests return False # marker return for tests
value = self.data[oid] value = self.data[oid]
for i, wrapper in enumerate(self.ring): was_in_ring = self._remove_from_ring(value)
if wrapper.object is value: if was_in_ring:
del self.ring[i] # Compensate for decrementing the count; by
self.ring.append(wrapper) # definition it should already have been not-a-ghost
break # so we can avoid a trip through Persistent.__getattribute__
else: self.ring.append(value)
# It wasn't present in the ring self.non_ghost_count += 1
if _OGA(value, '_p_state') != GHOST: elif _OGA(value, '_p_state') != GHOST:
self.non_ghost_count += 1 self.ring.append(value)
self.ring.append(_RingWrapper(value)) self.non_ghost_count += 1
def ringlen(self): def ringlen(self):
""" See IPickleCache. """ See IPickleCache.
...@@ -242,7 +220,7 @@ class PickleCache(object): ...@@ -242,7 +220,7 @@ class PickleCache(object):
def lru_items(self): def lru_items(self):
""" See IPickleCache. """ See IPickleCache.
""" """
return [(x.object._p_oid, x.object) for x in self.ring] return [(x._p_oid, x) for x in self.ring]
def klass_items(self): def klass_items(self):
""" See IPickleCache. """ See IPickleCache.
...@@ -369,13 +347,17 @@ class PickleCache(object): ...@@ -369,13 +347,17 @@ class PickleCache(object):
def _sweep(self, target, target_size_bytes=0): def _sweep(self, target, target_size_bytes=0):
# lock # lock
# We can't mutate while we're iterating, so store ejections by index here # We can't mutate while we're iterating, so store ejections by index here
# (deleting by index is potentially more efficient then by value) # (deleting by index is potentially more efficient then by value because it
# can use the rotate() method and not be O(n)). Also note that we do not use
# self._remove_from_ring because we need to decrement the non_ghost_count
# as we traverse the ring to be sure to meet our target
to_eject = [] to_eject = []
for i, node in enumerate(self.ring): i = -1 # Using a manual numeric counter instead of enumerate() is much faster on PyPy
for value in self.ring:
if self.non_ghost_count <= target and (self.total_estimated_size <= target_size_bytes or not target_size_bytes): if self.non_ghost_count <= target and (self.total_estimated_size <= target_size_bytes or not target_size_bytes):
break break
i += 1
if node.object._p_state == UPTODATE: if value._p_state == UPTODATE:
# The C implementation will only evict things that are specifically # The C implementation will only evict things that are specifically
# in the up-to-date state # in the up-to-date state
self._persistent_deactivate_ran = False self._persistent_deactivate_ran = False
...@@ -391,39 +373,29 @@ class PickleCache(object): ...@@ -391,39 +373,29 @@ class PickleCache(object):
# actions if our _p_deactivate ran, in case of buggy subclasses. # actions if our _p_deactivate ran, in case of buggy subclasses.
# see _persistent_deactivate_ran # see _persistent_deactivate_ran
node.object._p_deactivate() value._p_deactivate()
if (self._persistent_deactivate_ran if (self._persistent_deactivate_ran
# Test-cases sneak in non-Persistent objects, sigh, so naturally # Test-cases sneak in non-Persistent objects, sigh, so naturally
# they don't cooperate (without this check a bunch of test_picklecache # they don't cooperate (without this check a bunch of test_picklecache
# breaks) # breaks)
or not isinstance(node.object, _SWEEPABLE_TYPES)): or not isinstance(value, _SWEEPABLE_TYPES)):
to_eject.append(i) to_eject.append(i)
self.non_ghost_count -= 1 self.non_ghost_count -= 1
for i in reversed(to_eject): for i in reversed(to_eject):
del self.ring[i] del self.ring[i]
ejected = len(to_eject) if to_eject and _SWEEP_NEEDS_GC:
del to_eject
if ejected and _SWEEP_NEEDS_GC:
# See comments on _SWEEP_NEEDS_GC # See comments on _SWEEP_NEEDS_GC
gc.collect() gc.collect()
return ejected return len(to_eject)
@_sweeping_ring @_sweeping_ring
def _invalidate(self, oid): def _invalidate(self, oid):
value = self.data.get(oid) value = self.data.get(oid)
if value is not None and value._p_state != GHOST: if value is not None and value._p_state != GHOST:
value._p_invalidate() value._p_invalidate()
try: self._remove_from_ring(value)
self.ring.remove(_RingWrapper(value))
except ValueError:
# The ring has been corrupted!
# This should be impossible in real life; a contrived
# test case does this
pass
else:
self.non_ghost_count -= 1
elif oid in self.persistent_classes: elif oid in self.persistent_classes:
persistent_class = self.persistent_classes[oid] persistent_class = self.persistent_classes[oid]
del self.persistent_classes[oid] del self.persistent_classes[oid]
...@@ -434,3 +406,23 @@ class PickleCache(object): ...@@ -434,3 +406,23 @@ class PickleCache(object):
persistent_class._p_invalidate() persistent_class._p_invalidate()
except AttributeError: except AttributeError:
pass pass
def _remove_from_ring(self, value):
"""
Removes the previously non-ghost `value` from the ring, decrementing
the `non_ghost_count` if it's found. The value may be a ghost when
this method is called.
:return: A true value if the object was found in the ring.
"""
# Note that we do not use self.ring.remove() because that
# uses equality semantics and we don't want to call the persistent
# object's __eq__ method (which might wake it up just after we
# tried to ghost it)
i = 0 # Using a manual numeric counter instead of enumerate() is much faster on PyPy
for o in self.ring:
if o is value:
del self.ring[i]
self.non_ghost_count -= 1
return 1
i += 1
...@@ -979,8 +979,8 @@ class PickleCacheTests(unittest.TestCase): ...@@ -979,8 +979,8 @@ class PickleCacheTests(unittest.TestCase):
cache[p._p_oid] = p cache[p._p_oid] = p
self.assertEqual(cache.cache_non_ghost_count, 1) self.assertEqual(cache.cache_non_ghost_count, 1)
self.assertEqual(cache.ring[0].object, p) self.assertEqual(cache.ring[0], p)
cache.ring[0].object = None cache.ring[0] = None
# Nothing to test, just that it doesn't break # Nothing to test, just that it doesn't break
cache._invalidate(p._p_oid) cache._invalidate(p._p_oid)
...@@ -1065,11 +1065,6 @@ class PickleCacheTests(unittest.TestCase): ...@@ -1065,11 +1065,6 @@ class PickleCacheTests(unittest.TestCase):
self.assertTrue(pclass.invalidated) self.assertTrue(pclass.invalidated)
def test_ring_wrapper_equality(self):
from persistent.picklecache import _RingWrapper
self.assertEqual(_RingWrapper(self), _RingWrapper(self))
self.assertNotEqual(_RingWrapper(self), self)
class DummyPersistent(object): class DummyPersistent(object):
def _p_invalidate(self): def _p_invalidate(self):
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment