Python | Why is accessing an instance attribute slower than local?

import timeit class Hello(): def __init__(self): self.x = 5 def get_local_attr(self): x = self.x # 10x10 x;x;x;x;x;x;x;x;x;x; x;x;x;x;x;x;x;x;x;x; x;x;x;x;x;x;x;x;x;x; x;x;x;x;x;x;x;x;x;x; x;x;x;x;x;x;x;x;x;x; x;x;x;x;x;x;x;x;x;x; x;x;x;x;x;x;x;x;x;x; x;x;x;x;x;x;x;x;x;x; x;x;x;x;x;x;x;x;x;x; x;x;x;x;x;x;x;x;x;x; def get_inst_attr(self): # 10x10 self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x; self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x; self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x; self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x; self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x; self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x; self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x; self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x; self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x; self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x;self.x; if __name__ == '__main__': obj = Hello() print('Accessing Local Attribute:', min(timeit.Timer(obj.get_local_attr) .repeat(repeat=5))) print('Accessing Instance Attribute:', min(timeit.Timer(obj.get_inst_attr) .repeat(repeat=5))) 

Results from my computer:

Access to local attribute: 0.686281020000024

Instance attribute access: 3.7962001440000677

Why is this happening? Also, is it good to localize an instance variable before using it?

+6
source share
3 answers

Every time python searches for a variable, you pay a little ( LOAD_FAST op code). Each time you look at an attribute of an existing object, you pay a little more ( LOAD_ATTR op code). eg.

 >>> def f1(self): ... x = self.x ... x ... >>> def f2(self): ... self.x ... self.x ... >>> dis.dis(f1) 2 0 LOAD_FAST 0 (self) 3 LOAD_ATTR 0 (x) 6 STORE_FAST 1 (x) 3 9 LOAD_FAST 1 (x) 12 POP_TOP 13 LOAD_CONST 0 (None) 16 RETURN_VALUE >>> dis.dis(f2) 2 0 LOAD_FAST 0 (self) 3 LOAD_ATTR 0 (x) 6 POP_TOP 3 7 LOAD_FAST 0 (self) 10 LOAD_ATTR 0 (x) 13 POP_TOP 14 LOAD_CONST 0 (None) 17 RETURN_VALUE >>> 

Even if you donโ€™t know how to read python disassembled bytecode, you can see what more has been done for f2 than for f1 .

Also note that not all transaction codes are the same. LOAD_FAST is basically looking for an array in a local area (so it's FAST, as the name suggests). LOAD_ATTR (on the other hand) is a bit slower as it translates a function call ( __getattribute__ ), which (usually) does a dictionary lookup.


As for the โ€œbest practice,โ€ do what you read the easiest. I think it's pretty common to use self unless you demonstrate that there is a noticeable increase in performance avoiding this, but I don't think this is a tough rule.

+7
source

Since local variables are simply accessed using the LOAD_FAST step from one bytecode, on the other hand, self.x will require you to first find self using LOAD_FAST and then access x on it, which is also difficult, since Python must first check whether it is a data descriptor or just a simple attribute of an instance, and based on this, its value is selected.

Typically, when working with methods in CPython, it is recommended to cache such highly repeated calls, because otherwise a new related object is created each time. I barely saw a case where a regular attribute was cached in order to get some performance benefits. Other implementations, such as PyPy and Pyston, have their own way of speeding up attribute searches. From the data model page:

Note that the conversion from a function object to an (unrelated or related) method of the object occurs every time an attribute is retrieved from a class or instance. In some cases, a fruitful optimization is to assign an attribute to a local variable and call this local variable.

One example of this might be list.append (see also: https://hg.python.org/cpython/file/f7fd2776e80d/Lib/heapq.py#l372_ ), for example, if you list.append list with a large number elements and for some reason cannot use list comprehension, then caching list.append provides slight acceleration:

 >>> %%timeit lst = [] for _ in xrange(10**6): lst.append(_) ... 10 loops, best of 3: 47 ms per loop >>> %%timeit lst = [];append=lst.append for _ in xrange(10**6): append(_) ... 10 loops, best of 3: 31.3 ms per loop 

Python 3.7

In Python 3.7, two new byte codes will appear to speed up loading and calling the method.

Two new LOAD_METHOD CALL_METHOD : LOAD_METHOD and CALL_METHOD to avoid creating instances of related method objects for method calls, which leads to an acceleration of method calls up to 20%. (Courtesy of Yuri Selivanov and INADA Naoki at bpo-26110 .)

+6
source

You are faced with a review problem, which is described in some detail here

Although areas are defined statically, they are used dynamically. At any time during execution, there are at least three nested areas whose namespaces are directly accessible:

  • the innermost search area in which the search is performed contains local names
  • areas of any closing functions that start with the closest surrounding area contain non-local, but also non-global names
  • the next last area contains the current global module names
  • the outermost area (search for the latter) is a namespace containing embedded names

Thus, access to local variables is 1 less than the instance variable and fits into so many repetitions that it works more slowly.

This question is also a possible duplicate of this.

+1
source

Source: https://habr.com/ru/post/982719/


All Articles