【问题标题】:`in range` construction in python 2 --- working too slowpython 2中的`in range`构造---工作太慢
【发布时间】:2018-08-06 15:44:43
【问题描述】:

我想检查给定的x 是否位于区间[0,a-1] 中。 作为一个懒惰的编码员,我写了

x in range(a)

并且(因为那段代码在 4.5 嵌套循环中)很快就会遇到性能问题。我测试了它,事实上,n in range(n) 的运行时间在于 O(n),给予或接受。我实际上以为我的代码会优化为x >= 0 and x < a,但似乎情况并非如此。即使我提前修复了range(a),时间也不会变得恒定(尽管它改进了很多) - 请参阅附注。

所以,我的问题是:

我应该使用x >= 0 and x < a 并且永远不要再写x in range(a) 吗?有没有更好的写法?


旁注:

  1. 我尝试在 SO 中搜索 range, python-2.7, performance 标签,但一无所获(与 python-2.x 相同)。
  2. 如果我尝试以下操作:

    i = range(a)
    ...
    x in i
    

    所以范围是固定的,我只测量 x in i 的运行时间,我仍然得到 O(x) 的运行时间(假设 a 足够大)。

  3. n in xrange(n) 的运行时间也是 O(n)。
  4. 我找到了this post,它对python 3 提出了类似的问题。我决定在python 3 上测试相同的东西,它通过测试就像什么都没有。我为 python 2 感到难过。

【问题讨论】:

  • @KindStranger 是的,带有xrange 的版本比带有range 的版本运行得更快,但性能优于固定范围版本。所以,是的,还有线性时间。好吧,使用 xrange 也就不足为奇了,因为它只是一个生成器,AFAIK。
  • 另外,不确定您希望 Python 如何优化这样的事情。请注意,range 就像任何标识符一样,您可以为其分配任何内容。但无论如何,python 2 中的range 实现了一个列表,然后您进行线性搜索以找到x。所以,是的。
  • @juanpa.arrivillaga 版本与xrange 也需要线性时间。
  • @mike239x 天哪,它是。是的,另一个很好的理由不使用 Python 2。无论如何,xrange 在技术上不是生成器,哎呀,它甚至不是迭代器。它是一个序列类型。
  • 注意x >= 0 and x < a可以写成if 0 <= x < a...

标签: python python-2.7 performance range overhead


【解决方案1】:

range 在 Python 2 中的问题在于它创建了一个 list 的值,因此 x in range(a) 将创建一个列表并线性扫描该列表。 xrange 应该是一个生成器,但是快不了多少;可能仍然只是线性扫描值,而不是先创建整个列表。

In [2]: %timeit 5*10**5 in range(10**6 + 1)  # Python 2
10 loops, best of 3: 18.1 ms per loop

In [3]: %timeit 5*10**5 in xrange(10**6 + 1) # Python 2
100 loops, best of 3: 6.21 ms per loop

Python 3 中,range 更加智能,不仅不会创建整个列表,而且还提供了contains 检查的快速实现。

In [1]: %timeit 5*10**5 in range(10**6 + 1)  # Python 3
1000000 loops, best of 3: 324 ns per loop

更快,恕我直言,更具可读性:使用比较链接:

In [2]: %timeit 0 <= 5*10**5 < 10**6 + 1     # Python 2 or 3
10000000 loops, best of 3: 46.6 ns per loop

我应该使用x &gt;= 0 and x &lt; a 并且永远不再在 range(a) 中写入 x 吗?有没有更好的写法?

“否”、“取决于”和“是”。您不应该使用x &gt;= 0 and x &lt; a,因为0 &lt;= x &lt; a 更短且更易于解析(对于微不足道的人类),并且被解释为(0 &lt;= x) and (x &lt; a)。你不应该在 Python 2 中使用 in range,但是在 Python 3 中,你可以根据需要使用它。

不过,我更喜欢比较链接,因为a &lt;= x &lt; bx in range(a, b) 更明确地定义了边界(如果x == b 会怎样?),这可以防止许多非一错误或+1 填充范围。

