Commit 2b4dce7f authored by Jason Madden's avatar Jason Madden

In pickelcache.py, use collections.dequeue instead of hand-rolling a doubly linked list.

Under PyPy, CPython, and Jython, collections.dequeue is a built-in
type. Using it doubles PyPy's performance under `zodbshootout`
(previously, picklecache.mru() was the biggest bottleneck).
parent 6fa457df
...@@ -56,13 +56,23 @@ else: ...@@ -56,13 +56,23 @@ else:
_OGA = object.__getattribute__ _OGA = object.__getattribute__
class RingNode(object): class _RingWrapper(object):
# 32 byte fixed size wrapper. """
__slots__ = ('object', 'next', 'prev') A wrapper for objects in the cache's MRU deque that has value-based
def __init__(self, object, next=None, prev=None): identity semantics (to avoid activating ghosted objects or otherwise
self.object = object finding the wrong object).
self.next = next """
self.prev = prev __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
...@@ -77,6 +87,8 @@ def _sweeping_ring(f): ...@@ -77,6 +87,8 @@ def _sweeping_ring(f):
self._is_sweeping_ring = False self._is_sweeping_ring = False
return locked return locked
from collections import deque
@implementer(IPickleCache) @implementer(IPickleCache)
class PickleCache(object): class PickleCache(object):
...@@ -106,8 +118,9 @@ class PickleCache(object): ...@@ -106,8 +118,9 @@ 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()
self.ring = RingNode(None) # oldest is on the left, newest on the right so that default iteration order
self.ring.next = self.ring.prev = self.ring # is maintained from oldest to newest
self.ring = deque()
self.cache_size_bytes = cache_size_bytes self.cache_size_bytes = cache_size_bytes
# IPickleCache API # IPickleCache API
...@@ -178,13 +191,13 @@ class PickleCache(object): ...@@ -178,13 +191,13 @@ 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)
node = self.ring.next wrapper = _RingWrapper(value)
while node is not self.ring: try:
if node.object is value: self.ring.remove(wrapper)
node.prev.next, node.next.prev = node.next, node.prev except ValueError:
self.non_ghost_count -= 1 pass
break else:
node = node.next self.non_ghost_count -= 1
def get(self, oid, default=None): def get(self, oid, default=None):
""" See IPickleCache. """ See IPickleCache.
...@@ -204,43 +217,22 @@ class PickleCache(object): ...@@ -204,43 +217,22 @@ class PickleCache(object):
# because that could leave it inconsistent # because that could leave it inconsistent
return False # marker return for tests return False # marker return for tests
# Under certain benchmarks, like zodbshootout, this method is value = self.data[oid]
# the primary bottleneck (compare 33,473 "steamin" objects per for i, wrapper in enumerate(self.ring):
# second in the original version of this function with 542,144 if wrapper.object is value:
# objects per second if this function simply returns), so we del self.ring[i]
# take a few steps to reduce the pressure: self.ring.append(wrapper)
# * object.__getattribute__ here to avoid recursive calls break
# back to Persistent.__getattribute__. This alone makes a 50% else:
# difference in zodbshootout performance (55,000 OPS) # It wasn't present in the ring
node = self.ring.next
while node is not self.ring and _OGA(node.object, '_p_oid') != oid:
node = node.next
if node is self.ring:
value = self.data[oid]
if _OGA(value, '_p_state') != GHOST: if _OGA(value, '_p_state') != GHOST:
self.non_ghost_count += 1 self.non_ghost_count += 1
mru = self.ring.prev self.ring.append(_RingWrapper(value))
self.ring.prev = node = RingNode(value, self.ring, mru)
mru.next = node
else:
# This assertion holds, but it's a redundant getattribute access
#assert node.object._p_oid == oid
# remove from old location
node.prev.next, node.next.prev = node.next, node.prev
# splice into new
self.ring.prev.next, node.prev = node, self.ring.prev
self.ring.prev, node.next = node, self.ring
def ringlen(self): def ringlen(self):
""" See IPickleCache. """ See IPickleCache.
""" """
result = 0 return len(self.ring)
node = self.ring.next
while node is not self.ring:
result += 1
node = node.next
return result
def items(self): def items(self):
""" See IPickleCache. """ See IPickleCache.
...@@ -250,12 +242,7 @@ class PickleCache(object): ...@@ -250,12 +242,7 @@ class PickleCache(object):
def lru_items(self): def lru_items(self):
""" See IPickleCache. """ See IPickleCache.
""" """
result = [] return [(x.object._p_oid, x.object) for x in self.ring]
node = self.ring.next
while node is not self.ring:
result.append((node.object._p_oid, node.object))
node = node.next
return result
def klass_items(self): def klass_items(self):
""" See IPickleCache. """ See IPickleCache.
...@@ -313,9 +300,7 @@ class PickleCache(object): ...@@ -313,9 +300,7 @@ class PickleCache(object):
if value._p_state == GHOST: if value._p_state == GHOST:
value._p_activate() value._p_activate()
self.non_ghost_count += 1 self.non_ghost_count += 1
mru = self.ring.prev self.mru(oid)
self.ring.prev = node = RingNode(value, self.ring, mru)
mru.next = node
def invalidate(self, to_invalidate): def invalidate(self, to_invalidate):
""" See IPickleCache. """ See IPickleCache.
...@@ -383,12 +368,12 @@ class PickleCache(object): ...@@ -383,12 +368,12 @@ class PickleCache(object):
@_sweeping_ring @_sweeping_ring
def _sweep(self, target, target_size_bytes=0): def _sweep(self, target, target_size_bytes=0):
# lock # lock
node = self.ring.next # We can't mutate while we're iterating, so store ejections by index here
ejected = 0 # (deleting by index is potentially more efficient then by value)
to_eject = []
while (node is not self.ring for i, node in enumerate(self.ring):
and (self.non_ghost_count > target if self.non_ghost_count <= target and (self.total_estimated_size <= target_size_bytes or not target_size_bytes):
or (target_size_bytes and self.total_estimated_size > target_size_bytes))): break
if node.object._p_state == UPTODATE: if node.object._p_state == UPTODATE:
# The C implementation will only evict things that are specifically # The C implementation will only evict things that are specifically
...@@ -412,9 +397,14 @@ class PickleCache(object): ...@@ -412,9 +397,14 @@ class PickleCache(object):
# 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(node.object, _SWEEPABLE_TYPES)):
ejected += 1 to_eject.append(i)
self.__remove_from_ring(node) self.non_ghost_count -= 1
node = node.next
for i in reversed(to_eject):
del self.ring[i]
ejected = len(to_eject)
del to_eject
if ejected and _SWEEP_NEEDS_GC: if ejected and _SWEEP_NEEDS_GC:
# See comments on _SWEEP_NEEDS_GC # See comments on _SWEEP_NEEDS_GC
gc.collect() gc.collect()
...@@ -425,12 +415,15 @@ class PickleCache(object): ...@@ -425,12 +415,15 @@ class PickleCache(object):
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()
node = self.ring.next try:
while node is not self.ring: self.ring.remove(_RingWrapper(value))
if node.object is value: except ValueError:
self.__remove_from_ring(node) # The ring has been corrupted!
break # This should be impossible in real life; a contrived
node = node.next # 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]
...@@ -441,9 +434,3 @@ class PickleCache(object): ...@@ -441,9 +434,3 @@ class PickleCache(object):
persistent_class._p_invalidate() persistent_class._p_invalidate()
except AttributeError: except AttributeError:
pass pass
def __remove_from_ring(self, node):
"""Take the node, which previously contained a non-ghost, out of the ring."""
node.object = None
node.prev.next, node.next.prev = node.next, node.prev
self.non_ghost_count -= 1
...@@ -480,7 +480,6 @@ class PickleCacheTests(unittest.TestCase): ...@@ -480,7 +480,6 @@ class PickleCacheTests(unittest.TestCase):
gc.collect() # banish the ghosts who are no longer in the ring gc.collect() # banish the ghosts who are no longer in the ring
self.assertEqual(cache.cache_non_ghost_count, 0) self.assertEqual(cache.cache_non_ghost_count, 0)
self.assertTrue(cache.ring.next is cache.ring)
for oid in oids: for oid in oids:
self.assertTrue(cache.get(oid) is None) self.assertTrue(cache.get(oid) is None)
...@@ -503,7 +502,6 @@ class PickleCacheTests(unittest.TestCase): ...@@ -503,7 +502,6 @@ class PickleCacheTests(unittest.TestCase):
gc.collect() # banish the ghosts who are no longer in the ring gc.collect() # banish the ghosts who are no longer in the ring
self.assertEqual(cache.cache_non_ghost_count, 1) self.assertEqual(cache.cache_non_ghost_count, 1)
self.assertTrue(cache.ring.next is not cache.ring)
self.assertTrue(cache.get(oids[0]) is not None) self.assertTrue(cache.get(oids[0]) is not None)
for oid in oids[1:]: for oid in oids[1:]:
...@@ -527,7 +525,6 @@ class PickleCacheTests(unittest.TestCase): ...@@ -527,7 +525,6 @@ class PickleCacheTests(unittest.TestCase):
gc.collect() # banish the ghosts who are no longer in the ring gc.collect() # banish the ghosts who are no longer in the ring
self.assertEqual(cache.cache_non_ghost_count, 1) self.assertEqual(cache.cache_non_ghost_count, 1)
self.assertTrue(cache.ring.next is not cache.ring)
self.assertTrue(cache.get(oids[0]) is not None) self.assertTrue(cache.get(oids[0]) is not None)
for oid in oids[1:]: for oid in oids[1:]:
...@@ -980,9 +977,10 @@ class PickleCacheTests(unittest.TestCase): ...@@ -980,9 +977,10 @@ class PickleCacheTests(unittest.TestCase):
p._p_state = 0 # non-ghost, get in the ring p._p_state = 0 # non-ghost, get in the ring
cache[p._p_oid] = p cache[p._p_oid] = p
self.assertEqual(cache.cache_non_ghost_count, 1)
self.assertEqual(cache.ring.next.object, p) self.assertEqual(cache.ring[0].object, p)
cache.ring.next.object = None cache.ring[0].object = 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)
...@@ -1067,6 +1065,11 @@ class PickleCacheTests(unittest.TestCase): ...@@ -1067,6 +1065,11 @@ 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