【问题标题】:Why is creating a range from 0 to log(len(list), 2) so slow?为什么创建从 0 到 log(len(list), 2) 的范围这么慢?
【发布时间】:2013-04-23 18:01:53
【问题描述】:

我不知道为什么会发生这种情况。我弄乱了一些列表,我需要一个从 0 到 log(n, 2)for 循环,其中 n 是列表的长度。但是代码非常慢,所以经过一番研究,我发现问题出在范围生成上。演示示例代码:

n = len([1,2,3,4,5,6,7,8])
k = 8
timeit('range(log(n, 2))', number=2, repeat=3) # Test 1
timeit('range(log(k, 2))', number=2, repeat=3) # Test 2

输出

2 loops, best of 3: 2.2 s per loop
2 loops, best of 3: 3.46 µs per loop

测试的数量很少(我不希望它运行超过 10 分钟),但它已经表明 range(log(n, 2)) 比仅使用整数对数的对应物慢几个数量级。这真的很令人惊讶,我不知道为什么会发生这种情况。可能是我的 PC 上的问题,可能是 Sage 问题或 Python 错误(我没有在 Python 上尝试过同样的问题)。

使用xrange 代替range 也无济于事。另外,如果你得到带有.n() 的号码,测试 1 会以与 2 相同的速度运行。

有人知道会发生什么吗? 谢谢!

【问题讨论】:

  • 听起来像是 Sage(也许是 cython?)的问题。 Python range 甚至不接受浮点数。
  • 而且 Python 在全局命名空间中也没有 log(如果不将 setup 添加到 timeit 就无法到达那里)。并且n 也不适用于timeittimeit 上没有 repeat 参数(我假设您使用 from timeit import timeit 获得)。
  • 您的输出不是显示您的timeit 返回的值是相当随机的吗?毕竟你尝试了两次同样的事情(nk 都是 8),得到的结果差别很大。
  • 你真的预先计算了n吗?
  • “另外,如果你得到带有.n()的号码”。等等,什么?从哪里获取what号? AFAIK,Sage 建立在 ipython 之上,其所有“神奇”语法都以 %! 开头。

标签: python performance sage


【解决方案1】:

天哪——我认得这个。它与我的一个trac #12121 有关。首先,由于无聊的原因,使用 Python int 而不是 Sage Integer 会产生额外的开销:

sage: log(8, 2)
3
sage: type(log(8, 2))
sage.rings.integer.Integer
sage: log(8r, 2)
log(8)/log(2)
sage: type(log(8r, 2))
sage.symbolic.expression.Expression
sage: %timeit log(8, 2)
1000000 loops, best of 3: 1.4 us per loop
sage: %timeit log(8r, 2)
1000 loops, best of 3: 404 us per loop

r 后缀表示“原始”,并防止 Sage 预解析器将文字 2 包装成 Integer(2)

然后它变得很奇怪。为了产生一个 int 供range 使用,Sage 必须弄清楚如何将log(8)/log(2) 变成 3,结果证明她做了最糟糕的事情。抄袭我原来的诊断(比照):

首先,她检查这个对象是否有自己的方式来获取一个 int,但它没有。所以她用 log(8)/log(2) 构建了一个 RealInterval 对象,结果证明这是她能做的最糟糕的事情!她检查区间的上下部分是否一致(我的意思是在地板上)(以便她确定地板是什么)。但在这种情况下,因为它确实是一个整数!这总是看起来像:

sage: y = log(8)/log(2)
sage: rif = RealIntervalField(53)(y)
sage: rif
3.000000000000000?
sage: rif.endpoints()
(2.99999999999999, 3.00000000000001)

这两个界限的底线不相等,所以 Sage 认为她还没有解决这个问题,她不断将精度提高到 20000 位,看看她是否能证明它们是……但是通过建设它永远不会工作。最后她放弃并尝试简化它,它成功了:

sage: y.simplify_full()
3

证明它是完全可分情况的反常性质:

sage: %timeit range(log(8r, 2))
1 loops, best of 3: 2.18 s per loop
sage: %timeit range(log(9r, 2))
1000 loops, best of 3: 766 us per loop
sage: %timeit range(log(15r, 2))
1000 loops, best of 3: 764 us per loop
sage: %timeit range(log(16r, 2))
1 loops, best of 3: 2.19 s per loop

【讨论】:

  • 不错的诊断...你现在要为它写一个补丁吗? :)
【解决方案2】:

这看起来像是一个 Sage 错误。

我创建了一个新笔记本并这样做了:

