【问题标题】:Why is 2 * x * x faster than 2 * ( x * x ) in Python 3.x, for integers?对于整数,为什么 2 * x * x 在 Python 3.x 中比 2 * ( x * x ) 快?
【发布时间】:2019-05-03 09:30:14
【问题描述】:

以下 Python 3.x 整数乘法平均需要 1.66 秒到 1.77 秒:

import time
start_time = time.time()
num = 0
for x in range(0, 10000000):
    # num += 2 * (x * x)
    num += 2 * x * x
print("--- %s seconds ---" % (time.time() - start_time))

如果我将2 * x * x 替换为2 *(x * x),则需要在2.042.25 之间。怎么会?

另一方面,Java 中则相反:2 * (x * x) 在 Java 中更快。 Java测试链接:Why is 2 * (i * i) faster than 2 * i * i in Java?

每个版本的程序我运行了 10 次,结果如下。

   2 * x * x        |   2 * (x * x)
---------------------------------------
1.7717654705047607  | 2.0789272785186768
1.735931396484375   | 2.1166207790374756
1.7093875408172607  | 2.024367570877075
1.7004504203796387  | 2.047525405883789
1.6676218509674072  | 2.254328966140747
1.699510097503662   | 2.0949244499206543
1.6889283657073975  | 2.0841963291168213
1.7243537902832031  | 2.1290600299835205
1.712965488433838   | 2.1942825317382812
1.7622807025909424  | 2.1200053691864014

【问题讨论】:

  • 小提示:使用timeit 模块获得更好的统计数据
  • 为了更好的衡量标准,您不妨也输入2 * pow(x,2)2 * x**2。另外,请使用timeit 重做您的计时,它比time.time() 对于短流程更准确。
  • 相关的近乎重复,虽然不是很清楚地说明:Times-two faster than bit-shift, for Python 3.x integers?

标签: python python-3.x performance benchmarking integer-arithmetic


【解决方案1】:

如果您的基准测试是正确的(未检查),这可能是因为 Python 整数可能是两种不同的东西:当它们很小时是原生整数(计算速度很快),而当它们增加时是大整数大小(较慢的计算)。第一种语法在第一次操作后保持较小的大小,而第二种语法可能导致涉及大整数的两次操作。

【讨论】:

  • 似乎更像是一个猜测(尽管是一个很好的猜测)而不是一个答案。使用具有不同大小的timeit x 可能会提供所需的证据,将其从猜测推向答案。
  • python 为 -5 到 256 Dok “缓存”了整数 - 如果两个公式更接近相同的时间,如果只有小整数在起作用,这很容易实现吗?最大值为 2e+14,它小到足以容纳 64 位有符号整数,因此处理器 int 计算限制可能超出了游戏范围 - 对于 32 位无符号整数来说太大了,所以在 32 位上这可能考虑因素?
  • 我仍然 measure a 5% difference 使用 2 作为 x 的值。
  • 答案可能更像是一个猜测,但在我看来这很有帮助,因为我刚刚学到了更多关于 Python 的知识。
【解决方案2】:

整数的 Python 实习生表示是特殊的,它使用 30 位的槽:

In [6]: sys.getsizeof(2**30-1)
Out[6]: 28 # one slot + heading

In [7]: sys.getsizeof(2**30)
Out[7]: 32 # two slots 

所以一切都会发生,就好像 Python 在基数 B = 2**30 = 1 073 741 824 ~1 billion 中计数一样。

对于一个想要计算 2*4*4 的人来说,有两种方法:

  • (2*4)*4 = 8*4 =32 = 30 + 2 如果您知道添加表,则立即生效。
  • 2*(4*4) = 2*16 = 2*10 + 2*6 = (2*10+10) + 2 = 30 + 2 因为我们必须放下操作。

Python 也有同样的问题。如果 x 是诸如 2x < B < x² 之类的数字,则让 x² = aB+ba,b <B 存储在 2 个插槽中,我注意到 (a|b)。计算导致(此处不管理进位):

   (x*x)*2 =>  (a|b)*2 => (2*a|2*b)
   (2*x)*x =>  (2x)*x =>(2a|2b)

在第一种情况下,2* 操作执行了两次,而第一种情况下只针对一次。这就解释了差异。

【讨论】:

  • abX是从哪里来的,它们代表什么以及它们与x的价值有什么关系?
  • @Bergi: abx*x 的高和低 30 位块。 (大写X 不再出现在答案的当前版本中。)
【解决方案3】:

首先,请注意我们在 Python 2.x 中看不到同样的东西:

>>> timeit("for i in range(1000): 2*i*i")
51.00784397125244
>>> timeit("for i in range(1000): 2*(i*i)")
50.48330092430115

所以这让我们相信这是由于 Python 3 中整数的变化:具体来说,Python 3 在任何地方都使用long(任意大的整数)。

