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 @@
- Monkey-patching after the :mod:`ssl` module has been imported now
prints a warning because this can produce ``RecursionError``.
- :class:`gevent.local.local` objects are now about 3.4 times faster
reading simple attributes. See :issue:`1020`.
- :class:`gevent.local.local` objects are now between 3 and 5 times faster
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)
==================
......
......@@ -137,14 +137,24 @@ affects what we see:
Use a weak-reference to clear the greenlet link we establish in case
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 weakref import ref
from functools import partial
from gevent.hub import getcurrent
from gevent._compat import PYPY
from gevent.lock import RLock
__all__ = ["local"]
......@@ -157,7 +167,7 @@ _oga = object.__getattribute__
class _localimpl(object):
"""A class managing thread-local dicts"""
__slots__ = ('key', 'dicts', 'localargs', 'locallock', '__weakref__', 'dict_setter')
__slots__ = ('key', 'dicts', 'localargs', '__weakref__',)
def __init__(self):
# The key used in the Thread objects' attribute dicts.
......@@ -225,20 +235,14 @@ class _localimpl(object):
def _get_dict(self):
impl = _oga(self, '_local__impl')
orig_dct = _oga(self, '__dict__')
lock = impl.locallock
try:
dct = impl.get_dict()
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()
args, kw = impl.localargs
with lock:
self.__init__(*args, **kw)
self.__init__(*args, **kw)
setter = impl.dict_setter
return dct, orig_dct, lock, setter
return dct
_marker = object()
......@@ -256,8 +260,6 @@ class local(object):
self = object.__new__(cls)
impl = _localimpl()
impl.localargs = (args, kw)
impl.locallock = RLock()
impl.dict_setter = partial(_osa, self, '__dict__')
_osa(self, '_local__impl', impl)
# We need to create the thread dict in anticipation of
# __init__ being called, to make sure we don't call it
......@@ -265,10 +267,10 @@ class local(object):
impl.create_dict()
return self
def __getattribute__(self, name):
def __getattribute__(self, name): # pylint:disable=too-many-return-statements
if name == '__class__':
return _oga(self, name)
dct, orig_dct, lock, setter = _get_dict(self)
dct = _get_dict(self)
if name == '__dict__':
return dct
# If there's no possible way we can switch, because this
......@@ -281,54 +283,100 @@ class local(object):
# long as we were in here anyway.
# Similarly, a __getattr__ will still be called by _oga() if needed
# 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 not hasattr(type(self), name):
# We elide even setting the dict in the first place in
# this case.
if type_attr is _marker:
# If there is a dict value, and nothing in the type,
# it can't possibly be a descriptor, so it is just returned.
return dct[name]
else:
# It's not in the dict at all. Is it in the type, but
# not a descriptor (has no __get__)? Then it can't execute any
# code and so doesn't need the lock or dct swizzle
type_attr = getattr(type(self), name, _marker)
if type_attr is not _marker and not hasattr(type_attr, '__get__'):
# It's in the type *and* in the dict. If the type value is
# a data descriptor (defines __get__ *and* either __set__ or
# __delete__), then the type wins. If it's a non-data descriptor
# (defines just __get__), then the instance wins. If it's not a
# 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_type_attr.__get__(type_attr, self, type_self)
lock.acquire()
setter(dct)
try:
return _oga(self, name)
finally:
setter(orig_dct)
lock.release()
# It wasn't in the dict and it wasn't in the type.
# So the next step is to invoke type(self)__getattr__, if it
# exists, otherwise raise an AttributeError.
# we will invoke type(self).__getattr__ or raise an attribute error.
if hasattr(type_self, '__getattr__'):
return type_self.__getattr__(self, name)
raise AttributeError("%r object has no attribute '%s'"
% (type_self.__name__, name))
def __setattr__(self, name, value):
if name == '__dict__':
raise AttributeError(
"%r object attribute '__dict__' is read-only"
% self.__class__.__name__)
dct, orig_dct, lock, setter = _get_dict(self)
lock.acquire()
setter(dct)
try:
return _osa(self, name, value)
finally:
setter(orig_dct)
lock.release()
dct = _get_dict(self)
type_self = type(self)
if type_self is local:
# Optimization: If we're not subclassed, we can't
# have data descriptors, so this goes right in the dict.
dct[name] = value
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):
if name == '__dict__':
raise AttributeError(
"%r object attribute '__dict__' is read-only"
% self.__class__.__name__)
dct, orig_dct, lock, setter = _get_dict(self)
lock.acquire()
setter(dct)
dct = _get_dict(self)
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:
return object.__delattr__(self, name)
finally:
setter(orig_dct)
lock.release()
del dct[name]
except KeyError:
raise AttributeError(name)
def __copy__(self):
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