[Python-Dev] CALL_ATTR, A Method Proposal

Glyph Lefkowitz glyph@twistedmatrix.com
Fri, 14 Feb 2003 06:02:42 -0600 (CST)


----Security_Multipart0(Fri_Feb_14_06:02:42_2003_652)--
Content-Type: Multipart/Mixed;
 boundary="--Next_Part(Fri_Feb_14_06:02:42_2003_595)--"
Content-Transfer-Encoding: 7bit

----Next_Part(Fri_Feb_14_06:02:42_2003_595)--
Content-Type: Text/Plain; charset=us-ascii
Content-Transfer-Encoding: 7bit


I have an idea that may result in significant optimization of python.  However,
I have an incomplete understanding of what's involved, and I haven't had enough
time to puzzle out enough of Python's internals to write up a full PEP
describing this.

Method calls appear are a full 20% slower (simple benchmark included) than
function calls, and Python function calls are already pretty slow.  By my
understanding, one of the reasons for the difference is that if you have a
method call like this:

    a = A()
    a.b()

what's really happening is something along the lines of:

    temp = new.instancemethod(getattr(a.__class__, "b"), a, A)
    temp()
    free(temp)

This causes an unnecessary memory allocation: since the instancemethod object
is immediately being created, then called, then garbage collected.  Looking at
the output of dis.dis, I can see there are also 3 bytecodes being evaluated
rather than 1.

My proposal is to treat method calls as syntactically different from function
calls.  Rather than x.y() being completely synonymous with getattr(x, "y")(),
it could be analogous to 'x.y = z' or 'del x.y'.  For symmetry with these
statement types, the new bytecode could be called CALL_ATTR.

I think this is an important thing to consider as systems like Zope and Twisted
move towards using component models and Interfaces in Python.  The fact that
direct function calls are so much faster puts efficiency directly at odds with
structured flexibility.  With a method call primitive comparable to function
calls, most python code, especially in systems that make heavy use of
inter-object communication patterns, would immediately get as much as a 15%
speed boost.

CALL_ATTR should be implementable with no impact on existing python code,
except bytecode hacks.  It should be possible to retain a fully
backwards-compatible __getattr__ method, for places where method objects are
used (including the C API).  Likewise, the default __callattr__ could be set up
to first check if __getattr__ is defined, then the instance's dictionary or
__slots__.  For additional speed gains, new-object-model classes could set
'__fast_methods__ = True' and gain a semantic distinction between __getattr__
and __callattr__.

Better still, I think that Jython could use the subtle semantic change
to make Java reflection less expensive.  (Java's `new' is more expensive than
C's `malloc', after all.)

I have a sneaking suspicion that this would also be good for security purposes.
I haven't yet come up with a specific case where this is a big deal, but I
think capability-style data-hiding would be simplified if filtering
method-calls were different from filtering attributes.

I hope this idea is useful to some of you,

-- 
 |    <`'>    |  Glyph Lefkowitz: Traveling Sorcerer   |
 |   < _/ >   |  Lead Developer,  the Twisted project  |
 |  < ___/ >  |      http://www.twistedmatrix.com      |

----Next_Part(Fri_Feb_14_06:02:42_2003_595)--
Content-Type: Text/Plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline; filename="methods.py"

import time

class A:
    def b(self):
        pass

def b(self):
    pass

a = A()

def wallclock(f):
    then = time.time()
    f()
    now = time.time()
    elapsed = now - then
    return elapsed

NCALLS = 1000000

def methods():
    for x in xrange(NCALLS):
        a.b()

def functions():
    for x in xrange(NCALLS):
        b(a)

print wallclock(methods)
print wallclock(functions)

----Next_Part(Fri_Feb_14_06:02:42_2003_595)--
Content-Type: Text/Plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline; filename="results.txt"

% python2.0 methods.py 
1.85362696648
1.47611200809
% python2.1 methods.py 
1.21098303795
0.972733020782
% python2.2 methods.py 
1.15857589245
0.914402961731
% jython methods.py 
63.396000027656555
51.51300001144409

----Next_Part(Fri_Feb_14_06:02:42_2003_595)----

----Security_Multipart0(Fri_Feb_14_06:02:42_2003_652)--
Content-Type: application/pgp-signature
Content-Transfer-Encoding: 7bit

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.2.1 (GNU/Linux)

iD8DBQA+TNrkvVGR4uSOE2wRAiwFAKCdjtbXx1yz4QN0uX4JOTsQwTQf/gCeOemO
s2R3ZfWC3R4TREI8VULt/2o=
=+8zq
-----END PGP SIGNATURE-----

----Security_Multipart0(Fri_Feb_14_06:02:42_2003_652)----