Commit b3537301 authored by Jason Madden's avatar Jason Madden Committed by GitHub

Merge pull request #1120 from gevent/faster-stack

Speed up Greenlet creation on CPython
parents b84e1bd9 8d9ed09b
......@@ -87,13 +87,15 @@
- Greenlet objects now keep track of their spawning parent greenlet
and the code location that spawned them, in addition to maintaining
a "spawn tree local" mapping. Based on a proposal from PayPal and
comments by Mahmoud Hashemi and Kurt Rose. See :issue:`755` and
:pr:`1115`. As always, feedback is appreciated.
a "spawn tree local" mapping. This adds some runtime overhead in
relative terms, but absolute numbers are still relatively small.
Based on a proposal from PayPal and comments by Mahmoud Hashemi and
Kurt Rose. See :issue:`755` and :pr:`1115`. As always, feedback is
appreciated.
- The :mod:`gevent.greenlet` module is now compiled with Cython to
offset any performance decrease due to :issue:`755`. Please open
issues for any compatibility concerns. See :pr:`1115`.
issues for any compatibility concerns. See :pr:`1115` and :pr:`1120`.
- Greenlet objects now have a ``minimal_ident`` property. It functions
similarly to ``Thread.ident`` or ``id`` by uniquely identifying the
......
......@@ -4,6 +4,8 @@ cimport cython
from gevent.__ident cimport IdentRegistry
cdef bint _greenlet_imported
cdef bint _PYPY
cdef sys_getframe
cdef sys_exc_info
cdef extern from "greenlet/greenlet.h":
......@@ -25,6 +27,23 @@ cdef inline void greenlet_init():
PyGreenlet_Import()
_greenlet_imported = True
cdef extern from "Python.h":
ctypedef class types.CodeType [object PyCodeObject]:
pass
cdef extern from "frameobject.h":
ctypedef class types.FrameType [object PyFrameObject]:
cdef CodeType f_code
cdef int f_lineno
# We can't declare this in the object, because it's
# allowed to be NULL, and Cython can't handle that.
# We have to go through the python machinery to get a
# proper None instead.
# cdef FrameType f_back
cdef void _init()
cdef class SpawnedLink:
......@@ -42,18 +61,18 @@ cdef class FailureSpawnedLink(SpawnedLink):
@cython.final
@cython.internal
cdef class _Frame:
cdef readonly object f_code
cdef readonly CodeType f_code
cdef readonly int f_lineno
cdef public _Frame f_back
cdef readonly _Frame f_back
@cython.final
@cython.locals(
previous=_Frame,
first=_Frame,
next_frame=_Frame)
cdef _Frame _extract_stack(int limit, _Frame f_back)
@cython.locals(frames=list,frame=FrameType)
cdef inline list _extract_stack(int limit)
@cython.final
@cython.locals(previous=_Frame, frame=tuple, f=_Frame)
cdef _Frame _Frame_from_list(list frames)
cdef class Greenlet(greenlet):
......@@ -61,7 +80,10 @@ cdef class Greenlet(greenlet):
cdef readonly args
cdef readonly object spawning_greenlet
cdef public dict spawn_tree_locals
cdef readonly _Frame spawning_stack
# This is accessed with getattr() dynamically so it
# must be visible to Python
cdef readonly list _spawning_stack_frames
cdef list _links
cdef tuple _exc_info
......
......@@ -2,13 +2,15 @@
# cython: auto_pickle=False,embedsignature=True,always_allow_keywords=False
from __future__ import absolute_import, print_function, division
import sys
from sys import _getframe as sys_getframe
from sys import exc_info as sys_exc_info
from weakref import ref as wref
from greenlet import greenlet
from gevent._compat import reraise
from gevent._compat import PYPY as _PYPY
from gevent._tblib import dump_traceback
from gevent._tblib import load_traceback
from gevent.hub import GreenletExit
......@@ -18,8 +20,7 @@ from gevent.hub import get_hub
from gevent.hub import iwait
from gevent.hub import wait
from gevent.timeout import Timeout
_PYPY = hasattr(sys, 'pypy_version_info')
from gevent._util import Lazy
__all__ = [
......@@ -108,26 +109,28 @@ class _Frame(object):
self.f_lineno = f_lineno
self.f_back = None
f_globals = property(lambda _self: None)
@property
def f_globals(self):
return None
def _extract_stack(limit, f_back):
def _Frame_from_list(frames):
previous = None
frame = sys._getframe()
first = None
for frame in reversed(frames):
f = _Frame(*frame)
f.f_back = previous
previous = f
return previous
first = previous = _Frame(frame.f_code, frame.f_lineno)
limit -= 1
frame = frame.f_back
def _extract_stack(limit):
frame = sys_getframe()
frames = []
while limit and frame is not None:
limit -= 1
next_frame = _Frame(frame.f_code, frame.f_lineno)
previous.f_back = next_frame
previous = next_frame
frames.append((frame.f_code, frame.f_lineno))
frame = frame.f_back
previous.f_back = f_back
return first
return frames
_greenlet__init__ = greenlet.__init__
......@@ -274,9 +277,16 @@ class Greenlet(greenlet):
# Its children get separate locals.
spawner.spawn_tree_locals = self.spawn_tree_locals
self._spawning_stack_frames = _extract_stack(self.spawning_stack_limit)
self._spawning_stack_frames.extend(getattr(spawner, '_spawning_stack_frames', []))
self.spawning_stack = _extract_stack(self.spawning_stack_limit,
getattr(spawner, 'spawning_stack', None))
@Lazy
def spawning_stack(self):
# Store this in the __dict__. We don't use it from the C
# code. It's tempting to discard _spawning_stack_frames
# after this, but child greenlets may still be created
# that need it.
return _Frame_from_list(self._spawning_stack_frames)
def _get_minimal_ident(self):
reg = self.parent.ident_registry
......@@ -476,9 +486,9 @@ class Greenlet(greenlet):
.. versionadded:: 1.1
"""
exc_info = self._exc_info
if exc_info is not None and exc_info[0] is not None:
return (exc_info[0], exc_info[1], load_traceback(exc_info[2]))
ei = self._exc_info
if ei is not None and ei[0] is not None:
return (ei[0], ei[1], load_traceback(ei[2]))
def throw(self, *args):
"""Immediately switch into the greenlet and raise an exception in it.
......@@ -708,7 +718,7 @@ class Greenlet(greenlet):
try:
result = self._run(*self.args, **self.kwargs)
except: # pylint:disable=bare-except
self._report_error(sys.exc_info())
self._report_error(sys_exc_info())
return
self._report_result(result)
finally:
......@@ -801,7 +811,7 @@ class Greenlet(greenlet):
try:
link(self)
except: # pylint:disable=bare-except
self.parent.handle_error((link, self), *sys.exc_info())
self.parent.handle_error((link, self), *sys_exc_info())
class _dummy_event(object):
......@@ -828,7 +838,7 @@ def _kill(glet, exception, waiter):
glet.throw(exception)
except: # pylint:disable=bare-except
# XXX do we need this here?
glet.parent.handle_error(glet, *sys.exc_info())
glet.parent.handle_error(glet, *sys_exc_info())
if waiter is not None:
waiter.switch()
......@@ -863,7 +873,7 @@ def _killall3(greenlets, exception, waiter):
try:
g.throw(exception)
except: # pylint:disable=bare-except
g.parent.handle_error(g, *sys.exc_info())
g.parent.handle_error(g, *sys_exc_info())
if not g.dead:
diehards.append(g)
waiter.switch(diehards)
......@@ -875,7 +885,7 @@ def _killall(greenlets, exception):
try:
g.throw(exception)
except: # pylint:disable=bare-except
g.parent.handle_error(g, *sys.exc_info())
g.parent.handle_error(g, *sys_exc_info())
def killall(greenlets, exception=GreenletExit, block=True, timeout=None):
......
"""Benchmarking spawn() performance.
"""
from __future__ import print_function, absolute_import, division
import sys
import os
import time
import perf
try:
xrange
except NameError:
xrange = range
if hasattr(time, "perf_counter"):
curtime = time.perf_counter # 3.3
elif sys.platform.startswith('win'):
curtime = time.clock
else:
curtime = time.time
N = 100000
N = 10000
counter = 0
def incr(sleep, **_kwargs):
global counter
counter += 1
sleep(0)
def noop(_p):
pass
class Options(object):
# TODO: Add back an argument for that
eventlet_hub = None
loops = None
def __init__(self, sleep, join, **kwargs):
self.kwargs = kwargs
self.sleep = sleep
self.join = join
class Times(object):
def __init__(self,
spawn_duration,
sleep_duration=-1,
join_duration=-1):
self.spawn_duration = spawn_duration
self.sleep_duration = sleep_duration
self.join_duration = join_duration
def _report(name, delta):
print('%8s: %3.2f microseconds per greenlet' % (name, delta * 1000000.0 / N))
def test(spawn, sleep, kwargs):
start = curtime()
def _test(spawn, sleep, options):
global counter
counter = 0
before_spawn = perf.perf_counter()
for _ in xrange(N):
spawn(incr, sleep, **kwargs)
_report('spawning', curtime() - start)
assert counter == 0, counter
start = curtime()
sleep(0)
_report('sleep(0)', curtime() - start)
assert counter == N, (counter, N)
spawn(incr, sleep, **options.kwargs)
before_sleep = perf.perf_counter()
if options.sleep:
assert counter == 0, counter
sleep(0)
after_sleep = perf.perf_counter()
assert counter == N, (counter, N)
else:
after_sleep = before_sleep
if options.join:
before_join = perf.perf_counter()
options.join()
after_join = perf.perf_counter()
join_duration = after_join - before_join
else:
join_duration = -1
return Times(before_sleep - before_spawn,
after_sleep - before_sleep,
join_duration)
def test(spawn, sleep, options):
all_times = [_test(spawn, sleep, options)
for _ in xrange(options.loops)]
spawn_duration = sum(x.spawn_duration for x in all_times)
sleep_duration = sum(x.sleep_duration for x in all_times)
join_duration = sum(x.sleep_duration for x in all_times
if x != -1)
return Times(spawn_duration, sleep_duration, join_duration)
def bench_none(options):
kwargs = options.kwargs
start = curtime()
for _ in xrange(N):
incr(noop, **kwargs)
assert counter == N, (counter, N)
_report('noop', curtime() - start)
options.sleep = False
def spawn(f, sleep, **kwargs):
return f(sleep, **kwargs)
from time import sleep
return test(spawn,
sleep,
options)
def bench_gevent(options):
import gevent
print('using gevent from %s' % gevent.__file__)
from gevent import spawn, sleep
test(spawn, sleep, options.kwargs)
return test(spawn, sleep, options)
def bench_geventraw(options):
import gevent
print('using gevent from %s' % gevent.__file__)
from gevent import sleep, spawn_raw
test(spawn_raw, sleep, options.kwargs)
return test(spawn_raw, sleep, options)
def bench_geventpool(options):
import gevent
print('using gevent from %s' % gevent.__file__)
from gevent import sleep
from gevent.pool import Pool
p = Pool()
test(p.spawn, sleep, options.kwargs)
start = curtime()
p.join()
_report('joining', curtime() - start)
if options.join:
options.join = p.join
times = test(p.spawn, sleep, options)
return times
def bench_eventlet(options):
try:
import eventlet
except ImportError:
if options.ignore_import_errors:
return
raise
print('using eventlet from %s' % eventlet.__file__)
from eventlet import spawn, sleep
from eventlet.hubs import use_hub
if options.eventlet_hub is not None:
use_hub(options.eventlet_hub)
test(spawn, sleep, options.kwargs)
def bench_all():
from time import sleep
error = 0
names = sorted(all())
for func in names:
cmd = '%s %s %s --ignore-import-errors' % (sys.executable, __file__, func)
print(cmd)
sys.stdout.flush()
sleep(0.01)
if os.system(cmd):
error = 1
print('%s failed' % cmd)
print('')
for func in names:
cmd = '%s %s --with-kwargs %s --ignore-import-errors' % (sys.executable, __file__, func)
print(cmd)
sys.stdout.flush()
if os.system(cmd):
error = 1
print('%s failed' % cmd)
print('')
if error:
sys.exit(1)
return test(spawn, sleep, options)
def all():
result = [x for x in globals() if x.startswith('bench_') and x != 'bench_all']
try:
result.sort(key=lambda x: globals()[x].func_code.co_firstlineno)
except AttributeError:
result.sort(key=lambda x: globals()[x].__code__.co_firstlineno)
result.sort()
result = [x.replace('bench_', '') for x in result]
return result
def all_functions():
return [globals()['bench_%s' % x] for x in all()]
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--with-kwargs', default=False, action='store_true')
parser.add_argument('--eventlet-hub')
parser.add_argument('--ignore-import-errors', action='store_true')
parser.add_argument('benchmark', choices=all() + ['all'])
options = parser.parse_args()
if options.with_kwargs:
options.kwargs = {'foo': 1, 'bar': 'hello'}
else:
options.kwargs = {}
if options.benchmark == 'all':
bench_all()
def worker_cmd(cmd, args):
cmd.extend(args.benchmark)
runner = perf.Runner(add_cmdline_args=worker_cmd)
runner.argparser.add_argument('benchmark',
nargs='*',
default='all',
choices=all() + ['all'])
def spawn_time(loops, func, options):
options.loops = loops
times = func(options)
return times.spawn_duration
def sleep_time(loops, func, options):
options.loops = loops
times = func(options)
return times.sleep_duration
def join_time(loops, func, options):
options.loops = loops
times = func(options)
return times.join_duration
args = runner.parse_args()
if 'all' in args.benchmark or args.benchmark == 'all':
args.benchmark = ['all']
names = all()
else:
function = globals()['bench_' + options.benchmark]
function(options)
names = args.benchmark
names = sorted(set(names))
for name in names:
runner.bench_time_func(name + ' spawn',
spawn_time,
globals()['bench_' + name],
Options(False, False),
inner_loops=N)
if name != 'none':
runner.bench_time_func(name + ' sleep',
sleep_time,
globals()['bench_' + name],
Options(True, False),
inner_loops=N)
if 'geventpool' in names:
runner.bench_time_func('geventpool join',
join_time,
bench_geventpool,
Options(True, True),
inner_loops=N)
for name in names:
runner.bench_time_func(name + ' spawn kwarg',
spawn_time,
globals()['bench_' + name],
Options(False, False, foo=1, bar='hello'),
inner_loops=N)
if __name__ == '__main__':
main()
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