n = len([1,2,3,4,5,6,7,8])
k = 8
timeit('range(log(n, 2))', number=2, repeat=3) # Test 1
timeit('range(log(len([1,2,3,4,5,6,7,8]), 2))', number=2, repeat=3) # Test 1.5
timeit('range(log(k, 2))', number=2, repeat=3) # Test 2

测试 1.5 和测试 1 一样慢。但是,如果您以任何方式对其进行分解——去掉range,或者甚至添加m=n+0 并使用m 而不是n,它就会下降到微秒。

很明显,Sage 在评估表达式时试图在这里做一些复杂的事情,并感到困惑。


为了验证这一点,用普通的旧 ipython:

n = len([1,2,3,4,5,6,7,8])
k = 8
%timeit 'range(log(n, 2))'
%timeit 'range(log(len([1,2,3,4,5,6,7,8]), 2))'
%timeit 'range(log(k, 2))'

正如你所料,它们都同样快。


那么……你会怎么做呢?

好吧,您可能想尝试追踪 Sage 错误并将其提交到上游。但与此同时,您可能希望在代码中找到一种解决方法。

如上所述,只需执行m = n+0 并使用m 而不是n 似乎可以加快速度。看看这对你有用吗?

【讨论】:

  • 是的,解决方法就像计算 log(n, 2).n() 的对数值一样简单。这会将表达式 log(8)/log(2) 转换为数字 (3),从而加快执行速度。
【解决方案3】:

Python 2 允许 range(some_float),但它已被弃用并且在 python 3 中不起作用。

代码示例没有给出指定的输出。但我们可以通过它。首先timeit需要一个完整的脚本,调用timeit的脚本中的import是不用的:

>>> timeit('range(log(8,2))')
  Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib64/python2.6/timeit.py", line 226, in timeit
    return Timer(stmt, setup, timer).timeit(number)
  File "/usr/lib64/python2.6/timeit.py", line 192, in timeit
    timing = self.inner(it, self.timer)
  File "<timeit-src>", line 6, in inner
NameError: global name 'log' is not defined

如果将导入添加到正在计时的脚本中,它包括设置时间:

>>> timeit('from math import log;range(log(8,2))')
3.7010221481323242

如果您将导入移动到设置中,效果会更好,但众所周知,一次性的时间不准确:

>>> timeit('range(log(8,2))',setup='from math import log')
1.9139349460601807

最后,运行多次,你得到一个不错的数字:

>>> timeit('range(log(8,2))',setup='from math import log',number=100)
0.00038290023803710938

【讨论】:

  • 看起来 Sage 笔记本将 log 导入到全局变量中。 (并且 OP 已经在他的 timeit 上使用了 number。)
【解决方案4】:

也许首先使用log(x, 2)(又名ld())并不是一个好主意。我建议使用转换 int 值来实现ld()

n = len(array)
while n:
  n >>= 1
  # perform the loop stuff

这样您就可以避免range()log() 的所有这些丑陋之处。

在正常情况下,调用log() 应该比在 int 上进行简单的位移花费更多的时间。例子:

>>> timeit('for i in range(int(math.log(8, 2))): pass', setup='import math')
0.6762251853942871
>>> timeit('n = 8\nwhile n:\n  n >>= 1')
0.24107813835144043

n 的值越大,差异就越小。对于n = 10000,我得到了 0.8163230419158936 和 0.8106038570404053,但这应该是因为与循环初始化相比,循环体将花费大部分时间。

【讨论】:

  • 我愿意这样,在 Python 中编写一个围绕位移的正确循环将比调用 log 慢得多。但无论我是对还是错,你绝对不应该在没有尝试测试的情况下断言它更快。
  • 也许你不应该假设我没有测试。在当今的处理器中,位移并不比加法或其他简单算术慢,这是我的基本知识的一部分。但我添加了一些测试来证明我的观点。
  • 好的,所以你给苹果和橘子计时了,而不是什么都不计时。第一个版本甚至不等同于第二个版本,因为它有一个额外的 for i in range(…) 循环,而另一个没有(这消除了 Python 中没有紧密循环的所有优势)。
  • 另一个有 while 循环作为替换,正如我的回答所建议的那样。
  • 试试from math import log时间
猜你喜欢
  • 1970-01-01
  • 2021-10-08
  • 2022-11-30
  • 1970-01-01
  • 2010-10-15
  • 1970-01-01
  • 1970-01-01
  • 2021-12-21
  • 2022-08-10
相关资源
最近更新 更多