[Python-Dev] Speeding up instance attribute access

Guido van Rossum guido@python.org
Fri, 08 Feb 2002 14:18:20 -0500


(By mistake I sent an incomplete version of this earlier.  Please
ignore that and read this instead.)

Inspired by the second half of Jeremy's talk on DevDay, here's my
alternative approach for speeding up instance attribute access.  Like
my idea for globals, it uses double indirection rather than
recompilation.


- We only care about attributes of 'self' (which is identified as the
  first argument of a method, not by name).  We can exclude functions
  from our analysis that make any assignment to self -- this is
  extremely rare and would throw off our analysis.  We should also
  exclude static methods and class methods, since their first argument
  doesn't have the same role.

- Static analysis of the source code of a class (without access to the
  base class) can determine attributes of the class, and to some extent
  instance variables.  Without also analyzing the base classes, this
  analysis cannot reliably distinguish between instance variables and
  methods inherited from a base class; it can distinguish between
  instance variables and methods defined in the current class.

- We can guess the status of un-assigned-to inherited attributes by
  seeing whether they are called or not.  This is not 100% accurate,
  so we need things to work (if slower) even when we guess wrong.

- For instance variable references and stores of the form self.<name>,
  the bytecode compiler emits opcodes LOAD_SELF_IVAR <i> and
  STORE_SELF_IVAR <i>, where <i> is a small int identifying the
  instance variable (ivar).  A particular ivar is identified by the
  same <i> throughout all methods defined in the same class statement,
  but there is no attempt to coordinate this across different classes
  related by inheritance.

- Some data structure describing the mapping from <i> to attribute
  name, and whether it's an ivar or a method, is produced by the
  compiler and stored in the class, in a way that a user can't change
  it.  The function objects representing methods also contain a
  pointer to this data structure.  (Or the code objects?  But it needs
  to be shared.  Details, details.)

- At *run time*, when a class object is created, another data
  structure is created that accumulates the <i>-to-name mappings from
  that class and all its base classes.  This has more accurate
  information because it can collect information from the bases
  (though it can still be fooled by dynamic manipulations of classes).
  In particular, it has the canonical set of known instance variables
  for instances of this class.  This is stored in the class object, in
  a way that a user can't change it.

- When an instance is created, the run-time data structure stored in
  the class is consulted to know how many instance variables to
  allocate.  An array is allocated at the end of the instance (or in a
  separately allocated block?) with one PyObject pointer for each
  known instance variable.  There's also a pointer to the instance
  __dict__, to hold instance variables that the parser didn't spot,
  but this pointer starts off as NULL -- a dictionary is created for
  it only when needed.

- There is no requirement that the layout of the ivar array for
  instances of a subclass is an extension of the layout of the ivar
  array for its base classes.  But there *is* a requirement that all
  instances of the same class that are not instances of a subclass
  (i.e., all x and y where x.__class__ is y.__class__) have the same
  layout, and this layout is determined by the run-time data structure
  stored in the class.

- Now all we need is an efficient way to map LOAD_SELF_IVAR <i> to an
  index in the array of ivars.  Two different classes play a role
  here: the class of self (the run-time class) and the class that
  defined the method containing the LOAD_SELF_IVAR <i> opcode (the
  compile-time class).  We assume the run-time class is a subclass of
  the compile-time class.  The correct mapping can easily be
  calculated from the data structures left behind by the compiler in
  the compile-time class and by class construction at run time in the
  run-time class.  Since this mapping doesn't change, it can be
  calculated once and then cached.  The cache can be held in the
  run-time class; it can be a dictionary whose keys are compile-time
  classes.  This means a single dict lookup that must be done once per
  method call (and only when LOAD_SELF_IVAR <i> or STORE_SELF_IVAR <i>
  is used).  We could save even that dict lookup in most cases by
  caching the run-time class and the outcome with the method.

- We need fallbacks for various exceptional cases:

  * If the compile-time class uses LOAD_SELF_IVAR <i> but the run-time
    class doesn't think that is an instance variable, LOAD_SELF_IVAR
    must fall back to look in the instance dict (if non-NULL) and then
    down the run-time class and its base classes.

  * If the ivar slot in the instance corresponding to <i> exists but
    is NULL, LOAD_SELF_IVAR <i> must fall back to searching the
    run-time class and its base classes.

  For both cases, the mapping from <i> to attribute names must be
  available.  The language and the current code generation guarantee
  that 'self' is the first local variable.

- The instance __dict__ must become a proxy that knows whether a given
  name is stored in the array of ivars or in the overflow dict; this
  is much like Jeremy's DLict.

- Note that there are two sources of savings: the major savings
  (probably) comes from avoiding a dict lookup for every ivar access;
  an additional minor savings comes from collapsing two opcodes:

      LOAD_FAST       0 (self)
      LOAD_ATTR       1 (foo)

  into one:

      LOAD_SELF_IVAR  0 (self.foo)

- I don't know if we should try to generate LOAD_SELF_IVAR <i> only
  for things that really are (likely) ivars, or for all attributes.
  Maybe it would be nice if we also had a single-opcode way to express
  a method call on self, e.g. CALL_SELF_METHOD <i>, <n>, <k> where <i>
  identifies the method, and <n> and <k> are the number of positional
  and keyword arguments.  Or maybe we should just have
  LOAD_SELF_METHOD <i> which looks in the instance overflow dict (but
  only if non-NULL) and in the class and bases, but can avoid looking
  in the ivars array if <i> does not describe a known ivar (again this
  information can be cached).

- The required global analysis is a bit hairy, and not something we
  already do.  I believe that Jeremy thinks PyChecker already does
  this; I'm not sure if that means we can borrow code or just ideas.

--Guido van Rossum (home page: http://www.python.org/~guido/)