Why is a=a*100 almost two times faster than a*=100?

Viewed 883

Following the question about Chaining *= += operators and the good comment of Tom Wojcik ("Why would you assume aaa *= 200 is faster than aaa = aaa * 200 ?"), I tested it in Jupyter notebook:

%%timeit aaa = np.arange(1,101,1)
    aaa*=100

%%timeit aaa = np.arange(1,101,1)
    aaa=aaa*100

And I was surprised because the first test is longer than the second one: 1530ns and 952ns, respectively. Why these values are so different?

2 Answers

TL;DR: this question is equivalent to the performance difference between inplace_binop (INPLACE_*) (aaa*=100) vs binop (BINARY_*) (aaa=aaa*100). The difference can be found by using dis module:

import numpy as np
import dis

aaa = np.arange(1,101,1)
dis.dis('''
for i in range(1000000):
  aaa*=100
''')
  3          14 LOAD_NAME                2 (aaa)
             16 LOAD_CONST               1 (100)
             18 INPLACE_MULTIPLY
             20 STORE_NAME               2 (aaa)
             22 JUMP_ABSOLUTE           10
        >>   24 POP_BLOCK
        >>   26 LOAD_CONST               2 (None)
             28 RETURN_VALUE
dis.dis('''
for i in range(1000000):
  aaa=aaa*100
''')
  3          14 LOAD_NAME                2 (aaa)
             16 LOAD_CONST               1 (100)
             18 BINARY_MULTIPLY
             20 STORE_NAME               2 (aaa)
             22 JUMP_ABSOLUTE           10
        >>   24 POP_BLOCK
        >>   26 LOAD_CONST               2 (None)
             28 RETURN_VALUE

Then back to your question, which is absolutely faster?

Unluckily, it's hard to say which function is faster, here's why:

You can check compile.c of CPython code directly. If you trace a bit into CPython code, here's the function call difference:

  • inplace_binop -> compiler_augassign -> compiler_visit_stmt
  • binop -> compiler_visit_expr1 -> compiler_visit_expr -> compiler_visit_kwonlydefaults

Since the function call and logic are different, that means there are tons of factors (including your input size(*), CPU...etc) could matter to the performance as well, you'll need to work on profiling to optimize your code based on your use case.

*: from others comment, you can check this post to know the performance of different input size.

The += symbol appeared in the C language in the 1970s, and - with the C idea of "smart assembler" correspond to a clearly different machine instruction and addressing mode

"a=a * 100" "a *= 100" produce the same effect but correspond at low level to a different way the processor is working.

a *= 100 means

  • find the place identified by a
  • multiply with 100

a = a * 100 means:

  • evaluate a*100
  • Find the place identified by a
  • Copy a into an accumulator
  • multiply with 100 the accumulator
  • Store the result in a
  • Find the place identified by a
  • Copy the accumulator to it

Python is coded in C, it inherited the syntax from C, but since there is no translation / optimization before the execution in interpreted languages, things are not necessarily so intimately related (since there is one less parsing step). However, an interpreter can refer to different execution routines for the three types of expression, taking advantage of different machine code depending on how the expression is formed and on the evaluation context.

Related