Add PEP 509 (dict.__version__) and 510 (func.specialize)
This commit is contained in:
parent
d1dee05fa0
commit
534db9e409
|
@ -0,0 +1,387 @@
|
||||||
|
PEP: 509
|
||||||
|
Title: Add dict.__version__
|
||||||
|
Version: $Revision$
|
||||||
|
Last-Modified: $Date$
|
||||||
|
Author: Victor Stinner <victor.stinner@gmail.com>
|
||||||
|
Status: Draft
|
||||||
|
Type: Standards Track
|
||||||
|
Content-Type: text/x-rst
|
||||||
|
Created: 4-January-2016
|
||||||
|
Python-Version: 3.6
|
||||||
|
|
||||||
|
|
||||||
|
Abstract
|
||||||
|
========
|
||||||
|
|
||||||
|
Add a new read-only ``__version__`` property to ``dict`` and
|
||||||
|
``collections.UserDict`` types, incremented at each change.
|
||||||
|
|
||||||
|
|
||||||
|
Rationale
|
||||||
|
=========
|
||||||
|
|
||||||
|
In Python, the builtin ``dict`` type is used by many instructions. For
|
||||||
|
example, the ``LOAD_GLOBAL`` instruction searchs for a variable in the
|
||||||
|
global namespace, or in the builtins namespace (two dict lookups).
|
||||||
|
Python uses ``dict`` for the builtins namespace, globals namespace, type
|
||||||
|
namespaces, instance namespaces, etc. The local namespace (namespace of
|
||||||
|
a function) is usually optimized to an array, but it can be a dict too.
|
||||||
|
|
||||||
|
Python is hard to optimize because almost everything is mutable: builtin
|
||||||
|
functions, function code, global variables, local variables, ... can be
|
||||||
|
modified at runtime. Implementing optimizations respecting the Python
|
||||||
|
semantic requires to detect when "something changes": we will call these
|
||||||
|
checks "guards".
|
||||||
|
|
||||||
|
The speedup of optimizations depends on the speed of guard checks. This
|
||||||
|
PEP proposes to add a version to dictionaries to implement efficient
|
||||||
|
guards on namespaces.
|
||||||
|
|
||||||
|
Example of optimization: replace loading a global variable with a
|
||||||
|
constant. This optimization requires a guard on the global variable to
|
||||||
|
check if it was modified. If the variable is modified, the variable must
|
||||||
|
be loaded at runtime, instead of using the constant.
|
||||||
|
|
||||||
|
|
||||||
|
Guard example
|
||||||
|
=============
|
||||||
|
|
||||||
|
Pseudo-code of an efficient guard to check if a dictionary key was
|
||||||
|
modified (created, updated or deleted)::
|
||||||
|
|
||||||
|
UNSET = object()
|
||||||
|
|
||||||
|
class Guard:
|
||||||
|
def __init__(self, dict, key):
|
||||||
|
self.dict = dict
|
||||||
|
self.key = key
|
||||||
|
self.value = dict.get(key, UNSET)
|
||||||
|
self.version = dict.__version__
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
"""Return True if the dictionary value did not changed."""
|
||||||
|
version = self.dict.__version__
|
||||||
|
if version == self.version:
|
||||||
|
# Fast-path: avoid the dictionary lookup
|
||||||
|
return True
|
||||||
|
|
||||||
|
value = self.dict.get(self.key, UNSET)
|
||||||
|
if value == self.value:
|
||||||
|
# another key was modified:
|
||||||
|
# cache the new dictionary version
|
||||||
|
self.version = version
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
Changes
|
||||||
|
=======
|
||||||
|
|
||||||
|
Add a read-only ``__version__`` property to builtin ``dict`` type and to
|
||||||
|
the ``collections.UserDict`` type. New empty dictionaries are initilized
|
||||||
|
to version ``0``. The version is incremented at each change:
|
||||||
|
|
||||||
|
* ``clear()`` if the dict was non-empty
|
||||||
|
* ``pop(key)`` if the key exists
|
||||||
|
* ``popitem()`` if the dict is non-empty
|
||||||
|
* ``setdefault(key, value)`` if the `key` does not exist
|
||||||
|
* ``__detitem__(key)`` if the key exists
|
||||||
|
* ``__setitem__(key, value)`` if the `key` doesn't exist or if the value
|
||||||
|
is different
|
||||||
|
* ``update(...)`` if new values are different than existing values (the
|
||||||
|
version can be incremented multiple times)
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
>>> d = {}
|
||||||
|
>>> d.__version__
|
||||||
|
0
|
||||||
|
>>> d['key'] = 'value'
|
||||||
|
>>> d.__version__
|
||||||
|
1
|
||||||
|
>>> d['key'] = 'new value'
|
||||||
|
>>> d.__version__
|
||||||
|
2
|
||||||
|
>>> del d['key']
|
||||||
|
>>> d.__version__
|
||||||
|
3
|
||||||
|
|
||||||
|
If a dictionary is created with items, the version is also incremented
|
||||||
|
at each dictionary insertion. Example::
|
||||||
|
|
||||||
|
>>> d=dict(x=7, y=33)
|
||||||
|
>>> d.__version__
|
||||||
|
2
|
||||||
|
|
||||||
|
The version is not incremented is an existing key is modified to the
|
||||||
|
same value, but only the identifier of the value is tested, not the
|
||||||
|
content of the value. Example::
|
||||||
|
|
||||||
|
>>> d={}
|
||||||
|
>>> value = object()
|
||||||
|
>>> d['key'] = value
|
||||||
|
>>> d.__version__
|
||||||
|
2
|
||||||
|
>>> d['key'] = value
|
||||||
|
>>> d.__version__
|
||||||
|
2
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
CPython uses some singleton like integers in the range [-5; 257],
|
||||||
|
empty tuple, empty strings, Unicode strings of a single character in
|
||||||
|
the range [U+0000; U+00FF], etc. When a key is set twice to the same
|
||||||
|
singleton, the version is not modified.
|
||||||
|
|
||||||
|
The PEP is designed to implement guards on namespaces, only the ``dict``
|
||||||
|
type can be used for namespaces in practice. ``collections.UserDict``
|
||||||
|
is modified because it must mimicks ``dict``. ``collections.Mapping`` is
|
||||||
|
unchanged.
|
||||||
|
|
||||||
|
|
||||||
|
Integer overflow
|
||||||
|
================
|
||||||
|
|
||||||
|
The implementation uses the C unsigned integer type ``size_t`` to store
|
||||||
|
the version. On 32-bit systems, the maximum version is ``2**32-1``
|
||||||
|
(more than ``4.2 * 10 ** 9``, 4 billions). On 64-bit systems, the maximum
|
||||||
|
version is ``2**64-1`` (more than ``1.8 * 10**19``).
|
||||||
|
|
||||||
|
The C code uses ``version++``. On integer overflow, the version is
|
||||||
|
wrapped to ``0`` (and then continue to be incremented).
|
||||||
|
|
||||||
|
The check ``dict.__version__ == old_version`` can be true after an
|
||||||
|
integer overflow, so a guard can return false even if the value changed,
|
||||||
|
which is wrong. The bug occurs if the dict is modified at least ``2**64``
|
||||||
|
times (on 64-bit system) between two checks of the guard.
|
||||||
|
|
||||||
|
Using a more complex type (ex: ``PyLongObject``) to avoid the overflow
|
||||||
|
would slow down operations on the ``dict`` type. Even if there is a
|
||||||
|
theorical risk of missing a value change, the risk is considered too low
|
||||||
|
compared to the slow down of using a more complex type.
|
||||||
|
|
||||||
|
|
||||||
|
Usage of dict.__version__
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Detect dictionary mutation during iteration
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
Currently, iterating on a dictionary only detects when the dictionary
|
||||||
|
size changes, but not when keys or values are modified. Using the
|
||||||
|
dictionary version, it would be possible to detect when keys and values
|
||||||
|
are modified.
|
||||||
|
|
||||||
|
See the `issue #19332: Guard against changing dict during iteration
|
||||||
|
<https://bugs.python.org/issue19332>`_.
|
||||||
|
|
||||||
|
|
||||||
|
astoptimizer of FAT Python
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
The astoptimizer of the FAT Python project implements many optimizations
|
||||||
|
which require guards on namespaces. Examples:
|
||||||
|
|
||||||
|
* Call pure builtins: to replace ``len("abc")`` with ``3``, guards on
|
||||||
|
``builtins.__dict__['len']`` and ``globals()['len']`` are required
|
||||||
|
* Loop unrolling: to unroll the loop ``for i in range(...): ...``,
|
||||||
|
guards on ``builtins.__dict__['range']`` and ``globals()['range']``
|
||||||
|
are required
|
||||||
|
|
||||||
|
The `FAT Python
|
||||||
|
<http://faster-cpython.readthedocs.org/fat_python.html>`_ project is a
|
||||||
|
static optimizer for Python 3.6.
|
||||||
|
|
||||||
|
|
||||||
|
Pyjion
|
||||||
|
------
|
||||||
|
|
||||||
|
According of Brett Cannon, one of the two main developers of Pyjion, Pyjion can
|
||||||
|
also benefit from dictionary version to implement optimizations.
|
||||||
|
|
||||||
|
Pyjion is a JIT compiler for Python based upon CoreCLR (Microsoft .NET Core
|
||||||
|
runtime).
|
||||||
|
|
||||||
|
|
||||||
|
Unladen Swallow
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Even if dictionary version was not explicitly mentionned, optimization globals
|
||||||
|
and builtins lookup was part of the Unladen Swallow plan: "Implement one of the
|
||||||
|
several proposed schemes for speeding lookups of globals and builtins."
|
||||||
|
Source: `Unladen Swallow ProjectPlan
|
||||||
|
<https://code.google.com/p/unladen-swallow/wiki/ProjectPlan>`_.
|
||||||
|
|
||||||
|
Unladen Swallow is a fork of CPython 2.6.1 adding a JIT compiler implemented
|
||||||
|
with LLVM. The project stopped in 2011: `Unladen Swallow Retrospective
|
||||||
|
<http://qinsb.blogspot.com.au/2011/03/unladen-swallow-retrospective.html>`_.
|
||||||
|
|
||||||
|
|
||||||
|
Implementation
|
||||||
|
==============
|
||||||
|
|
||||||
|
See the `issue #26058: Add dict.__version__ read-only property
|
||||||
|
<https://bugs.python.org/issue26058>`_.
|
||||||
|
|
||||||
|
On pybench and timeit microbenchmarks, the patch does not seem to add
|
||||||
|
any overhead on dictionary operations.
|
||||||
|
|
||||||
|
|
||||||
|
Alternatives
|
||||||
|
============
|
||||||
|
|
||||||
|
Add a version to each dict entry
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
A single version per dictionary requires to keep a strong reference to
|
||||||
|
the value which can keep the value alive longer than expected. If we add
|
||||||
|
also a version per dictionary entry, the guard can rely on the entry
|
||||||
|
version and so avoid the strong reference to the value (only strong
|
||||||
|
references to a dictionary and key are needed).
|
||||||
|
|
||||||
|
Changes: add a ``getversion(key)`` method to dictionary which returns
|
||||||
|
``None`` if the key doesn't exist. When a key is created or modified,
|
||||||
|
the entry version is set to the dictionary version which is incremented
|
||||||
|
at each change (create, modify, delete).
|
||||||
|
|
||||||
|
Pseudo-code of an efficient guard to check if a dict key was modified
|
||||||
|
using ``getversion()``::
|
||||||
|
|
||||||
|
UNSET = object()
|
||||||
|
|
||||||
|
class Guard:
|
||||||
|
def __init__(self, dict, key):
|
||||||
|
self.dict = dict
|
||||||
|
self.key = key
|
||||||
|
self.dict_version = dict.__version__
|
||||||
|
self.entry_version = dict.getversion(key)
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
"""Return True if the dictionary value did not changed."""
|
||||||
|
dict_version = self.dict.__version__
|
||||||
|
if dict_version == self.version:
|
||||||
|
# Fast-path: avoid the dictionary lookup
|
||||||
|
return True
|
||||||
|
|
||||||
|
# lookup in the dictionary, but get the entry version,
|
||||||
|
#not the value
|
||||||
|
entry_version = self.dict.getversion(self.key)
|
||||||
|
if entry_version == self.entry_version:
|
||||||
|
# another key was modified:
|
||||||
|
# cache the new dictionary version
|
||||||
|
self.dict_version = dict_version
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
This main drawback of this option is the impact on the memory footprint.
|
||||||
|
It increases the size of each dictionary entry, so the overhead depends
|
||||||
|
on the number of buckets (dictionary entries, used or unused yet). For
|
||||||
|
example, it increases the size of each dictionary entry by 8 bytes on
|
||||||
|
64-bit system if we use ``size_t``.
|
||||||
|
|
||||||
|
In Python, the memory footprint matters and the trend is more to reduce
|
||||||
|
it. Examples:
|
||||||
|
|
||||||
|
* `PEP 393 -- Flexible String Representation
|
||||||
|
<https://www.python.org/dev/peps/pep-0393/>`_
|
||||||
|
* `PEP 412 -- Key-Sharing Dictionary
|
||||||
|
<https://www.python.org/dev/peps/pep-0412/>`_
|
||||||
|
|
||||||
|
|
||||||
|
Add a new dict subtype
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Add a new ``verdict`` type, subtype of ``dict``. When guards are needed,
|
||||||
|
use the ``verdict`` for namespaces (module namespace, type namespace,
|
||||||
|
instance namespace, etc.) instead of ``dict``.
|
||||||
|
|
||||||
|
Leave the ``dict`` type unchanged to not add any overhead (memory
|
||||||
|
footprint) when guards are not needed.
|
||||||
|
|
||||||
|
Technical issue: a lot of C code in the wild, including CPython core,
|
||||||
|
expect the exact ``dict`` type. Issues:
|
||||||
|
|
||||||
|
* ``exec()`` requires a ``dict`` for globals and locals. A lot of code
|
||||||
|
use ``globals={}``. It is not possible to cast the ``dict`` to a
|
||||||
|
``dict`` subtype because the caller expects the ``globals`` parameter
|
||||||
|
to be modified (``dict`` is mutable).
|
||||||
|
* Functions call directly ``PyDict_xxx()`` functions, instead of calling
|
||||||
|
``PyObject_xxx()`` if the object is a ``dict`` subtype
|
||||||
|
* ``PyDict_CheckExact()`` check fails on ``dict`` subtype, whereas some
|
||||||
|
functions require the exact ``dict`` type.
|
||||||
|
* ``Python/ceval.c`` does not completly supports dict subtypes for
|
||||||
|
namespaces
|
||||||
|
|
||||||
|
|
||||||
|
The ``exec()`` issue is a blocker issue.
|
||||||
|
|
||||||
|
Other issues:
|
||||||
|
|
||||||
|
* The garbage collector has a special code to "untrack" ``dict``
|
||||||
|
instances. If a ``dict`` subtype is used for namespaces, the garbage
|
||||||
|
collector may be unable to break some reference cycles.
|
||||||
|
* Some functions have a fast-path for ``dict`` which would not be taken
|
||||||
|
for ``dict`` subtypes, and so it would make Python a little bit
|
||||||
|
slower.
|
||||||
|
|
||||||
|
|
||||||
|
Prior Art
|
||||||
|
=========
|
||||||
|
|
||||||
|
Guard against changing dict during iteration
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
In 2013, Serhiy Storchaka proposed a patch for the `issue #19332: Guard
|
||||||
|
against changing dict during iteration
|
||||||
|
<https://bugs.python.org/issue19332>`_ (mentioned above) which adds a
|
||||||
|
``size_t ma_count`` field to the ``PyDictObject`` structure. This field
|
||||||
|
is incremented when the dictionary is modified, and so is very similar
|
||||||
|
to the proposed ``dict.__version__``.
|
||||||
|
|
||||||
|
|
||||||
|
Cached globals+builtins lookup
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
In 2006, Andrea Griffini proposes a patch implementing a `Cached
|
||||||
|
globals+builtins lookup optimization <https://bugs.python.org/issue1616125>`_.
|
||||||
|
The patch adds a private ``timestamp`` field to dict.
|
||||||
|
|
||||||
|
See the thread on python-dev: `About dictionary lookup caching
|
||||||
|
<https://mail.python.org/pipermail/python-dev/2006-December/070348.html>`_.
|
||||||
|
|
||||||
|
|
||||||
|
Globals / builtins cache
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
In 2010, Antoine Pitrou proposed a `Globals / builtins cache
|
||||||
|
<http://bugs.python.org/issue10401>`_ which adds a private
|
||||||
|
``ma_version`` field to the ``dict`` type. The patch adds a "global and
|
||||||
|
builtin cache" to functions and frames, and changes ``LOAD_GLOBAL`` and
|
||||||
|
``STORE_GLOBAL`` instructions to use the cache.
|
||||||
|
|
||||||
|
|
||||||
|
PySizer
|
||||||
|
-------
|
||||||
|
|
||||||
|
`PySizer <http://pysizer.8325.org/>`_: a memory profiler for Python,
|
||||||
|
Google Summer of Code 2005 project by Nick Smallbone.
|
||||||
|
|
||||||
|
This project has a patch for CPython 2.4 which adds ``key_time`` and
|
||||||
|
``value_time`` fields to dictionary entries. It uses a global
|
||||||
|
process-wide counter for dictionaries, incremented each time that a
|
||||||
|
dictionary is modified. The times are used to decide when child objects
|
||||||
|
first appeared in their parent objects.
|
||||||
|
|
||||||
|
|
||||||
|
Discussion
|
||||||
|
==========
|
||||||
|
|
||||||
|
Thread on the python-ideas mailing list: `RFC: PEP: Add dict.__version__
|
||||||
|
<https://mail.python.org/pipermail/python-ideas/2016-January/037702.html>`_.
|
||||||
|
|
||||||
|
|
||||||
|
Copyright
|
||||||
|
=========
|
||||||
|
|
||||||
|
This document has been placed in the public domain.
|
|
@ -0,0 +1,213 @@
|
||||||
|
PEP: 510
|
||||||
|
Title: Specialized functions with guards
|
||||||
|
Version: $Revision$
|
||||||
|
Last-Modified: $Date$
|
||||||
|
Author: Victor Stinner <victor.stinner@gmail.com>
|
||||||
|
Status: Draft
|
||||||
|
Type: Standards Track
|
||||||
|
Content-Type: text/x-rst
|
||||||
|
Created: 4-January-2016
|
||||||
|
Python-Version: 3.6
|
||||||
|
|
||||||
|
|
||||||
|
Abstract
|
||||||
|
========
|
||||||
|
|
||||||
|
Add an API to add specialized functions with guards to functions, to
|
||||||
|
support static optimizers respecting the Python semantic.
|
||||||
|
|
||||||
|
|
||||||
|
Rationale
|
||||||
|
=========
|
||||||
|
|
||||||
|
Python is hard to optimize because almost everything is mutable: builtin
|
||||||
|
functions, function code, global variables, local variables, ... can be
|
||||||
|
modified at runtime. Implement optimizations respecting the Python
|
||||||
|
semantic requires to detect when "something changes", we will call these
|
||||||
|
checks "guards".
|
||||||
|
|
||||||
|
This PEP proposes to add a ``specialize()`` method to functions to add a
|
||||||
|
specialized functions with guards. When the function is called, the
|
||||||
|
specialized function is used if nothing changed, otherwise use the
|
||||||
|
original bytecode.
|
||||||
|
|
||||||
|
Writing an optimizer is out of the scope of this PEP.
|
||||||
|
|
||||||
|
|
||||||
|
Example
|
||||||
|
=======
|
||||||
|
|
||||||
|
Using bytecode
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Replace ``chr(65)`` with ``"A"``::
|
||||||
|
|
||||||
|
import myoptimizer
|
||||||
|
|
||||||
|
def func():
|
||||||
|
return chr(65)
|
||||||
|
|
||||||
|
def fast_func():
|
||||||
|
return "A"
|
||||||
|
|
||||||
|
func.specialize(fast_func.__code__, [myoptimizer.GuardBuiltins("chr")])
|
||||||
|
del fast_func
|
||||||
|
|
||||||
|
print("func(): %s" % func())
|
||||||
|
print("#specialized: %s" % len(func.get_specialized()))
|
||||||
|
print()
|
||||||
|
|
||||||
|
import builtins
|
||||||
|
builtins.chr = lambda obj: "mock"
|
||||||
|
|
||||||
|
print("func(): %s" % func())
|
||||||
|
print("#specialized: %s" % len(func.get_specialized()))
|
||||||
|
|
||||||
|
Output::
|
||||||
|
|
||||||
|
func(): A
|
||||||
|
#specialized: 1
|
||||||
|
|
||||||
|
func(): mock
|
||||||
|
#specialized: 0
|
||||||
|
|
||||||
|
The hypothetical ``myoptimizer.GuardBuiltins("len")`` is a guard on the
|
||||||
|
builtin ``len()`` function and the ``len`` name in the global namespace.
|
||||||
|
The guard fails if the builtin function is replaced or if a ``len`` name
|
||||||
|
is defined in the global namespace.
|
||||||
|
|
||||||
|
The first call returns directly the string ``"A"``. The second call
|
||||||
|
removes the specialized function because the builtin ``chr()`` function
|
||||||
|
was replaced, and executes the original bytecode
|
||||||
|
|
||||||
|
On a microbenchmark, calling the specialized function takes 88 ns,
|
||||||
|
whereas the original bytecode takes 145 ns (+57 ns): 1.6 times as fast.
|
||||||
|
|
||||||
|
|
||||||
|
Using builtin function
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Replace a slow Python function calling ``chr(obj)`` with a direct call
|
||||||
|
to the builtin ``chr()`` function::
|
||||||
|
|
||||||
|
import myoptimizer
|
||||||
|
|
||||||
|
def func(arg):
|
||||||
|
return chr(arg)
|
||||||
|
|
||||||
|
func.specialize(chr, [myoptimizer.GuardBuiltins("chr")])
|
||||||
|
|
||||||
|
print("func(65): %s" % func(65))
|
||||||
|
print("#specialized: %s" % len(func.get_specialized()))
|
||||||
|
print()
|
||||||
|
|
||||||
|
import builtins
|
||||||
|
builtins.chr = lambda obj: "mock"
|
||||||
|
|
||||||
|
print("func(65): %s" % func(65))
|
||||||
|
print("#specialized: %s" % len(func.get_specialized()))
|
||||||
|
|
||||||
|
Output::
|
||||||
|
|
||||||
|
func(): A
|
||||||
|
#specialized: 1
|
||||||
|
|
||||||
|
func(): mock
|
||||||
|
#specialized: 0
|
||||||
|
|
||||||
|
The first call returns directly the builtin ``chr()`` function (without
|
||||||
|
creating a Python frame). The second call removes the specialized
|
||||||
|
function because the builtin ``chr()`` function was replaced, and
|
||||||
|
executes the original bytecode.
|
||||||
|
|
||||||
|
On a microbenchmark, calling the specialized function takes 95 ns,
|
||||||
|
whereas the original bytecode takes 155 ns (+60 ns): 1.6 times as fast.
|
||||||
|
Calling directly ``chr(65)`` takes 76 ns.
|
||||||
|
|
||||||
|
|
||||||
|
Python Function Call
|
||||||
|
====================
|
||||||
|
|
||||||
|
Pseudo-code to call a Python function having specialized functions with
|
||||||
|
guards::
|
||||||
|
|
||||||
|
def call_func(func, *args, **kwargs):
|
||||||
|
# by default, call the regular bytecode
|
||||||
|
code = func.__code__.co_code
|
||||||
|
specialized = func.get_specialized()
|
||||||
|
nspecialized = len(specialized)
|
||||||
|
|
||||||
|
index = 0
|
||||||
|
while index < nspecialized:
|
||||||
|
guard = specialized[index].guard
|
||||||
|
# pass arguments, some guards need them
|
||||||
|
check = guard(args, kwargs)
|
||||||
|
if check == 1:
|
||||||
|
# guard succeeded: we can use the specialized function
|
||||||
|
code = specialized[index].code
|
||||||
|
break
|
||||||
|
elif check == -1:
|
||||||
|
# guard will always fail: remove the specialized function
|
||||||
|
del specialized[index]
|
||||||
|
elif check == 0:
|
||||||
|
# guard failed temporarely
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
# code can be a code object or any callable object
|
||||||
|
execute_code(code, args, kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
Changes
|
||||||
|
=======
|
||||||
|
|
||||||
|
* Add two new methods to functions:
|
||||||
|
|
||||||
|
- ``specialize(code, guards: list)``: add specialized
|
||||||
|
function with guard. `code` is a code object (ex:
|
||||||
|
``func2.__code__``) or any callable object (ex: ``len``).
|
||||||
|
The specialization can be ignored if a guard already fails.
|
||||||
|
- ``get_specialized()``: get the list of specialized functions with
|
||||||
|
guards
|
||||||
|
|
||||||
|
* Base ``Guard`` type which can be used as parent type to implement
|
||||||
|
guards. It requires to implement a ``check()`` function, with an
|
||||||
|
optional ``first_check()`` function. API:
|
||||||
|
|
||||||
|
* ``int check(PyObject *guard, PyObject **stack)``: return 1 on
|
||||||
|
success, 0 if the guard failed temporarely, -1 if the guard will
|
||||||
|
always fail
|
||||||
|
* ``int first_check(PyObject *guard, PyObject *func)``: return 0 on
|
||||||
|
success, -1 if the guard will always fail
|
||||||
|
|
||||||
|
Microbenchmark on ``python3.6 -m timeit -s 'def f(): pass' 'f()'`` (best
|
||||||
|
of 3 runs):
|
||||||
|
|
||||||
|
* Original Python: 79 ns
|
||||||
|
* Patched Python: 79 ns
|
||||||
|
|
||||||
|
According to this microbenchmark, the changes has no overhead on calling
|
||||||
|
a Python function without specialization.
|
||||||
|
|
||||||
|
|
||||||
|
Behaviour
|
||||||
|
=========
|
||||||
|
|
||||||
|
When a function code is replaced (``func.__code__ = new_code``), all
|
||||||
|
specialized functions are removed.
|
||||||
|
|
||||||
|
When a function is serialized (by ``marshal`` or ``pickle`` for
|
||||||
|
example), specialized functions and guards are ignored (not serialized).
|
||||||
|
|
||||||
|
|
||||||
|
Discussion
|
||||||
|
==========
|
||||||
|
|
||||||
|
Thread on the python-ideas mailing list: `RFC: PEP: Specialized
|
||||||
|
functions with guards
|
||||||
|
<https://mail.python.org/pipermail/python-ideas/2016-January/037703.html>`_.
|
||||||
|
|
||||||
|
|
||||||
|
Copyright
|
||||||
|
=========
|
||||||
|
|
||||||
|
This document has been placed in the public domain.
|
Loading…
Reference in New Issue