Commit df90b2c6 authored by Jason Madden's avatar Jason Madden

Further optimizations: completely elide the lock, fully implement the descriptor protocol.

parent a7b9b614
...@@ -54,8 +54,11 @@ ...@@ -54,8 +54,11 @@
- Monkey-patching after the :mod:`ssl` module has been imported now - Monkey-patching after the :mod:`ssl` module has been imported now
prints a warning because this can produce ``RecursionError``. prints a warning because this can produce ``RecursionError``.
- :class:`gevent.local.local` objects are now about 3.4 times faster - :class:`gevent.local.local` objects are now between 3 and 5 times faster
reading simple attributes. See :issue:`1020`. getting, setting and deleting attributes on CPython (the fastest
access occurs when ``local`` is not subclassed). This involved
implementing more of the attribute protocols directly. Please open
an issue if you have any compatibility problems. See :issue:`1020`.
1.2.2 (2017-06-05) 1.2.2 (2017-06-05)
================== ==================
......
...@@ -137,14 +137,24 @@ affects what we see: ...@@ -137,14 +137,24 @@ affects what we see:
Use a weak-reference to clear the greenlet link we establish in case Use a weak-reference to clear the greenlet link we establish in case
the local object dies before the greenlet does. the local object dies before the greenlet does.
.. versionchanged:: 1.3a1
Implement the methods for attribute access directly, handling
descriptors directly here. This allows removing the use of a lock
and facilitates greatly improved performance.
.. versionchanged:: 1.3a1
The ``__init__`` method of subclasses of ``local`` is no longer
called with a lock held. CPython does not use such a lock in its
native implementation. This could potentially show as a difference
if code that uses multiple dependent attributes in ``__slots__``
(which are shared across all greenlets) switches during ``__init__``.
""" """
from copy import copy from copy import copy
from weakref import ref from weakref import ref
from functools import partial
from gevent.hub import getcurrent from gevent.hub import getcurrent
from gevent._compat import PYPY from gevent._compat import PYPY
from gevent.lock import RLock
__all__ = ["local"] __all__ = ["local"]
...@@ -157,7 +167,7 @@ _oga = object.__getattribute__ ...@@ -157,7 +167,7 @@ _oga = object.__getattribute__
class _localimpl(object): class _localimpl(object):
"""A class managing thread-local dicts""" """A class managing thread-local dicts"""
__slots__ = ('key', 'dicts', 'localargs', 'locallock', '__weakref__', 'dict_setter') __slots__ = ('key', 'dicts', 'localargs', '__weakref__',)
def __init__(self): def __init__(self):
# The key used in the Thread objects' attribute dicts. # The key used in the Thread objects' attribute dicts.
...@@ -225,20 +235,14 @@ class _localimpl(object): ...@@ -225,20 +235,14 @@ class _localimpl(object):
def _get_dict(self): def _get_dict(self):
impl = _oga(self, '_local__impl') impl = _oga(self, '_local__impl')
orig_dct = _oga(self, '__dict__')
lock = impl.locallock
try: try:
dct = impl.get_dict() dct = impl.get_dict()
except KeyError: except KeyError:
# it's OK to acquire the lock here and not earlier, because the above code won't switch out
# however, subclassed __init__ might switch, so we do need to acquire the lock here
dct = impl.create_dict() dct = impl.create_dict()
args, kw = impl.localargs args, kw = impl.localargs
with lock:
self.__init__(*args, **kw) self.__init__(*args, **kw)
setter = impl.dict_setter return dct
return dct, orig_dct, lock, setter
_marker = object() _marker = object()
...@@ -256,8 +260,6 @@ class local(object): ...@@ -256,8 +260,6 @@ class local(object):
self = object.__new__(cls) self = object.__new__(cls)
impl = _localimpl() impl = _localimpl()
impl.localargs = (args, kw) impl.localargs = (args, kw)
impl.locallock = RLock()
impl.dict_setter = partial(_osa, self, '__dict__')
_osa(self, '_local__impl', impl) _osa(self, '_local__impl', impl)
# We need to create the thread dict in anticipation of # We need to create the thread dict in anticipation of
# __init__ being called, to make sure we don't call it # __init__ being called, to make sure we don't call it
...@@ -265,10 +267,10 @@ class local(object): ...@@ -265,10 +267,10 @@ class local(object):
impl.create_dict() impl.create_dict()
return self return self
def __getattribute__(self, name): def __getattribute__(self, name): # pylint:disable=too-many-return-statements
if name == '__class__': if name == '__class__':
return _oga(self, name) return _oga(self, name)
dct, orig_dct, lock, setter = _get_dict(self) dct = _get_dict(self)
if name == '__dict__': if name == '__dict__':
return dct return dct
# If there's no possible way we can switch, because this # If there's no possible way we can switch, because this
...@@ -281,54 +283,100 @@ class local(object): ...@@ -281,54 +283,100 @@ class local(object):
# long as we were in here anyway. # long as we were in here anyway.
# Similarly, a __getattr__ will still be called by _oga() if needed # Similarly, a __getattr__ will still be called by _oga() if needed
# if it's not in the dict. # if it's not in the dict.
type_self = type(self)
# Optimization: If we're not subclassed, then
# there can be no descriptors except for methods, which will
# never need to use __dict__.
if type_self is local:
return dct[name] if name in dct else _oga(self, name)
type_attr = getattr(type_self, name, _marker)
if name in dct: if name in dct:
if not hasattr(type(self), name): if type_attr is _marker:
# We elide even setting the dict in the first place in # If there is a dict value, and nothing in the type,
# this case. # it can't possibly be a descriptor, so it is just returned.
return dct[name] return dct[name]
else:
# It's not in the dict at all. Is it in the type, but # It's in the type *and* in the dict. If the type value is
# not a descriptor (has no __get__)? Then it can't execute any # a data descriptor (defines __get__ *and* either __set__ or
# code and so doesn't need the lock or dct swizzle # __delete__), then the type wins. If it's a non-data descriptor
type_attr = getattr(type(self), name, _marker) # (defines just __get__), then the instance wins. If it's not a
if type_attr is not _marker and not hasattr(type_attr, '__get__'): # descriptor at all (doesn't have __get__), the instance wins.
# NOTE that the docs for descriptors say that these methods must be
# defined on the *class* of the object in the type.
type_type_attr = type(type_attr)
if not hasattr(type_type_attr, '__get__'):
# Entirely not a descriptor. Instance wins.
return dct[name]
if hasattr(type_type_attr, '__set__') or hasattr(type_type_attr, '__delete__'):
# A data descriptor.
# arbitrary code execution while these run. If they touch self again,
# they'll call back into us and we'll repeat the dance.
return type_type_attr.__get__(type_attr, self, type_self)
# Last case is a non-data descriptor. Instance wins.
return dct[name]
elif type_attr is not _marker:
# It's not in the dict at all. Is it in the type?
type_type_attr = type(type_attr)
if not hasattr(type_type_attr, '__get__'):
# Not a descriptor, can't execute code
return type_attr return type_attr
return type_type_attr.__get__(type_attr, self, type_self)
lock.acquire() # It wasn't in the dict and it wasn't in the type.
setter(dct) # So the next step is to invoke type(self)__getattr__, if it
try: # exists, otherwise raise an AttributeError.
return _oga(self, name) # we will invoke type(self).__getattr__ or raise an attribute error.
finally: if hasattr(type_self, '__getattr__'):
setter(orig_dct) return type_self.__getattr__(self, name)
lock.release() raise AttributeError("%r object has no attribute '%s'"
% (type_self.__name__, name))
def __setattr__(self, name, value): def __setattr__(self, name, value):
if name == '__dict__': if name == '__dict__':
raise AttributeError( raise AttributeError(
"%r object attribute '__dict__' is read-only" "%r object attribute '__dict__' is read-only"
% self.__class__.__name__) % self.__class__.__name__)
dct, orig_dct, lock, setter = _get_dict(self) dct = _get_dict(self)
lock.acquire()
setter(dct) type_self = type(self)
try: if type_self is local:
return _osa(self, name, value) # Optimization: If we're not subclassed, we can't
finally: # have data descriptors, so this goes right in the dict.
setter(orig_dct) dct[name] = value
lock.release() return
type_attr = getattr(type_self, name, _marker)
if type_attr is not _marker:
type_type_attr = type(type_attr)
if hasattr(type_type_attr, '__set__'):
# A data descriptor, like a property or a slot.
type_type_attr.__set__(type_attr, self, value)
return
# Otherwise it goes directly in the dict
dct[name] = value
def __delattr__(self, name): def __delattr__(self, name):
if name == '__dict__': if name == '__dict__':
raise AttributeError( raise AttributeError(
"%r object attribute '__dict__' is read-only" "%r object attribute '__dict__' is read-only"
% self.__class__.__name__) % self.__class__.__name__)
dct, orig_dct, lock, setter = _get_dict(self)
lock.acquire() dct = _get_dict(self)
setter(dct) type_self = type(self)
type_attr = getattr(type_self, name, _marker)
if type_attr is not _marker:
type_type_attr = type(type_attr)
if hasattr(type_type_attr, '__delete__'):
# A data descriptor, like a property or a slot.
type_type_attr.__delete__(type_attr, self)
return
# Otherwise it goes directly in the dict
try: try:
return object.__delattr__(self, name) del dct[name]
finally: except KeyError:
setter(orig_dct) raise AttributeError(name)
lock.release()
def __copy__(self): def __copy__(self):
impl = object.__getattribute__(self, '_local__impl') impl = object.__getattribute__(self, '_local__impl')
......
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