对于足够小的整数(包括我们在这里考虑的整数),CPython 实际上只使用 O(MN) grade-school digit by digit multiplication algorithm(对于更大的整数,它会切换到 Karatsuba algorithm)。你可以在source看到这个。

x*x 的位数大约是2*xx 的两倍(因为 log(x2) = 2 log(x))。请注意,此上下文中的“数字”不是以 10 为底的数字,而是 30 位值(在 CPython 的实现中被视为单个数字)。因此,2 是一位数值,x2*x 是循环所有迭代的一位数值,但 x*x 对于 x >= 2**15 是两位数值。因此,对于x >= 2**152*x*x 只需要一位数乘法,而 2*(x*x) 需要一位数乘法和一位数乘法(因为 x*x 有 2 30-位数字)。

这是查看此内容的直接方法(Python 3):

>>> timeit("a*b", "a,b = 2, 123456**2", number=100000000)
5.796971936999967
>>> timeit("a*b", "a,b = 2*123456, 123456", number=100000000)
4.3559221399999615

再次将其与 Python 2 进行比较,后者不会在任何地方使用任意长度的整数:

>>> timeit("a*b", "a,b = 2, 123456**2", number=100000000)
3.0912468433380127
>>> timeit("a*b", "a,b = 2*123456, 123456", number=100000000)
3.1120400428771973

(一个有趣的注释:如果您查看源代码,您会发现该算法实际上具有平方数的特殊情况(我们正在这里做),但这仍然不足以克服这一事实2*(x*x) 只需要处理更多的数字。)

【讨论】:

  • Karatsuba 算法比数字乘法算法慢吗?
  • @BanghuaZhao 它具有更好的运行时复杂度(相对于位数),但位数必须足够大才能真正值得。
  • 来源:#define KARATSUBA_CUTOFF 70。因此,仅当整数具有大约 600 个十进制数字时才使用 Karatsuba 算法。这不是这里的问题。
  • 在 Python 中,“数字”是 30 位或 60 位的块,对吧?所以基数 2^30,而不是 十进制 数字。同样至关重要的是:Java int 在 2^32 处换行,而 Python 执行任意精度。此外,Java JIT 使用 32 位寄存器编译为(低效的)本机代码,而 CPython 则一路解释。所以这些差异很大。
  • 问题中涉及的数字足够小,不涉及 Karatsuba,除此之外,它们足够小,以至于渐近考虑完全无关紧要。所有涉及的操作数和结果都适合两个 30 位内部“数字”。
【解决方案4】:

据我所知,在使用2 * (x * x) 的版本中,它归结为更多的内存访问。我打印了反汇编的字节码,似乎证明了:

2 * x * x的相关部分:

7          28 LOAD_FAST                1 (num)
           30 LOAD_CONST               3 (2)
           32 LOAD_FAST                2 (x)
           34 BINARY_MULTIPLY
           36 LOAD_FAST                2 (x)
           38 BINARY_MULTIPLY
           40 INPLACE_ADD
           42 STORE_FAST               1 (num)
           44 JUMP_ABSOLUTE           24

2 * (x * x)的相关部分:

  7          28 LOAD_FAST                1 (num)
             30 LOAD_CONST               3 (2)
             32 LOAD_FAST                2 (x)
             34 LOAD_FAST                2 (x)
             36 BINARY_MULTIPLY                 <=== 1st multiply x*x in a temp value
             38 BINARY_MULTIPLY                 <=== then multiply result with 2
             40 INPLACE_ADD
             42 STORE_FAST               1 (num)
             44 JUMP_ABSOLUTE           24

【讨论】:

  • 如果是这种情况,我们会在 Python 2 中看到相同的效果,但我们没有。
  • 它只是在一条指令之后移动了LOAD_FAST。这个“更多的内存访问”如何?
  • @arshajii 你是对的,我检查了 Python 2 的反汇编代码,它显示了同样的内容。尽管我仍然相信它可能会产生影响,但与您提到的相比微不足道。
  • @SamMason 在第二种情况下,它需要将x*x 的结果写回一个临时值,因为下一次乘法需要它,而在第一种情况下,它总是写入相同的登记。所以第二种情况是先读后写。然而,这是一个非常小的惩罚,但在热循环中,它可能会产生影响。
  • CPython 是一个堆栈机器,它总是将计算结果留在/写入堆栈顶部......可能会有一些与缓存相关的影响,但对于如此接近的代码来说,它会非常小跨度>
猜你喜欢
  • 2015-05-07
  • 2017-07-10
  • 2012-04-06
  • 2013-08-29
  • 1970-01-01
  • 1970-01-01
  • 2019-08-02
  • 1970-01-01
  • 2020-06-26
相关资源
最近更新 更多