divmod()比使用%和//运算符更快吗?

36

我还记得在汇编中,整数除法指令会返回商和余数。那么在Python中,使用内置的divmod()函数相比于使用%//运算符(假设需要同时获取商和余数)性能更好吗?

q, r = divmod(n, d)
q, r = (n // d, n % d)

10
为什么不直接使用timeit来找出答案? - Martijn Pieters
1
函数调用开销(字节码中的CALL_FUNCTION)可能会导致第一个版本执行较慢,但是使用timeit可以确保。 - Łukasz Rogalski
1
哪种实现? - Peter Wood
3个回答

68

测量即了解(以下所有时间均基于 Macbook Pro 2.8Ghz i7):

>>> import sys, timeit
>>> sys.version_info
sys.version_info(major=2, minor=7, micro=12, releaselevel='final', serial=0)
>>> timeit.timeit('divmod(n, d)', 'n, d = 42, 7')
0.1473848819732666
>>> timeit.timeit('n // d, n % d', 'n, d = 42, 7')
0.10324406623840332

divmod()函数在这里处于不利地位,因为每次需要查找全局变量。将其绑定到本地变量(timeit时间试验中的所有变量都是本地变量)可以稍微提高性能:

>>> timeit.timeit('dm(n, d)', 'n, d = 42, 7; dm = divmod')
0.13460898399353027

但运算符仍然获胜,因为在执行函数调用 divmod() 时不必保留当前帧:

>>> import dis
>>> dis.dis(compile('divmod(n, d)', '', 'exec'))
  1           0 LOAD_NAME                0 (divmod)
              3 LOAD_NAME                1 (n)
              6 LOAD_NAME                2 (d)
              9 CALL_FUNCTION            2
             12 POP_TOP             
             13 LOAD_CONST               0 (None)
             16 RETURN_VALUE        
>>> dis.dis(compile('(n // d, n % d)', '', 'exec'))
  1           0 LOAD_NAME                0 (n)
              3 LOAD_NAME                1 (d)
              6 BINARY_FLOOR_DIVIDE 
              7 LOAD_NAME                0 (n)
             10 LOAD_NAME                1 (d)
             13 BINARY_MODULO       
             14 BUILD_TUPLE              2
             17 POP_TOP             
             18 LOAD_CONST               0 (None)
             21 RETURN_VALUE        

//%变体使用更多的操作码,但从性能上来看,CALL_FUNCTION字节码比较棘手。


对于小整数,在PyPy中实际上没有太大区别;操作码的微小速度优势在C整数算术的高速性下消失得无影无踪:

>>>> import platform, sys, timeit
>>>> platform.python_implementation(), sys.version_info
('PyPy', (major=2, minor=7, micro=10, releaselevel='final', serial=42))
>>>> timeit.timeit('divmod(n, d)', 'n, d = 42, 7', number=10**9)
0.5659301280975342
>>>> timeit.timeit('n // d, n % d', 'n, d = 42, 7', number=10**9)
0.5471200942993164

我不得不把重复次数增加到10亿次才能展示出差异是多么微小,PyPy在这里的速度非常快。

然而,当数字变得较大时,divmod()函数的速度则显著胜出:

>>>> timeit.timeit('divmod(n, d)', 'n, d = 2**74207281 - 1, 26', number=100)
17.620037078857422
>>>> timeit.timeit('n // d, n % d', 'n, d = 2**74207281 - 1, 26', number=100)
34.44323515892029

(现在,为了能在合理的时间内得到结果,我不得不将重复次数与霍布斯的数字相比降低一个数量级。)

这是因为PyPy不能再将那些整数作为C整数来解包;你可以看到使用sys.maxintsys.maxint + 1之间的时间差异显著:

>>>> timeit.timeit('divmod(n, d)', 'import sys; n, d = sys.maxint, 26', number=10**7)
0.008622884750366211
>>>> timeit.timeit('n // d, n % d', 'import sys; n, d = sys.maxint, 26', number=10**7)
0.007693052291870117
>>>> timeit.timeit('divmod(n, d)', 'import sys; n, d = sys.maxint + 1, 26', number=10**7)
0.8396248817443848
>>>> timeit.timeit('n // d, n % d', 'import sys; n, d = sys.maxint + 1, 26', number=10**7)
1.0117690563201904

5
为什么在处理大数时divmod完全取代了//和%的功能呢? - Dimitris Fasarakis Hilliard
3
@JimFasarakis-Hilliard:大数支持。您不能再在C整数值上使用基本的C &掩码操作,因此通常的优化不再适用。您必须使用与int大小成正比的算法,并且在进行divmod时执行一次,而使用单个运算符执行两次会对其造成严重损害。 - Martijn Pieters
@AnnZen 你是在使用PyPy还是代码处于关键路径并需要尽可能快地执行?如果不是,那就选择你认为更能传达意图的方式。 - Martijn Pieters
@MartijnPieters 代码不需要尽可能快...但我实际上需要坐标反转。使用divmod(a, b)[::-1]a // b, a % b更能表达意图吗?谢谢! - Ann Zen
@AnnZen 我认为不行。(a // b, a % b)divmod(a, b)[::-1] 更能传达意图。我老实说,我不会记得 divmod 的输出顺序。当将输出分配给两个变量时,变量名通常可以清楚地表明它们的含义。 - nog642

28

如果你使用“小型”原生整数,其中算术运算比函数调用快得多,那么Martijn的答案是正确的。然而,对于大整数,情况完全不同:

>>> import timeit
>>> timeit.timeit('divmod(n, d)', 'n, d = 2**74207281 - 1, 26', number=1000)
24.22666597366333
>>> timeit.timeit('n // d, n % d', 'n, d = 2**74207281 - 1, 26', number=1000)
49.517399072647095

当分割一个22百万位数字时,divmod几乎比单独执行除法和模数运算快两倍,正如您所预期的那样。

在我的机器上,交叉点大约在2 ^ 63左右,但不要相信我的话。正如Martijn所说,进行测量!当性能真正重要时,不要假设在一个地方成立的事情仍然会在另一个地方成立。


-1
我正在遍历一个数组pulse_onset来计算divmod
pulse_onset.shape
(14307,)

使用这两个变量,divmod 更快。我猜测两次迭代数组比使用 divmod 更差,但我对内部机制不够熟悉。

t0 = time.time()
div_array = np.array([divmod(i, 256) for i in pulse_onset])
t1 = time.time()
total = t1-t0
print(total)
0.008155584335327148

t0 = time.time()
pulse_onset_int = np.array([round(i / 256) for i in pulse_onset])
remainder = np.array([i % 256 for i in pulse_onset])
t1 = time.time()
0.5383238792419434

做类似于这样的事情

np.array([(round(i / 256), i % 256) for i in pulse_onset])

稍微节省了一点时间(0.39537501335144043),所以可能不是大部分时间在数组上进行两次迭代,而是进行两次除法运算的时间更长。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接