a possible leak in the object namespace...

Hi All, Decided to test the 1.4 with a couple of my usecases, got quite surprising results... To keep it short, I have several usecases that involve replacing the object dictionary. Here's a simple example: ---cut--- class D(dict): def __setitem__(self, key, value): print 'updating attr "%s" to "%s"...' % (key, value) super(D, self).__setitem__(key, value) class X(object): pass x = X() x.__dict__ = D() x.a = 1 # will print a nice log string... # NOTE: this will not print anything in CPython (see P.S.) --uncut-- With the release of version 1.4, I decided to test these usecases out and benchmark them on PyPy and 15 minutes later I got results that were surprising to say the least... Expectations: 1) the normal/native namespace should have been a bit faster than the hooked object on the first run. Both cases should have leveled to about the same performance after the JIT finished it's job +/- a constant. 2) all times should have been near constant. What I got per point: 1) the object with native dict was slower by about three orders of magnitude than the object with a hooked namespace. 2) sequential write benchmark runs on the normal object did not level out, as they did with the hook, rather, they exhibited exponential times (!!) For details and actual test code see the attached file. P.S. This specific functionality is a weak point in CPython for two reasons: 1) writing to .__dict__ demands a subclass of dict (not a real problem, though over-restrictive in my view) 2) all interactions with the namespace are done directly via the Python C-API, completely neglecting the high-level dict interface. Thanks! -- Alex.

Hi Alex, On 11/29/2010 03:04 PM, Alex A. Naanou wrote:
Don't do that then :-).
For details and actual test code see the attached file.
The code you are trying is essentially this: def test(obj, count=10000): t0 = time.time() for i in xrange(count): setattr(obj, 'a' + str(i), i) t1 = time.time() # return: total, time per write return t1 - t0, (t1 - t0)/count This is not working very well with the non-overridden dict, because we don't optimize for this case at all. You are using a) lots of attributes, which we expect to be rare b) access them with setattr, which is a lot slower than a fixed attribute c) access a different attribute every loop iteration, which means the compiler has to produce one bit of code for every attribute Read this, for some hints why this is the case: http://morepypy.blogspot.com/2010/11/efficiently-implementing-python-objects... This is in theory fixable with enough work, but I am not sure that this is a common or useful use case. If you really need to do this, just use a normal dictionary. Or show me some real-world code that does this, and might think about the case some more. Anyway, the timing behavior of the above loop is merely quadratic in the number of attributes, not exponential :-). Cheers, Carl Friedrich

On Mon, Nov 29, 2010 at 21:46, Carl Friedrich Bolz <cfbolz@gmx.de> wrote:
:)
This is intentional (all three points), I did not want the jit to factor out the loop -- I wanted to time the initial attribute creation...
Accepted, my mistake :) But quadratic behaviour + a three orders of magnitude increase in time it takes to create an attr is scarry... but you are right, how often does that usecase happen? :) Retested with: setattr(obj, 'a', i) The results are *allot* better and it is indeed the common case :) Thanks!
-- Alex.

Hi Alex, On 11/29/2010 09:02 PM, Alex A. Naanou wrote:
Yes, I fear initial attribute creation is never going to be very efficient. [snip]
I improved some things so that setattr/getattr is a bit faster. Should now only be one or two orders of magnitude :-). If you run it tomorrow with the nightly build from tonight, you should see an improvement. The quadraticness is not easily fixable without giving up all optimizations with instances of more than X (maybe 1000?) attributes. Again, I don't think this is common. And I don't want to chose an X. Cheers, Carl Friedrich

On Tue, Nov 30, 2010 at 20:33, Carl Friedrich Bolz <cfbolz@gmx.de> wrote:
That's good news! It is not uncommon, at least in my work, to write generic code that does not know of attribute names it uses, thus getattr/setattr functions are unavoidable.
I'd even argue that most of the cases were the number of attributes exceeds a certain "sane" N would and should be better implemented via a container... (in spite of the how I wrote the code in my first mail ;) ) I've been planning of moving most of my commercial projects to PyPy as soon as it is stable enough -- I'm tired of fighting the CPython implementation for most of the time -- we will see how it goes... too bad some of the code will be PyPy specific due to the way CPython works. Thanks!
Cheers,
Carl Friedrich
-- Alex.

Hi Alex, On 11/29/2010 03:04 PM, Alex A. Naanou wrote:
Don't do that then :-).
For details and actual test code see the attached file.
The code you are trying is essentially this: def test(obj, count=10000): t0 = time.time() for i in xrange(count): setattr(obj, 'a' + str(i), i) t1 = time.time() # return: total, time per write return t1 - t0, (t1 - t0)/count This is not working very well with the non-overridden dict, because we don't optimize for this case at all. You are using a) lots of attributes, which we expect to be rare b) access them with setattr, which is a lot slower than a fixed attribute c) access a different attribute every loop iteration, which means the compiler has to produce one bit of code for every attribute Read this, for some hints why this is the case: http://morepypy.blogspot.com/2010/11/efficiently-implementing-python-objects... This is in theory fixable with enough work, but I am not sure that this is a common or useful use case. If you really need to do this, just use a normal dictionary. Or show me some real-world code that does this, and might think about the case some more. Anyway, the timing behavior of the above loop is merely quadratic in the number of attributes, not exponential :-). Cheers, Carl Friedrich

On Mon, Nov 29, 2010 at 21:46, Carl Friedrich Bolz <cfbolz@gmx.de> wrote:
:)
This is intentional (all three points), I did not want the jit to factor out the loop -- I wanted to time the initial attribute creation...
Accepted, my mistake :) But quadratic behaviour + a three orders of magnitude increase in time it takes to create an attr is scarry... but you are right, how often does that usecase happen? :) Retested with: setattr(obj, 'a', i) The results are *allot* better and it is indeed the common case :) Thanks!
-- Alex.

Hi Alex, On 11/29/2010 09:02 PM, Alex A. Naanou wrote:
Yes, I fear initial attribute creation is never going to be very efficient. [snip]
I improved some things so that setattr/getattr is a bit faster. Should now only be one or two orders of magnitude :-). If you run it tomorrow with the nightly build from tonight, you should see an improvement. The quadraticness is not easily fixable without giving up all optimizations with instances of more than X (maybe 1000?) attributes. Again, I don't think this is common. And I don't want to chose an X. Cheers, Carl Friedrich

On Tue, Nov 30, 2010 at 20:33, Carl Friedrich Bolz <cfbolz@gmx.de> wrote:
That's good news! It is not uncommon, at least in my work, to write generic code that does not know of attribute names it uses, thus getattr/setattr functions are unavoidable.
I'd even argue that most of the cases were the number of attributes exceeds a certain "sane" N would and should be better implemented via a container... (in spite of the how I wrote the code in my first mail ;) ) I've been planning of moving most of my commercial projects to PyPy as soon as it is stable enough -- I'm tired of fighting the CPython implementation for most of the time -- we will see how it goes... too bad some of the code will be PyPy specific due to the way CPython works. Thanks!
Cheers,
Carl Friedrich
-- Alex.
participants (2)
-
Alex A. Naanou
-
Carl Friedrich Bolz