Commit 5ba9ab73 authored by Jason Madden's avatar Jason Madden

Compile local.py with Cython when available.

This makes things *much* faster. We had to make some small code
changes to acheive this (things like directly accessing variables
instead of redirecting through object.__getattribute__), but this
didn't impart PyPy at all (in fact it may actually be faster). New
timings compared to previous commit:

| Operation             | PyPy 2        | Python 2.7    | Python 3.6    |
|---------------------- |-------:       |-----------:   |-----------:   |
| getattr local         |   76ns        |      233ns    |      206ns    |
| setattr local         |   79ns        |      232ns    |      202ns    |
| getattr subclass      |   77ns        |      238ns    |      203ns    |
| setattr subclass      |   75ns        |      238ns    |      204ns    |
| getattr native local  |    2ns        |      139ns    |       73ns    |
| setattr native local  |    2ns        |      168ns    |       98ns    |
| getattr slowdown        	|  38x 	|     1.7x 	|     2.8x 	|
| setattr slowdown        	|  39x 	|     1.7x 	|     2.1x 	|
parent 9708c0e0
......@@ -54,12 +54,17 @@
- 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 between 3 and 5 times faster
getting, setting and deleting attributes on CPython (the fastest
access occurs when ``local`` is not subclassed). This involved
- :class:`gevent.local.local` objects are now approximately 3.5 times faster
getting, setting and deleting attributes on PyPy. This involved
implementing more of the attribute protocols directly. Please open
an issue if you have any compatibility problems. See :issue:`1020`.
- :class:`gevent.local.local` is compiled with Cython on CPython. It
was already 5 to 6 times faster due to the work on :issue:`1020`,
and compiling it with Cython makes it another 5 times faster, for a
total speed up of about 35 times. It is now in the same ball park as
the native :class:`threading.local` class.
1.2.2 (2017-06-05)
==================
......
......@@ -12,7 +12,7 @@ export PATH:=$(BUILD_RUNTIMES)/snakepit:$(TOOLS):$(PATH)
export LC_ALL=C.UTF-8
all: src/gevent/libev/gevent.corecext.c src/gevent/gevent.ares.c src/gevent/gevent._semaphore.c
all: src/gevent/libev/gevent.corecext.c src/gevent/gevent.ares.c src/gevent/gevent._semaphore.c src/gevent/gevent._local.c
src/gevent/libev/gevent.corecext.c: src/gevent/libev/corecext.ppyx src/gevent/libev/libev.pxd util/cythonpp.py
$(PYTHON) util/cythonpp.py -o gevent.corecext.c --module-name gevent.libev.corecext.pyx src/gevent/libev/corecext.ppyx
......@@ -34,11 +34,17 @@ src/gevent/gevent._semaphore.c: src/gevent/_semaphore.py src/gevent/_semaphore.p
mv gevent._semaphore.* src/gevent/
# rm src/gevent/_semaphore.py
src/gevent/gevent._local.c: src/gevent/local.py
$(CYTHON) -o gevent._local.c src/gevent/local.py
mv gevent._local.* src/gevent/
clean:
rm -f corecext.pyx src/gevent/libev/corecext.pyx
rm -f gevent.corecext.c gevent.corecext.h src/gevent/libev/gevent.corecext.c src/gevent/libev/gevent.corecext.h
rm -f gevent.ares.c gevent.ares.h src/gevent/gevent.ares.c src/gevent/gevent.ares.h
rm -f gevent._semaphore.c gevent._semaphore.h src/gevent/gevent._semaphore.c src/gevent/gevent._semaphore.h
rm -f gevent._local.c gevent._local.h src/gevent/gevent._local.c src/gevent/gevent._local.h
rm -f src/gevent/*.so src/gevent/libev/*.so
rm -rf src/gevent/libev/*.o src/gevent/*.o
rm -rf src/gevent/__pycache__ src/greentest/__pycache__ src/gevent/libev/__pycache__
......
......@@ -7,3 +7,5 @@ cython -o gevent.ares.c src\gevent\ares.pyx
move gevent.ares.* src\gevent
cython -o gevent._semaphore.c src\gevent\_semaphore.py
move gevent._semaphore.* src\gevent
cython -o gevent._local.c src\gevent\local.py
move gevent._local.c src\gevent
......@@ -55,10 +55,15 @@ from _setupares import ARES
SEMAPHORE = Extension(name="gevent._semaphore",
sources=["src/gevent/gevent._semaphore.c"])
LOCAL = Extension(name="gevent.local",
sources=["src/gevent/gevent._local.c"])
EXT_MODULES = [
CORE,
ARES,
SEMAPHORE,
LOCAL,
]
cffi_modules = ['src/gevent/libev/_corecffi_build.py:ffi']
......@@ -67,6 +72,7 @@ if PYPY:
install_requires = []
setup_requires = []
EXT_MODULES.remove(CORE)
EXT_MODULES.remove(LOCAL)
EXT_MODULES.remove(SEMAPHORE)
# By building the semaphore with Cython under PyPy, we get
# atomic operations (specifically, exiting/releasing), at the
......
......@@ -16,10 +16,10 @@ _version_info = namedtuple('version_info',
#: The programatic version identifier. The fields have (roughly) the
#: same meaning as :data:`sys.version_info`
#: Deprecated in 1.2.
version_info = _version_info(1, 2, 2, 'dev', 0)
version_info = _version_info(1, 3, 0, 'dev', 0)
#: The human-readable PEP 440 version identifier
__version__ = '1.2.3.dev0'
__version__ = '1.3.0.dev0'
__all__ = ['get_hub',
......
# cython: auto_pickle=False
cdef class _wrefdict(dict):
cdef object __weakref__
cdef class _localimpl:
cdef str key
cdef dict dicts
cdef tuple localargs
cdef object __weakref__
cdef dict create_dict(self)
cdef dict get_dict(self)
cdef class local:
cdef _localimpl _local__impl
cdef _local__copy_dict_from(self, _localimpl impl, dict duplicate)
......@@ -150,6 +150,7 @@ affects what we see:
(which are shared across all greenlets) switches during ``__init__``.
"""
from __future__ import print_function
from copy import copy
from weakref import ref
......@@ -162,20 +163,23 @@ __all__ = ["local"]
class _wrefdict(dict):
"""A dict that can be weak referenced"""
_osa = object.__setattr__
_oga = object.__getattribute__
class _localimpl(object):
"""A class managing thread-local dicts"""
__slots__ = ('key', 'dicts', 'localargs', '__weakref__',)
def __init__(self):
def __init__(self, args, kwargs):
# The key used in the Thread objects' attribute dicts.
# We keep it a string for speed but make it unlikely to clash with
# a "real" attribute.
self.key = '_threading_local._localimpl.' + str(id(self))
# { id(Thread) -> (ref(Thread), thread-local dict) }
self.dicts = _wrefdict()
self.localargs = args, kwargs
# We need to create the thread dict in anticipation of
# __init__ being called, to make sure we don't call it
# again ourselves. MUST do this before setting any attributes.
self.create_dict()
def get_dict(self):
"""Return the dict for the current thread. Raises KeyError if none
......@@ -214,7 +218,7 @@ class _localimpl(object):
rawlink(thread_deleted)
wrthread = ref(thread)
def local_deleted(_, key=key, wrthread=wrthread):
def local_deleted(_, key=key, wrthread=wrthread, thread_deleted=thread_deleted):
# When the localimpl is deleted, remove the thread attribute.
thread = wrthread()
if thread is not None:
......@@ -233,7 +237,6 @@ class _localimpl(object):
return localdict
_impl_getter = None
_marker = object()
class local(object):
......@@ -242,26 +245,24 @@ class local(object):
"""
__slots__ = ('_local__impl',)
def __new__(cls, *args, **kw):
def __cinit__(self, *args, **kw):
if args or kw:
if cls.__init__ == object.__init__:
raise TypeError("Initialization arguments are not supported")
self = object.__new__(cls)
impl = _localimpl()
impl.localargs = (args, kw)
_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
# again ourselves.
impl.create_dict()
return self
if type(self).__init__ == object.__init__:
raise TypeError("Initialization arguments are not supported", args, kw)
impl = _localimpl(args, kw)
self._local__impl = impl # pylint:disable=attribute-defined-outside-init
def __getattribute__(self, name): # pylint:disable=too-many-return-statements
if name == '__class__':
return _oga(self, name)
if name in ('__class__', '_local__impl', '__cinit__'):
# The _local__impl and __cinit__ won't be hit by the
# Cython version, if we've done things right. If we haven't,
# they will be, and this will produce an error.
return object.__getattribute__(self, name)
# Begin inlined function _get_dict()
impl = _impl_getter(self, local)
# Using object.__getattribute__ here disables Cython knowing the
# type of the object and using cdef calls to it.
impl = self._local__impl
try:
dct = impl.get_dict()
except KeyError:
......@@ -288,7 +289,7 @@ class local(object):
# 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)
return dct[name] if name in dct else object.__getattribute__(self, name)
# NOTE: If this is a descriptor, this will invoke its __get__.
# A broken descriptor that doesn't return itself when called with
......@@ -341,10 +342,14 @@ class local(object):
if name == '__dict__':
raise AttributeError(
"%r object attribute '__dict__' is read-only"
% self.__class__.__name__)
% type(self))
if name == '_local__impl':
object.__setattr__(self, '_local__impl', value)
return
# Begin inlined function _get_dict()
impl = _impl_getter(self, local)
impl = self._local__impl
try:
dct = impl.get_dict()
except KeyError:
......@@ -388,7 +393,7 @@ class local(object):
# Otherwise it goes directly in the dict
# Begin inlined function _get_dict()
impl = _impl_getter(self, local)
impl = self._local__impl
try:
dct = impl.get_dict()
except KeyError:
......@@ -403,23 +408,39 @@ class local(object):
raise AttributeError(name)
def __copy__(self):
impl = _oga(self, '_local__impl')
current = getcurrent()
currentId = id(current)
impl = self._local__impl
d = impl.get_dict()
duplicate = copy(d)
cls = type(self)
if cls.__init__ != object.__init__:
args, kw = impl.localargs
instance = cls(*args, **kw)
else:
instance = cls()
args, kw = impl.localargs
instance = cls(*args, **kw)
local._local__copy_dict_from(instance, impl, duplicate)
return instance
new_impl = object.__getattribute__(instance, '_local__impl')
def _local__copy_dict_from(self, impl, duplicate):
current = getcurrent()
currentId = id(current)
new_impl = self._local__impl
assert new_impl is not impl
tpl = new_impl.dicts[currentId]
new_impl.dicts[currentId] = (tpl[0], duplicate)
return instance
_impl_getter = local._local__impl.__get__
# Cython doesn't let us use __new__, it requires
# __cinit__. But we need __new__ if we're not compiled
# (e.g., on PyPy). So we set it at runtime. Cython
# will raise an error if we're compiled.
def __new__(cls, *args, **kw):
self = super(local, cls).__new__(cls)
# We get the cls in *args for some reason
# too when we do it this way.
self.__cinit__(*args[1:], **kw)
return self
try:
local.__new__ = __new__
except TypeError:
pass
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