[Python-Dev] Proposed PEP: Optimizing Global Variable and Attribute Access
skip at pobox.com
Tue Aug 14 14:37:14 CEST 2001
Roman> What about threads? What if math.sin changes while in cache?
I added the following to a new Questions section:
Q. What about threads? What if math.sin changes while in cache?
A. I believe the global interpreter lock will protect values from being
corrupted. In any case, the situation would be no worse than it is
today. If one thread modified math.sin after another thread had
already executed "LOAD_GLOBAL math", but before it executed
"LOAD_ATTR sin", the client thread would see the old value of
The idea is this. I will use a multi-attribute load as an example,
not because it would happen very often, but because by demonstrating
the recursive nature with an extra call hopefully it will become
clearer what I have in mind. Suppose a function defined in module
foo wants to access spam.eggs.ham and that spam is a module imported
at the module level in foo:
x = spam.eggs.ham
Upon entry to somefunc, a TRACK_GLOBAL instruction will be executed:
TRACK_GLOBAL spam.eggs.ham &4
"spam.eggs.ham" is a string literal stored in the function's
constants array. "&4" is a reference to a slot in the executing
frame's fast locals array. Here's what I envision happening:
1. The TRACK_GLOBAL instruction locates the object referred to by
the name "spam" and finds it in its global scope. It then
executes a C function like
_PyObject_TrackName(m, "spam.eggs.ham", &4)
where "m" is the module object
2. The module object strips the leading "spam." stores the necessary
information ("eggs.ham" and &4) in case its binding for the name
"eggs" changes. It then locates the next name in its dict, in
this case "eggs", and recursively calls
_PyObject_TrackName(eggs, "eggs.ham", &4)
3. The eggs object strips the leading "eggs.", stores the ("ham",
&4) info, locates the object in its namespace called "ham" and
calls _PyObject_TrackName once again:
_PyObject_TrackName(ham, "ham", &4)
4. The "ham" object strips the leading string (no "." this time, but
that's a minor point), sees that the result is empty, then uses
its own value (self, probably) to update the location it was
&4 = self;
Now, at this point, each object involved in resolving
"spam.eggs.ham" knows which entry in its namespace needs to be
tracked and what location to update if that name changes.
Furthermore, if the one name it is tracking in its local storage
changes, it can call _PyObject_TrackName using the new object once
the change has been made. At the bottom end of the food chain, the
last object will always strip a name, see the empty string and know
that its value should be stuffed into the location it's been passed.
When the object referred to by the dotted expression "spam.eggs.ham"
is going to go out of scope, "UNTRACK_GLOBAL spam.eggs.ham &4" is
executed. It has the effect of causing each object in the chain to
delete its tracking information and recursively call its subsidiary
object to do the same.
The tracking operation may seem expensive, but recall that the
objects being tracked are assumed to be "almost constant", so the
setup cost will be traded off against hopefully multiple local
instead of global loads. For globals with attributes the tracking
setup cost grows but is offset by avoiding the extra LOAD_ATTR cost.
The TRACK_GLOBAL instruction needs to perform a PyDict_GetItemString
for the first name in the chain to determine where the top-level
object resides. Each object in the chain has to store a string and
an address somewhere, probably in a dict that uses storage locations
as keys (e.g. the &4) and strings as values. (This dict could
possibly be a central dict of dicts whose keys are object addresses
instead of a per-object dict.) It shouldn't be the other way around
because multiple active frames may want to track "spam.eggs.ham",
but only one frame will want to associate that name with one of its
fast locals slots.
Let me know if that doesn't address your concerns.
More information about the Python-list