GeezerVisonThe World Wide Web, restored to 800 pixels of glory. |
| Home|About|Gaming cabinet|Archives|How it works|Starter sites |
GeezerVison is showing peps.python.org — PEP 509 – Add a private version to dict | peps.python.org
PEP 509 – Add a private version to dict
PEP 509 – Add a private version to dict
AbstractAdd a new private version to the builtin RationaleIn Python, the builtin 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 semantics 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 private version to dictionaries to implement fast guards on namespaces. Dictionary lookups can be skipped if the version does not change, which is the common case for most namespaces. The version is globally unique, so checking the version is also enough to verify that the namespace dictionary was not replaced with a new dictionary. When the dictionary version does not change, the performance of a guard does not depend on the number of watched dictionary entries: the complexity is O(1). Example of optimization: copy the value of a global variable to function constants. This optimization requires a guard on the global variable to check if it was modified after it was copied. If the global variable is not modified, the function uses the cached copy. If the global variable is modified, the function uses a regular lookup, and maybe also deoptimizes the function (to remove the overhead of the guard check for next function calls). See the 510 – Specialized functions with guards for concrete usage of guards to specialize functions and for a more general rationale on Python static optimizers. Guard examplePseudo-code of a fast guard to check if a dictionary entry was modified
(created, updated or deleted) using a hypothetical
UNSET = object() class GuardDictKey: def __init__(self, dict, key): self.dict = dict self.key = key self.value = dict.get(key, UNSET) self.version = dict_get_version(dict) def check(self): """Return True if the dictionary entry did not change and the dictionary was not replaced.""" # read the version of the dictionary version = dict_get_version(self.dict) if version == self.version: # Fast-path: dictionary lookup avoided return True # lookup in the dictionary value = self.dict.get(self.key, UNSET) if value is self.value: # another key was modified: # cache the new dictionary version self.version = version return True # the key was modified return False Usage of the dict versionSpeedup method callsYury Selivanov wrote a patch to optimize method calls. The patch depends on the “implement per-opcode cache in ceval” patch which requires dictionary versions to invalidate the cache if the globals dictionary or the builtins dictionary has been modified. The cache also requires that the dictionary version is globally unique.
It is possible to define a function in a namespace and call it in a
different namespace, using Specialized functions using guardsPEP 510 proposes an API to support specialized functions with guards. It allows to implement static optimizers for Python without breaking the Python semantics. The fatoptimizer of the FAT Python project is an example of a static Python optimizer. It implements many optimizations which require guards on namespaces:
PyjionAccording of Brett Cannon, one of the two main developers of Pyjion, Pyjion can benefit from dictionary version to implement optimizations. Pyjion is a JIT compiler for Python based upon CoreCLR (Microsoft .NET Core runtime). CythonCython can benefit from dictionary version to implement optimizations. Cython is an optimising static compiler for both the Python programming language and the extended Cython programming language. Unladen SwallowEven if dictionary version was not explicitly mentioned, optimizing 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). 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. ChangesAdd a Each time a dictionary is created, the global version is incremented and the dictionary version is initialized to the global version. Each time the dictionary content is modified, the global version must be incremented and copied to the dictionary version. Dictionary methods which can modify its content:
The choice of increasing or not the version when a dictionary method does not change its content is left to the Python implementation. A Python implementation can decide to not increase the version to avoid dictionary lookups in guards. Examples of cases when dictionary methods don’t modify its content:
Setting a key to a new value equals to the old value is also considered as an operation modifying the dictionary content. Two different empty dictionaries must have a different version to be able to identify a dictionary just by its version. It allows to verify in a guard that a namespace was not replaced without storing a strong reference to the dictionary. Using a borrowed reference does not work: if the old dictionary is destroyed, it is possible that a new dictionary is allocated at the same memory address. By the way, dictionaries don’t support weak references. The version increase must be atomic. In CPython, the Global Interpreter
Lock (GIL) already protects Example using a hypothetical >>> d = {} >>> dict_get_version(d) 100 >>> d['key'] = 'value' >>> dict_get_version(d) 101 >>> d['key'] = 'new value' >>> dict_get_version(d) 102 >>> del d['key'] >>> dict_get_version(d) 103 The field is called Backwards CompatibilitySince the Implementation and PerformanceThe issue #26058: PEP 509: Add ma_version_tag to PyDictObject contains a patch implementing this PEP. On pybench and timeit microbenchmarks, the patch does not seem to add any overhead on dictionary operations. For example, the following timeit micro-benchmarks takes 318 nanoseconds before and after the change: python3.6 -m timeit 'd={1: 0}; d[2]=0; d[3]=0; d[4]=0; del d[1]; del d[2]; d.clear()' When the version does not change, The fat module implements
such guards: Integer overflowThe implementation uses the C type After an integer overflow, a guard can succeed whereas the watched
dictionary key was modified. The bug only occurs at a guard check if
there are exactly If a dictionary is modified every nanosecond, A risk of a bug every 584 years is acceptable. AlternativesExpose the version at Python level as a read-only __version__ propertyThe first version of the PEP proposed to expose the dictionary version
as a read-only There are multiple issues:
Mandatory bikeshedding on the property name:
Add a version to each dict entryA 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 only store the entry version (a simple integer) to avoid the strong reference to the value: only strong references to the dictionary and to the key are needed. Changes: add a Pseudo-code of a fast guard to check if a dictionary key was modified
using hypothetical UNSET = object() class GuardDictKey: def __init__(self, dict, key): self.dict = dict self.key = key self.dict_version = dict_get_version(dict) self.entry_version = dict_get_entry_version(dict, key) def check(self): """Return True if the dictionary entry did not change and the dictionary was not replaced.""" # read the version of the dictionary dict_version = dict_get_version(self.dict) if dict_version == self.version: # Fast-path: dictionary lookup avoided return True # lookup in the dictionary to read the entry version entry_version = get_dict_key_version(dict, key) if entry_version == self.entry_version: # another key was modified: # cache the new dictionary version self.dict_version = dict_version self.entry_version = entry_version return True # the key was modified return False The 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 not used). For example, it increases the size of each dictionary entry by 8 bytes on 64-bit system. In Python, the memory footprint matters and the trend is to reduce it. Examples: Add a new dict subtypeAdd a new Leave the Technical issue: a lot of C code in the wild, including CPython core,
expecting the exact
The Other issues:
Prior ArtMethod cache and type version tagIn 2007, Armin Rigo wrote a patch to implement a cache of methods. It
was merged into Python 2.6. The patch adds a “type attribute cache
version tag” ( The type version tag is not exposed at the Python level. The version tag has the C type By default, a type has its “valid version tag” flag cleared to indicate that the version tag is invalid. When the first method of the type is cached, the version tag and the “valid version tag” flag are set. When a type is modified, the “valid version tag” flag of the type and its subclasses is cleared. Later, when a cache entry of these types is used, the entry is removed because its version tag is outdated. On integer overflow, the whole cache is cleared and the global version
tag is reset to See Method cache (issue #1685986) and Armin’s method cache optimization updated for Python 2.6 (issue #1700288). Globals / builtins cacheIn 2010, Antoine Pitrou proposed a Globals / builtins cache (issue
#10401) which adds a private
The patch adds a “global and builtin cache” to functions and frames, and
changes The change on the Cached globals+builtins lookupIn 2006, Andrea Griffini proposed a patch implementing a Cached
globals+builtins lookup optimization. The patch adds a private
Thread on python-dev: About dictionary lookup caching (December 2006). Guard against changing dict during iterationIn 2013, Serhiy Storchaka proposed Guard against changing dict during
iteration (issue #19332) which
adds a PySizerPySizer: 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 DiscussionThread on the mailing lists:
AcceptanceThe PEP was accepted on 2016-09-07 by Guido van Rossum. The PEP implementation has since been committed to the repository. CopyrightThis document has been placed in the public domain. |
| You are visitor 01402503 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required. |