[Python-ideas] Interrogate alternate namespace keyword and concept

Steven D'Aprano steve at pearwood.info
Sat Aug 15 04:02:29 CEST 2009


On Sat, 15 Aug 2009 06:29:11 am ilya wrote:
> That's a useful statistics, but the bottleneck is **only** because of
> parsing 'value +=5'.
>
> Here's how I time it:
>
> # insert my old program here...
>
>
> from timeit import Timer
> from codeop import compile_command
>
> def timing(something):
>     setup = 'from __main__ import test, interrogate, command, inc5'
>     best = sorted(Timer(something, setup).repeat(3, 1000))[0]

min(alist) is a more-direct, simpler, faster, easier to read way of 
calculating sorted(alist)[0].


>     print('{0!r} -> {1:.3} ms'.format(something, best))
>     print('# test.value =', test.value)
>
> command = '''
> for i in range(10):
>     value += 1
> '''

You're muddying the water by including a for-loop and call to range() 
inside the code snippet being tested. We're trying to compare your 
function interrogate(test, 'value += 1') with the standard call to 
test.value += 1. Why include the time required to generate a range() 
object, and iterate over it ten times, as part of the code snippet? All 
that does is mix up the time required to execute common code and the 
time required to execute the code we care about.

If it's not obvious why your approach is flawed, consider this extreme 
example:

Timer("time.sleep(1000); interrogate(test, 'value += 1')", ...)
Timer("time.sleep(1000); test.value += 1", ...)

The differences in speed between the interrogate call (using exec) and 
the direct access to test.value will be swamped by the time used by the 
common code. 

A more accurate measurement is to remove the "for i in..." part from 
command, and increase the number=1000 argument to Timer.repeat() to 
10000.


> inc5 = compile_command(command)

This is an unfair test. We're comparing directly accessing test.value 
versus indirectly accessing test.value using exec. Regardless of 
whether the caller compiles the statement manually before passing it to 
exec, or just passes it to exec to compile it automatically, the cost 
of that compilation has to be payed. Pulling that outside of the timing 
code just hides the true cost.

Pre-compiling before passing to exec is a good optimization for the 
cases where you need to exec the same code snippet over and over again. 
If you want to execute "for i in range(1000): exec(s)" then it makes 
sense to pull out the compilation of s outside of the loop. But 
generally when timing code snippets, the only purpose of the loop is to 
minimize errors and give more accurate results, so pulling out the 
compilation just hides some of the real cost.


> timing("interrogate(test, command)")
> timing(command.replace('value', 'test.value'))
> timing("interrogate(test, inc5)")
>
> Result:
>
> 15

Where does the 15 come from?

> 'interrogate(test, command)' -> 0.0908 ms
> # test.value = 30015
> '\nfor i in range(10):\n    test.value += 1\n' -> 0.00408 ms
> # test.value = 60015
> 'interrogate(test, inc5)' -> 0.00469 ms
> # test.value = 90015
>
> so interrogate() with additional precompiling introduces very little
> overhead. 

Only because you're ignoring the overhead of pre-compiling. A more 
accurate test would be:

timing("inc5 = compile_command(command); interrogate(test, inc5)")


> Though I agree it's inconvenient to write functions as 
> strings; 

If you think that's inconvenient, just try writing functions as code 
objects without calling compile :)



-- 
Steven D'Aprano



More information about the Python-ideas mailing list