另外,请注意0 &lt;= x &lt; ax in range(0, a) 并不完全相同,因为range 只会包含整数值,即1.5 in range(0, 5)False,而0 &lt;= 1.5 &lt; 5True,这可能不是你想要的。此外,使用range 您可以使用1 以外的步骤,例如5 in range(4, 10, 2)False,但同样可以使用纯数学来实现,例如作为(4 &lt;= x &lt; 10) and (x - 4 % 2 == 0)

【讨论】:

  • 附录:如另一个答案中所述,a &lt;= x &lt; bx in range(a, b) 并不完全相同,因为范围只会包含整数值。此外,使用 range 您可以使用除 1 以外的步骤,例如5 in range(2, 10, 2)False
  • 您能否在您的答案中添加python3范围可以使用标题中的from future.builtins import range导入python2?
  • @mike239x:python 文档中没有提到。 docs.python.org/2/library/future_builtins.html
  • @HåkenLid 1) 我的是future.builtins,你的是future_builtins 2) 我找到了这个网站:python-future.org/imports.html 并且范围在 builtins 下有说明 3) 它对我有用
  • 任何人都知道为什么要投反对票,所以我可以解决它,不管它是什么?
【解决方案2】:

通过使用自定义范围类并覆盖in 运算符,您可以获得与python3 中相同的性能。对于琐碎的情况,它的性能不如简单比较,但您将避免使用内置 range()xrange() 获得的 O(n) 内存和时间使用。

请注意,测试 value in range(low, high)low &lt; value &lt;= high 不同,因为范围仅包含整数。所以7.2 in range(10) == False

但更重要的是range() 可以采用可选的第三个step 参数,因此如果您需要测试value in range(low, high, step),您可以考虑使用自定义类。

编辑:@mike239x 找到了future package,其中包含一个类似于我的答案中的range 对象(除了帮助您编写与python2/3 交叉兼容的代码的其他函数之外)。使用它应该是安全的,因为它可能已经过良好的测试和稳定。

这个类的一个对象包装了一个xrange 对象,并且只覆盖了非常昂贵的in 操作。对于常规迭代,它的工作原理与 xrange 一样。

class range(object):
  """Python 2 range class that emulates the constant time `in` operation from python 3"""

  def __init__(self, *args):
    self.start, self.end = (0, args[0]) if len(args) == 1 else args[:2]
    self.step = 1 if len(args) < 3 else args[2]
    self.xrange = xrange(*args)

  def __contains__(self, other):
    # implements the `in` operator as O(1) instead of xrange's O(n)
    try:
      assert int(other) == other
    except Exception:
      return False  # other is not an integer
    if self.step > 0:
      if not self.start <= other < self.end:
        return False  # other is out of range
    else:
      if not self.start >= other > self.end:
        return False  # other is out of range
    # other is in range. Check if it's a valid step
    return (self.start - other) % self.step == 0

  def __iter__(self):
    # returns an iterator used in for loops
    return iter(self.xrange)

  def __getattr__(self, attr):
    # delegate failed attribute lookups to the encapsulated xrange
    return getattr(self.xrange, attr)

内置的xrange对象是用C实现的,所以我们不能使用类继承。相反,我们可以使用组合并将除__contains__ 之外的所有内容委托给封装的xrange 对象。

contains 的实现可以与 cpython rangeobject 实现中的range_contains_long 进行比较。 Here's the python 3.6 source code for that function.

编辑:如需更全面的 python 实现,请查看future.builtins.range from the future library.

