【问题标题】:What is the preferred way to take the modulus by a power of 2 of an int in Python在 Python 中以 2 的幂取模的首选方法是什么?
【发布时间】:2014-02-07 12:56:15
【问题描述】:

假设您有一些计数器或其他数据元素需要存储在二进制协议的字段中。该字段自然有一些固定数量的 n 位,并且协议规定您应该存储计数器的 n 个最低有效位,以便它在它太高时回绕大的。一种可能的实现方法实际上是将模数乘以 2 的幂:

field_value = counter % 2 ** n

但这肯定不是最有效的方法,甚至可能不是最容易理解的方法,考虑到规范正在讨论最低有效位并且没有提到模运算。因此,研究替代方案是适当的。一些例子是:

field_value = counter % (1 << n)
field_value = counter & (1 << n) - 1
field_value = counter & ~(-1 << 8)

有经验的 Python 程序员希望在不牺牲太多性能的情况下最大限度地提高代码清晰度来实现此类要求的方式是什么?

这个问题当然没有正确或错误的答案,所以我想用这个问题来收集这个看似微不足道的需求的所有合理实现。答案应列出替代方案,并简要说明在什么情况下最好使用哪种替代方案。

【问题讨论】:

  • 这是什么版本的 Python?
  • “规范是在讨论最低有效位,没有提到取模运算。” 没错,但我怀疑它也没有提到&lt;&lt; 或@987654324 @ 或 ~,所以在这方面它并不比你的任何其他例子差。
  • 使其易于理解和阅读。忘记性能。稍后在 Profiler 的帮助下进行优化。
  • 只有分析器才能告诉你什么是更快的。如果 python 解释器有任何价值,它就会知道这些类型的转换,并且无论如何都应该为你应用它们。
  • @SimeonVisser Solutions 最好在 2.7 和 3.0 及更高版本中工作。

标签: python integer twos-complement


【解决方案1】:

在您的情况下,位移和按位运算更具可读性。因为它只是告诉读者,你在这里做的是按位运算。如果使用数字运算,读者可能无法理解取模这个数字是什么意思。

说到性能,其实在 Python 中你不必太担心这个。因为对 Python 对象本身的操作非常昂贵,所以无论是在数字操作还是按位操作中进行操作,都无关紧要。这里我用直观的方式解释一下

<-------------- Python object operation cost --------------><- bit op ->
<-------------- Python object operation cost --------------><----- num op ----->

这只是对执行最简单的位运算或数字运算的成本的简单粗略概念。可以看到 Python 对象操作的开销占大多数,所以不管是按位还是数值,差别太小可以忽略。

如果你真的需要性能,你必须处理大量的数据,你应该考虑

  1. 在Python的C/C++模块中编写逻辑,你可以使用像Boost.Python这样的库
  2. 使用第三方库进行海量数字处理,例如numpy

【讨论】:

  • 性能肯定不是我提供的任何解决方案的问题。在我的例子中,Python 应用程序的执行看起来像这样:------------------------------------------------------------X---------------------------------,其中X 是执行一两个此类操作的部分。我想阻止答案,例如引入了一种新的数字类型,重载了 + 运算符,该运算符在内部进行模运算。
【解决方案2】:

你应该简单地扔掉最上面的部分。

#field_value = counter & (1 << n) - 1
field_value = counter & ALLOWED_BIT_WIDTH

如果这是在嵌入式设备中实现的,则使用的寄存器可能是限制因素。以我的经验,这是通常完成的方式。

协议中的“限制”是一种约束协议所需开销带宽的方式。

【讨论】:

  • 这……并没有真正解决我的问题。我必须这样做,并且协议要求我这样做是有原因的,这是前提的一部分。在这里,我正在尝试研究不同的实际操作方式。
  • 是的,我同意。我给了你我认为在嵌入式设备中常见的方式。
【解决方案3】:

它可能依赖于 python 实现,但在 CPython 2.6 中,它看起来像这样:

In [1]: counter = 0xfedcba9876543210
In [10]: %timeit counter % 2**15
1000000 loops, best of 3: 304 ns per loop

In [11]: %timeit counter % (1<<15)
1000000 loops, best of 3: 302 ns per loop

In [12]: %timeit counter & ((1<<15)-1)
10000000 loops, best of 3: 104 ns per loop

In [13]: %timeit counter & ~(1<<15)
10000000 loops, best of 3: 170 ns per loop

在这种情况下,counter &amp; ((1&lt;&lt;15)-1) 显然是赢家。有趣的是2**151&lt;&lt;15 花费相同的时间(或多或少);我猜 Python 在内部优化了这种情况,无论如何 2**15 -> 1

我曾经写过一个类,可以让你这样做:

 bc = BitSliceLong(counter)
 bc = bc[15:0]

派生自 long,但它是一种更通用的实现(允许您采用任何范围的位,而不仅仅是 x:0),并且为此产生的额外开销使其速度变慢了一个数量级,即使它使用的是相同的里面的方法。

编辑:顺便说一句,预先计算这些值似乎没有任何好处——这里的主要因素不是实际的数学运算。如果我们这样做

cx_mask = 2**15
counter % cx_mask

时间与必须计算 2**15 时相同。对于我们的“最佳情况”也是如此——预先计算 ((1

另外,在前面的例子中,我使用了一个大数字,它在 python 中实现为long。这不是真正的原生类型 - 它支持任意长度的数字,因此几乎需要处理任何东西,因此实现操作不仅仅是一个 ALU 调用 - 它涉及一系列位移和算术运算。

如果您可以将计数器保持在 sys.maxint 以下,您将改用 int 类型,它们看起来都更快,而且更受实际数学代码的支配:

In [55]: %timeit x % (1<<15)
10000000 loops, best of 3: 53.6 ns per loop

In [56]: %timeit x & ((1<<15)-1)
10000000 loops, best of 3: 49.2 ns per loop

In [57]: %timeit x % (2**15)
10000000 loops, best of 3: 53.9 ns per loop

这些都差不多,所以你在这里真正使用哪一个并不重要。 (mod 稍慢,但在随机变化范围内)。 div/mod 对非常大的数字进行昂贵的操作是有意义的,具有更复杂的算法,而对于“小”整数,它可以在硬件中完成。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-10-03
    • 1970-01-01
    • 2014-05-28
    • 1970-01-01
    • 1970-01-01
    • 2012-03-25
    • 1970-01-01
    • 2011-05-17
    相关资源
    最近更新 更多