Python 是否会自动将 * 2 替换为 << 1?

Does Python automatically replace * 2 with << 1?

我看到一些建议(请参见 Is multiplication and division using shift operators in C actually faster?)您不应手动将乘法替换为移位运算符,因为编译器必须自动执行此操作,而移位运算符会降低可读性。我写了一个简单的测试来检查这个:

import numpy as np
import time

array1 = np.random.randint(size=10 ** 6, low=0, high=10 ** 5)
array2 = np.zeros((10 ** 6,), dtype=np.int)

total = 0.0
for i in range(100):
    start = time.clock()
    for j in range(len(array2)):
        array2[j] = array1[j] * 2
    total += time.clock() - start
print("*2 time = " + str(round(total / 10, 5)) + " ms")


total = 0.0
for i in range(100):
    start = time.clock()
    for j in range(len(array2)):
        array2[j] = array1[j] << 1
    total += time.clock() - start
print("<< 1 time = " + str(round(total / 10, 5)) + " ms")


total = 0.0
for i in range(100):
    start = time.clock()
    for j in range(len(array2)):
        array2[j] = array1[j] // 2
    total += time.clock() - start
print("//2 time = " + str(round(total / 10, 5)) + " ms")


total = 0.0
for i in range(100):
    start = time.clock()
    for j in range(len(array2)):
        array2[j] = array1[j] >> 1
    total += time.clock() - start
print(">> 1 time = " + str(round(total / 10, 5)) + " ms")

我使用了等效操作(* 2 等效于 << 1// 2 等效于 >> 1),结果如下:

*2 time = 5.15086 ms
<< 1 time = 4.76214 ms
//2 time = 5.17429 ms
>> 1 time = 4.79294 ms

怎么了?我的测试方法错了吗?时间测量有误吗?或者 Python 不执行此类优化(如果是,我应该害怕那个)吗?我在 Win 8.1 x64 上使用了 cPython 3.4.2 x64。

此优化不发生在字节码级别:

>>> import dis
>>> dis.dis(lambda x: x*2)
  1           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (2)
              6 BINARY_MULTIPLY
              7 RETURN_VALUE
>>> dis.dis(lambda x: x<<1)
  1           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (1)
              6 BINARY_LSHIFT
              7 RETURN_VALUE

dis 模块允许您向您展示代码执行时 "inside" Python 发生了什么,或者更准确地说,执行了什么。输出显示 * 运算符映射到 BINARY_MULTIPLY<< 运算符映射到 BINARY_LSHIFT。这两个字节码操作是用C实现的。

使用 dis (to look at the bytecode equivalent of functions) and timeit(比尝试使用 time 手动执行更可靠的计时)可以让您更好地了解内部发生的事情。测试脚本:

def multiply(x):
    return x * 2

def l_shift(x):
    return x << 1

def divide(x):
    return x // 2

def r_shift(x):
    return x >> 1

if __name__ == '__main__':
    import dis
    import timeit

    methods = (multiply, l_shift, divide, r_shift)
    setup = 'from __main__ import {}'.format(
        ', '.join(method.__name__ for method in methods),
    )
    for method in methods:
        print method.__name__
        dis.dis(method)
        print timeit.timeit(
            'for x in range(10): {}(x)'.format(method.__name__),
            setup=setup,
        )
        print

并输出(Windows 7 上的 CPython v2.7.6):

multiply
  2           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (2)
              6 BINARY_MULTIPLY     
              7 RETURN_VALUE        
2.22467834797

l_shift
  5           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (1)
              6 BINARY_LSHIFT       
              7 RETURN_VALUE        
2.05381004158

divide
  8           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (2)
              6 BINARY_FLOOR_DIVIDE 
              7 RETURN_VALUE        
2.43717730095

r_shift
 11           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (1)
              6 BINARY_RSHIFT       
              7 RETURN_VALUE        
2.08359396854

显然 Python 是 而不是 将 multiplication/division 操作替换为等效的移位(例如 BINARY_FLOOR_DIVIDE 未替换为 BINARY_RSHIFT),尽管看起来这样的优化 可以 提高性能。至于 为什么 位移更快,请参见例如Speeds of << >> multiplication and division 关于程序员。

只有在非常有限的情况下,CPython 才能实现这些优化。原因是 CPython 是一种 ducked 类型的语言。

鉴于代码片段 x * 2,这可能意味着非常不同的事情,具体取决于 x 的值。如果 x 是一个整数,那么它确实与 x << 1 具有相同的含义。但是,如果 x 是一个浮点数或字符串或列表或任何其他以其独特的方式实现 __mul__ 的 class 那么它肯定与 x << 1。例如,"a" * 2 == "aa"。因此,除非 x 的值在编译时已知,否则无法进行此优化。如果 x 的值是 事先已知的,那么整个操作可以被优化掉,例如

In [3]: import dis

In [4]: def f():
   ...:     return 2 * 2
   ...: 

In [5]: dis.dis(f)
  2           0 LOAD_CONST               2 (4)
              3 RETURN_VALUE

可以看到编译器自己进行了运算,只是returns常量值4.