gevent.local.local is slow

Created on 23 Sep 2017  路  6Comments  路  Source: gevent/gevent

  • gevent version: 1.2/master
  • Python version: All
  • Operating System: All

Description:

gevent.local.local is quite slow compared to the (C-based) stdlib version.

Here we are in CPython 3.4 accessing a single variable:

In [27]: %timeit gevent_local.attr
9.05 碌s 卤 417 ns per loop (mean 卤 std. dev. of 7 runs, 100000 loops each)
In [33]: %timeit native_local.attr
146 ns 卤 3.03 ns per loop (mean 卤 std. dev. of 7 runs, 10000000 loops each)

Or, 9,050ns +- 417ns vs 146ns +- 3.03ns. A 61x difference.

It's approximately the same on Python 2.7.

CPython 3.6 is better in both cases in absolute terms, but in relative terms the difference is now a larger ~78x:

In [6]: %timeit gevent_local.attr
6.35 碌s 卤 113 ns per loop (mean 卤 std. dev. of 7 runs, 100000 loops each)

In [9]: %timeit native_local.attr
81.6 ns 卤 0.786 ns per loop (mean 卤 std. dev. of 7 runs, 10000000 loops each)

The numbers are all better on PyPy2: 694ns vs 32ns, or only 21x slower.

For comparison, the time for a normal instance attribute access on Python 3.6 is about 46ns, 116ns under Python 3.4 and 2.7, and 29ns in PyPy.

Any improvements to gevent.local (which is based on the Python implementation _threading_local from Python 3.4---which hasn't changed from 3.1 up through 3.7) would be welcome.

Enhancement

Most helpful comment

OK, with the latest code here are the timings:

| Operation | PyPy 2 | Python 2.7 | Python 3.6 |
|---------------------- |-------: |-----------: |-----------: |
| getattr local | 79ns | 1580ns | 1120ns |
| setattr local | 86ns | 1600ns | 1090ns |
| getattr subclass | 81ns | 1640ns | 1090ns |
| setattr subclass | 84ns | 1560ns | 1090ns |

Compared with the original code (master at 13d860ae84a5aa5dec384d9fd1d67c2a642c9686), showing the relative slowdown vs native threading.local:

| Operation | PyPy 2 | Python 2.7 | Python 3.6 |
|---------------------- |-------: |-----------: |-----------: |
| getattr local | 274ns | 7980ns | 7160ns |
| setattr local | 275ns | 8350ns | 7340ns |
| getattr subclass | 269ns | 8440ns | 7110ns |
| setattr subclass | 276ns | 8210ns | 6760ns |
| getattr native local | 2ns | 139ns | 73ns |
| setattr native local | 2ns | 168ns | 98ns |
| getattr slowdown | 137x | 57x | 98x |
| setattr slowdown | 137x | 50x | 75x |

That shows improvements (and the new slowdown relative to native of):

| Operation | PyPy 2 | Python 2.7 | Python 3.6 |
|------------------ |-------: |-----------: |-----------: |
| getattr local | 3.5x | 5.1x | 6.4x |
| setattr local | 3.2x | 5.2x | 6.8x |
| getattr subclass | 3.8x | 5.1x | 6.5x |
| setattr subclass | 3.3x | 5.3x | 6.2x |
| getattr slowdown | 43x | 11x | 15x |
| setattr slowdown | 43x | 9.5x | 11x |

A large, across the board improvement.

All 6 comments

Python issue that provided that implementation: https://bugs.python.org/issue9707

A couple of constraints: __slots__ are not greenlet/thread local, and class properties must be available by default even if not in the __dict__.

Some relatively low-hanging fruit (removing the use of contextlib, inlining some common cases) gets the Python 3.4 number down to about 2.6us, a 3.5x improvement.

Implementing the complete attribute access protocol, including dealing with all types of data descriptors (which means functions!), gets Python 3.4 to about 2.7us, and Python 3.6 to 1.72us. Interestingly, PyPy is almost unchanged, decreasing slightly to ~630ns. Introducing the same optimization that CPython uses when local isn't subclassed gets the Python 3.4 number down to 1.76us, an impressive 5x improvement.

This lets us avoid using a lock on attribute access (since we are no longer swizzling the __dict__ attribute in a visible way.

We could also almost avoid using a lock to cover __init__ as well, but there's the issue that multiple dependent values in __slots__ could potentially be inconsistent, because they are always shared among all greenlets, if two __init__ execute at once. (Imagine an object that keeps a counter and a timestamp in __slots__.) However, CPython does not take a lock when running subclass __init__, so I don't see why we should either.

OK, with the latest code here are the timings:

| Operation | PyPy 2 | Python 2.7 | Python 3.6 |
|---------------------- |-------: |-----------: |-----------: |
| getattr local | 79ns | 1580ns | 1120ns |
| setattr local | 86ns | 1600ns | 1090ns |
| getattr subclass | 81ns | 1640ns | 1090ns |
| setattr subclass | 84ns | 1560ns | 1090ns |

Compared with the original code (master at 13d860ae84a5aa5dec384d9fd1d67c2a642c9686), showing the relative slowdown vs native threading.local:

| Operation | PyPy 2 | Python 2.7 | Python 3.6 |
|---------------------- |-------: |-----------: |-----------: |
| getattr local | 274ns | 7980ns | 7160ns |
| setattr local | 275ns | 8350ns | 7340ns |
| getattr subclass | 269ns | 8440ns | 7110ns |
| setattr subclass | 276ns | 8210ns | 6760ns |
| getattr native local | 2ns | 139ns | 73ns |
| setattr native local | 2ns | 168ns | 98ns |
| getattr slowdown | 137x | 57x | 98x |
| setattr slowdown | 137x | 50x | 75x |

That shows improvements (and the new slowdown relative to native of):

| Operation | PyPy 2 | Python 2.7 | Python 3.6 |
|------------------ |-------: |-----------: |-----------: |
| getattr local | 3.5x | 5.1x | 6.4x |
| setattr local | 3.2x | 5.2x | 6.8x |
| getattr subclass | 3.8x | 5.1x | 6.5x |
| setattr subclass | 3.3x | 5.3x | 6.2x |
| getattr slowdown | 43x | 11x | 15x |
| setattr slowdown | 43x | 9.5x | 11x |

A large, across the board improvement.

Trivially compiling with Cython approximately speeds things up by 2-3x more again. For example, the CPython 2.7 numbers go to 583, 573, 574, 561, in order. I'm not sure whether or not that's worth it though.

Brilliant!

Was this page helpful?
0 / 5 - 0 ratings