Timeit question

I’m looking at timeit and came upon something interesting, leaves a lot of questions.

  1. Why would lambda take more time?
  2. Is it possible that the function has to be defined behind the sceen?
    I know this is just one iteration, I study that next(many iterations). As I see it, removing the print function and storing the variable instead got closer to the base time for each.
import timeit
print()
#print(timeit.timeit(lambda: 4 + 5))        ###STARTED WITH###
lambda_output = timeit.timeit(lambda: 4 + 5)
print('Lambda Output:', '{:.5f}'.format(lambda_output))

#print(timeit.timeit("output = 4 + 5"))       ###STARTED WITH###
standard_output = timeit.timeit("output = 4 + 5")
print('Standard Output:','{:.5f}'.format(standard_output))

times_faster = lambda_output / standard_output
print('Times Faster:', "{:.1f}".format(times_faster))

Output:

Lambda Output: 0.09217
Standard Output: 0.01505
Times Faster: 6.1

Process finished with exit code 0

Thanks for your thoughs and insights.

The reason is because of how timeit executes the provided code. If provided a string, it simply inserts the code into its timing loop, then execs the whole thing. But if you provide a function/callable, it doesn’t have access to the source, so it just inserts a call to the function.

The additional time you’re observing is the time taken to call the function on each iteration of the loop. Actually since the code you’re trying to test (4 + 5) only contains constant values, CPython is smart enough to precalculate this result, so no addition actually takes place. So you’re comparing a local variable assignment to a function call, which is a lot more expensive. To fix that, make one of the two parameters a variable set via the init code/global namespace.

1 Like

We can get a hint by disassembling the source code using the dis module.

>>> import dis
>>> dis.dis('output = 4 + 5')
  1           0 LOAD_CONST               0 (9)
              2 STORE_NAME               0 (output)
              4 LOAD_CONST               1 (None)
              6 RETURN_VALUE

The Python interpreter is smart enough to see that 4 and 5 are small integers that can be pre-computed, so all you are timing is binding the value 9 to the name “output”.

In your other example:

>>> dis.dis('lambda: 4 + 5')
  1           0 LOAD_CONST               0 (<code object <lambda> at 0x7f8514392150, file "<dis>", line 1>)
              2 LOAD_CONST               1 ('<lambda>')
              4 MAKE_FUNCTION            0
              6 RETURN_VALUE

Disassembly of <code object <lambda> at 0x7f8514392150, file "<dis>", line 1>:
  1           0 LOAD_CONST               1 (9)
              2 RETURN_VALUE

you are timing the creation of a function, not calling the function.

Try using this instead:

timeit.timeit('f()', setup='f = lambda: 4 + 5')

(Untested.)

The fourth column is a list of numbers (0, 1 , 1, 0,), are they the machine cycles needed to do the command in assembly?
As I see it, cpu work load is the overhead of code. If both have the same human end result, wouldn’t you lean towards using the faster code style in that case. I’m looking well beyond the examples (types of programming, oop, functional, etc), they are wonderful. I can’t get this from any book that I know of, thanks so much.

No, Python isn’t compiled to assembler. This is bytecode executed by the interpreter’s evaluation loop. In dis’s output, the columns are the line number, offset in the bytecode, the operation name, numeric opcode parameter, then a decoded version of the parameter for some opcodes. For instance LOAD_CONST indexes the func.__code__.co_const tuple, so it displays the repr of that item. For a list of bytecode instructions, see the dis docs.

I miss wrote… You are right, it is interpreted language. What I was trying to say is that the bytecode instructions look a lot like assembly language. If this is so, then it helps explain, after it is interpreted, how it is executed. If what comes out of the interpreter is executable code, then why do we have to convert .py to exe? I know the interpreter does all the converting from human to machine usable form, so nothing more would be needed, in my way of seeing it. Just save that as the exec.
I know that .py has to be converted to make sure all the parts of the program have their supporting parts.
Thanks again for helping clear things up. Concepts are so important, we must have them correct as the foundation.

It’s low-level and uses explicit jumps like assembly, but it isn’t implemented directly by any CPU. In CPython there’s a massive switch statement inside a while loop which checks the opcode and then does that operation.

No, those are parameter values. CPython is built with a stack-based bytecode interpreter, and most of the opcodes are either like “RETURN_VALUE” (just does its thing, no extra information needed) or like “LOAD_CONST 1” (take constant number 1 from the list of constants and load it onto the stack). You can see some more examples here:

>>> dis.dis(lambda s, p, a, m: s * p + a * m + s + p * a + m)
  1           0 LOAD_FAST                0 (s)
              2 LOAD_FAST                1 (p)
              4 BINARY_MULTIPLY
              6 LOAD_FAST                2 (a)
              8 LOAD_FAST                3 (m)
             10 BINARY_MULTIPLY
             12 BINARY_ADD
             14 LOAD_FAST                0 (s)
             16 BINARY_ADD
             18 LOAD_FAST                1 (p)
             20 LOAD_FAST                2 (a)
             22 BINARY_MULTIPLY
             24 BINARY_ADD
             26 LOAD_FAST                3 (m)
             28 BINARY_ADD
             30 RETURN_VALUE

The variable ‘m’ is always referred to as “fast local #3”, and the disassembler shows that name in parens afterwards. The same goes for constants.

You can see the full tables by delving into the function’s __code__ object:

>>> f = lambda s, p, a, m: s * p + a * m + s + p * a + m
>>> f.__code__.co_varnames
('s', 'p', 'a', 'm')
>>> f.__code__.co_consts
(None,)

(Fun fact: Lambda functions almost always have None as their first constant. Why? I’ll give you a hint: def functions might not have None as a constant, but only if Python isn’t running in -OO mode.)

Thank You. That makes sense now. Its not something we deal with daily but nice to understand how and why for the big picture. Its like looking under the hood of a car… you don’t need to understand the pistons in the engine, but you want to know the type of engine etc Just for knowledge.