【问题标题】:Why are bitwise operators slower than multiplication/division/modulo?为什么按位运算符比乘法/除法/模运算慢?
【发布时间】:2019-05-31 12:10:18
【问题描述】:

众所周知,乘法、整数除法和以 2 的幂为模可以更有效地重写为按位运算:

>>> x = randint(50000, 100000)
>>> x << 2 == x * 4
True
>>> x >> 2 == x // 4
True
>>> x & 3 == x % 4
True

在 C/C++ 和 Java 等编译语言中,测试表明按位运算通常比算术运算快。 (见here 和here)。但是,当我在 Python 中测试这些时,我得到了相反的结果:

In [1]: from random import randint
   ...: nums = [randint(0, 1000000) for _ in range(100000)]

In [2]: %timeit [i * 8 for i in nums]
7.73 ms ± 397 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [3]: %timeit [i << 3 for i in nums]
8.22 ms ± 368 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [4]: %timeit [i // 8 for i in nums]
7.05 ms ± 393 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [5]: %timeit [i >> 3 for i in nums]
7.55 ms ± 367 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [6]: %timeit [i % 8 for i in nums]
5.96 ms ± 503 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [7]: %timeit [i & 7 for i in nums]
8.29 ms ± 816 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

如您所见,按位运算比对应的算术运算要慢,尤其是模运算。我对另一组数字重复了这个测试,得到了相同的结果。是否有一个原因?如果重要的话,这些测试在 CPython 3.6.7 中。

【问题讨论】:

  • 好吧,对于初学者来说,Python int 对象与 C int 完全不同,它们是对象。底层表示甚至不是 C int,它是一个数字数组,如果你了解 C,请自己看看:github.com/python/cpython/blob/master/Include/longintrepr.h 这是因为 Python int 对象是任意大小的 i>,不是固定大小。
  • 除以 2 的幂并取模不是一个很好的测试;任何体面的 C 编译器都会将其编译为转变。 (或者对于已签名的i,移位 + 东西以获得可能为负的i 的舍入语义)。我不希望 CPython 会,但如果你想将它与 C 进行比较,请使用运行时变量除数。 (否则i / 12345 仍然可以编译为快速乘法/移位。Why does GCC use multiplication by a strange number in implementing integer division?。现代 x86 CPU 具有非常高性能的乘法,大约与 3 次移位一样快。)
  • @PeterCordes 我知道编译器会进行这样的优化,所以我使用dis 模块来检查i % &lt;power of 2&gt; 没有进行位移,据我所知,它不是吨。但是,也许优化进一步下降,所以dis 模块无法显示它......我不确定。
  • 我对 Python 内部结构了解不多,但这种优化只有在可以对常数进行一次优化的情况下才可能/非常好,而不是每次都通过循环。检查 x &amp; (x-1) == 0 以检查 2 的幂,然后使用 POSIX ffs() 或 x86 bsf / tzcnt 之类的位扫描来获得移位计数可能不值得,除非实现期望 2 的幂很常见。 (然后仅用于除法,而不是乘法,除非输入具有多个肢体)。

标签: python optimization bitwise-operators micro-optimization


【解决方案1】:

我测试大数,位运算符更快。

python -m timeit '[i for i in range(10**64, 10**64+1000) if i & 0b10==0]'
1000 loops, best of 3: 238 usec per loop

python -m timeit '[i for i in range(10**64, 10**64+1000) if i % 2==0]'
1000 loops, best of 3: 303 usec per loop

【讨论】:

    【解决方案2】:

    *、% 和 / 都有用于单“肢体”整数的快速路径。 &lt;&lt;、&gt;&gt; 和 &amp; 不要。他们正在通过通用的任意精度代码路径。

    【讨论】:

    • 所以你说的这条“快速路径”是在代码的解释中出现的,对吗?
    • 在算子实现中。
    • 谢谢,实现的源代码很有帮助。还有一件事,你能详细说明什么是“单肢”整数吗?
    • 四肢是数字,但更大。看起来 CPython 源代码没有使用该术语;我从阅读有关 GMP 的文章中得到了这个词。 CPython 源代码只使用“数字”。
    • @Tomothy32:“肢体”是以 2^30 为底的“数字”,以二进制整数形式存储在 uint32_t 或等效项中。 CPython 将大整数存储在该大小的块中。就像您可以一次在纸上添加一个数字一样,使用 base-2^30 数字允许 CPython 完成 CPU 执行的每个原始添加操作的大量工作。 (使用基本 2^32 块将允许在 asm 中使用 add / add-with-carry 以利用大多数架构上对大整数的硬件支持,但这不是 CPython 所做的。不过,2^30 有一些优势,尤其是. 对于没有 asm 的纯 C。)
    猜你喜欢
    • 1970-01-01
    • 2016-11-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多