New-style classes + __getattr__ sucks, here is why:
commitcef928d21ff98656d2ba5388a53794dee8763596
authorKirill Smelkov <kirr@landau.phys.spbu.ru>
Mon, 28 Jul 2008 04:36:11 +0000 (28 08:36 +0400)
committerKirill Smelkov <kirr@landau.phys.spbu.ru>
Mon, 28 Jul 2008 04:36:11 +0000 (28 08:36 +0400)
tree7edcf8d57e29bc2cd695b440551a126defa323b2
parent8fa0484bd71da210a4de6485587a80a27a73868c
New-style classes + __getattr__ sucks, here is why:

# new-style class with deep inheritance with root=object
class Base(...)

class A(Base):              class B(Base):
    ...                         ...

    # no __getattr__            def __getattr__(self, name):
                                    # not important what is here
                                    pass

    def func():                 def func():
        # some function             # some function
        pass                        pass

a = A()
b = B()

# note - no call is made, just attribute access

%timeit a.func  -->             830 ns
%timeit b.func  -->  2.19 µs = 2190 ns

That's more than 2.5x slowdown!

----
Let's have a look here:

http://landau.phys.spbu.ru/~kirr/cgi-bin/hg.cgi/py-fast-property

The following is what I get on my host (rev a063185b02f5 used):

========================================
 *** INSTANCE VAR ACCESS TIMES (ns) ***
========================================

     cls     cls_o    cls(BB)  cls(OO)  note

E:    527      607      591      589    class var (just for reference -- can *not* be used)
I:    586      485      592      481    __slots__
F:    715      478     1625      488    inst var
G:    691      477     1638      482    inst var (+ another inst vars)
J:    881      477     1825      477    __getattr__ (empty)  + __slots__
C:   2035     2155     1966     2160    @property
D:   1017      479     2885      490    __getattr__ (cached)
H:   1010      488     2903      477    __getattr__ (empty)  + inst var
d:   7190     7186     9381     8850    __getattr__

=========================================
 *** INSTANCE FUNC ACCESS TIMES (ns) ***
=========================================

     cls     cls_o    cls(BB)  cls(OO)  note

I:    719      757      704      780    __slots__
G:    797      768      771      767    inst var (+ another inst vars)
F:    781      769      775      790    inst var
C:    689      782      778      793    @property
E:    686      828      782      774    class var (just for reference -- can *not* be used)
J:   1011      761     1969      818    __getattr__ (empty)  + __slots__
d:   1010      769     2041      770    __getattr__
D:   1079      792     2048      757    __getattr__ (cached)
H:   1089      781     2111      769    __getattr__ (empty)  + inst var

----
legend:

 cls     -- new-style class without base
 cls_o   -- old-style class without base
 cls_BB  -- new-style class with long base
 cls_OO  -- old-style class with long base

For -*- new-style -*-  classes this means that:

  - table 1 -

(F) access to __dict__  vars is much slower in case of deep inheritance
(I) access to __slots__ vars is independent of inheritance

(J,H) access to both either __dict__ or __slots__ instance vars is _much_ slower
    in presence of __getattr__ (!)

  - table 2 -

(I,F) access to function attributes is independent of inheritance
(J,d,D,H) access to function attributes is _much_ slower if there is a
    __getattr__ (!)

So you see, __getattr__ + new-style classes + inheritance is slow even if
__getattr__ is not used at runtime!

But for -*- old-style -*- classes the tables say that instance var & function
attribute access times do not depend on inheritance and are immutable to
__getattr__ presence.

I just can't believe old-style classes are better!?

(An interested reader is directed to read the following parts of CPython
 runtime:

 Objects/object.c
 Objects/typeobject.c
 Objects/classobject.c

 in particular:

 (old-style classes)
 `instance_gettattr`

 (new-style classes)
 `PyObject_GenericGetAttr`
 `slot_tp_getattro`
 `slot_tp_getattr_hook

 and maybe more ...
)

----------------------------------------

So what this all means for SymPy?

Since Basic is a new-style class, and we use __getattr__

    all attribute access is *SLOW*

So, we have to choose to either

1. switch to old-style classes, or
2. remove __getattr__ from Basic

Personally, I'm afraid to go the old-style classes way, because:

- new-style classes have nice features as e.g. __slots__
- new-style classes are better maintained
- e.g. Cython classes are new-style (not sure for 100%)
- ...

So here we choose (2).

The only thing that Basic.__getattr__ was used for is to jump to assumptions
code so things like:

    expr.is_positive

work.

There is a nice idea how this could be done without __getattr__ (please read
the patch), so I see no reason not to remove it.

So let's make this story short and just do it -- SymPy is 20% faster now:

                                                             3
            %timeit   %timeit       fem_test.py   integrate(x sin(x), x)
            x.diff    x.is_integer  (cache:on)    (cache:on)

old:        2.18 µs   15   µs        3.72 s         5.18 s
new:        1.01 µs    4.2 µs        3.16 s         4.33 s

speedup:     1.16x     3.57x          18%            20%

Signed-off-by: Kirill Smelkov <kirr@landau.phys.spbu.ru>
Signed-off-by: Ondrej Certik <ondrej@certik.cz>
Signed-off-by: Mateusz Paprocki <mattpap@gmail.com>
sympy/core/assumptions.py
sympy/core/basic.py