【讨论】:

  • 您可能会感兴趣,一旦确实进行了基准测试,原生 range() 的类定义替代品承受 O( 1 ) 缩放的恒定成本 ~ 5 [us] - 29 [us] (即更多时间),比 O/P 要求比较的 - 即 if ( 0 &lt;= x &lt; a ) 语法,其处理成本约为 0 [us]。
  • 感谢基准测试。你的结果符合我的预期。即使改进的range 提供恒定时间查找,由于对象创建、方法调用等仍然存在开销。
  • 听到这么多次“谢谢”真的很有趣,同时却因为同样的、数量上公平且可重复的基准而获得了几次愤怒和可恨的 DownVotes。 (您无需命名与您提出的方法相关的开销,已经花费了数十年的时间来进行性能调整和延迟削减(有关更多详细信息,请参阅重新制定的 Amdahl 定律上下文,其中幼稚/处理不当的开销成本可能会变成 N 并行代码- 执行计划很容易工作比纯[SERIAL]、原始代码执行策略慢)。基准提供事实+显示成本(in-)效率 G/ L
【解决方案3】:

Call x in range( a ) slow?(注意 py2 hidden RISK if using range() ...)

       23[us] spent in [py2] to process ( x in range( 10E+0000 ) )
        4[us] spent in [py2] to process ( x in range( 10E+0001 ) )
        3[us] spent in [py2] to process ( x in range( 10E+0002 ) )
       37[us] spent in [py2] to process ( x in range( 10E+0003 ) )
      404[us] spent in [py2] to process ( x in range( 10E+0004 ) )
     4433[us] spent in [py2] to process ( x in range( 10E+0005 ) )
    45972[us] spent in [py2] to process ( x in range( 10E+0006 ) )
   490026[us] spent in [py2] to process ( x in range( 10E+0007 ) )
  2735056[us] spent in [py2] to process ( x in range( 10E+0008 ) )

MemoryError

in range( a ) 构造函数的语法不仅在 [TIME] 域中是 slow,而且具有 --at best-- O (log N),如果做得更聪明,比通过列表值的枚举域进行纯顺序搜索,但是

py2,本机 range() 总是有一个复合附加组件 O( N ) 成本,即 [TIME] 域成本(构建时间)和这种基于 range 的内存的 [SPACE] 域成本(分配存储空间 + 花费更多时间将所有这些数据通过...) - 表示构造。


让我们对安全的O( 1 ) 规模化方法进行基准测试(+总是做基准测试)

>>> from zmq import Stopwatch
>>> aClk = Stopwatch()
>>> a = 123456789; x = 123456; aClk.start(); _ = ( 0 <= x < a );aClk.stop()
4L
>>> a = 123456789; x = 123456; aClk.start(); _ = ( 0 <= x < a );aClk.stop()
3L

评估基于条件的公式需要 3 ~ 4 [us],具有 O(1) 缩放,对于 x 量级不变。


接下来,使用 x in range( a ) 公式进行测试:

>>> a = 123456789; x = 123456; aClk.start(); _ = ( x in range( a ) );aClk.stop()

并且您的机器几乎会在内存吞吐量受限的 CPU 饥饿中死机(更不用说从一些 ~ 100 [ns] 成本范围到一些 @ 987654340@ 交换磁盘 IO 数据流的成本)。


不,不,不。永远无法测试 x 是否在有界范围内。

创建其他基于类的评估器的想法,仍然通过枚举( set )解决问题将永远无法满足基准 3 ~ 4 [us] (如果不使用一些超出我对经典和量子物理学因果定律的理解的外星魔法)


Python 3 has changed the way, how the range()-constructor works,但这不是原帖的核心价值:

    3 [us] spent in [py3] to process ( x in range( 10E+0000 ) )
    2 [us] spent in [py3] to process ( x in range( 10E+0001 ) )
    1 [us] spent in [py3] to process ( x in range( 10E+0002 ) )
    2 [us] spent in [py3] to process ( x in range( 10E+0003 ) )
    1 [us] spent in [py3] to process ( x in range( 10E+0004 ) )
    1 [us] spent in [py3] to process ( x in range( 10E+0005 ) )
    1 [us] spent in [py3] to process ( x in range( 10E+0006 ) )
    1 [us] spent in [py3] to process ( x in range( 10E+0007 ) )
    1 [us] spent in [py3] to process ( x in range( 10E+0008 ) )
    1 [us] spent in [py3] to process ( x in range( 10E+0009 ) )
    2 [us] spent in [py3] to process ( x in range( 10E+0010 ) )
    1 [us] spent in [py3] to process ( x in range( 10E+0011 ) ) 

在 Python 2 中,range()xrange() 都没有摆脱 O( N ) 缩放的陷阱,其中 xrange()-generator 的运行速度似乎只有 2x 慢

>>> from zmq import Stopwatch
>>> aClk = Stopwatch()

>>> for expo in xrange( 8 ):
...     a = int( 10**expo); x = a-2; aClk.start(); _ = ( x in range( a ) );aClk.stop()
...
3L
8L
5L
40L
337L
3787L
40466L
401572L
>>> for expo in xrange( 8 ):
...     a = int( 10**expo); x = a-2; aClk.start(); _ = ( x in xrange( a ) );aClk.stop()
...
3L
10L
7L
77L
271L
2772L
28338L
280464L

范围边界语法享有 ~ &lt; 1 [us]O( 1 ) 恒定时间,如上所示,因此设置了再次比较的标准:

>>> for expo in xrange( 8 ):
...     a = int( 10**expo); x = a-2; aClk.start(); _ = ( 0 <= x < a );aClk.stop()
...
2L
0L
1L
0L
0L
1L
0L
1L

【讨论】:

  • 不是我的反对意见,但您基本上只是重复 OP 已经说过的话:in range 很慢(至少在 Python 2 中)。你没有提到为什么会这样(尽管可以在你知道为什么的字里行间读出来;也许它看起来太明显了?)
  • @user3666197,如果您认为学习基准方法很重要,那么问题下的 cmets 就可以了。不是答案。
  • @tobias_k ,通常情况下,我会引导我的学生更好地先从测试中收集定量证据,然后尝试阐明为什么会发生这种情况。告诉结果永远不会比按照自己的方式从假设到结论更有帮助。最后,下半场的反模式测试正是这样,如果在a(1E3,1E4,1E5,1E6,1E7,1E8,1E9)的规模上运行可能会更好) 以便在缓存和内存计算开始交换并几乎冻结 O/S 性能之后闻到烟味。
  • @mike239x 你专注于可怕这个词——你注意到这个主题了吗? 隐藏在您的代码中的风险确实 IS AWFUL。严肃和负责任的设计不会留下破坏系统的空间。那么,如果结果的成本不超过~ 3 [us]a的大小无关,为什么还要冒着冻结操作系统的风险(交换memory2disk直到死) /b> 直到任何高于1E+123... 的无限制精度 python 可以提供 - 你是否意识到问题的根本原因是什么?似乎“AWFUL”这个词让你错过了核心信息。我的错。
  • @mike239x StackOverflow 不是 PR 站点。如果阅读 (cit.): "...可能会被解读为“你说这慢吗?你错了”暗示“它很快”......之后一个人不会再读下去,只是投反对票。”嗯,这样的人很自暴自弃。在 StackOverflow 上遇到了超过 510.000 名读者的关注,只有一小部分人决定表达你在解释中勾勒出的行为。是的,那些没有进一步重新思考课文就做出反应的人是在仇恨中迷失的学生。答案中的第一行清晰且听起来“不仅 ...”
【解决方案4】:

所以,是的,基本上,在 Python 2 中使用 range(如上所述)是一个坏主意 - python 实际上创建了一个包含范围的所有值的列表 + 之后它以最直接的方式搜索整个列表大大地。

其中一个选项如下:使用 Python 3 中的 range,它可以更好地处理这种情况 for various reasons。 “好吧”,你问,“我如何在 Python 2 中使用来自 Python 3 的range”?答案是:使用futurelibrary。安装那个,写下来

from future.builtins import range

在您的代码头和 wuolah 中!- 您的范围现在的行为与 Python 3 中的一样,现在您可以再次使用 x in range(a),而不会出现任何性能问题。

【讨论】:

    猜你喜欢
    • 2017-11-23
    • 2023-03-20
    • 1970-01-01
    • 2022-06-26
    • 2014-04-06
    • 1970-01-01
    • 2019-10-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多