【问题标题】:How can bisect_right() be 4x slower than insort_right()?bisect_right() 怎么会比 insort_right() 慢 4 倍?
【发布时间】:2020-04-12 12:16:56
【问题描述】:

我正在尝试优化在竞争性编程网站上超时的解决方案。我开始使用 cProfile,它似乎显示 bisect_right() 需要 4 倍于 insort_right() 的时间。这是在输入列表上运行的分析,包含超过 40k 个条目:

         89936 function calls in 3.596 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    3.596    3.596 <string>:1(<module>)
        1    0.180    0.180    3.596    3.596 leetcode.py:38(go)
        1    3.357    3.357    3.415    3.415 leetcode.py:6(numSubarrayProductLessThanK)
    44965    0.045    0.000    0.045    0.000 {built-in method _bisect.bisect_right}
    44965    0.014    0.000    0.014    0.000 {built-in method _bisect.insort_right}
        1    0.000    0.000    3.596    3.596 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.print}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

我认为所有列表插入都是 O(n),因为平均必须移动 n/2 个元素。并且简单地确定插入排序列表的位置将是 O(log n)。但在配置文件报告中,它看起来颠倒了:插入器insort_right() 比位置确定器bisect_right() 慢。我哪里错了?代码如下:

from bisect import insort, bisect

class Solution:
    def numSubarrayProductLessThanK(self, nums, k):
        if k == 0:
            return 0

        result = 0
        product = 1 
        products = [1]
        products_n = 1

        for num in nums:
            product *= num

            factor_min = product // k
            n_factors_less = products_n - bisect(products, factor_min)

            result += n_factors_less

            insort(products, product)
            products_n += 1

        return result

感谢您的关注。

【问题讨论】:

  • stackoverflow.com/questions/1110332/performance-of-list-insert 非常密切相关(其他人也对 bisect 性能感到好奇),但不是重复的。
  • ...也就是说,就您的目标是弄清楚实际性能而言,我可以看到我们进入了与您的硬件细节相关的领域——big-O 描述了如何当然,操作的数量会随着数据结构的大小而变化,但它并没有说明这些操作实际花费在挂钟时间中多长时间的常数因素。
  • 您发布的代码调用 bisectinsort,参数完全不同。看起来insort 调用可能更适合分支预测,和/或它们可能具有更好的缓存行为。 (看起来insort 总是用product 调用,最终会出现在products 列表的末尾。)

标签: python list bisect


【解决方案1】:

您的bisectinsort 调用传递完全不同的参数。

假设nums 是一个正整数列表,您的insort 调用将始终在products 列表的末尾插入新的product,这需要O(1) 而不是O(n) 时间用于插入,并且非常适合二进制搜索的分支预测。 (Python 对象比较比简单的硬件比较指令更复杂,但分支预测仍然适用于比较钩子内的逻辑,特别是因为 int 比较是用 C 实现的。)

同时,假设k 远大于nums 的典型元素,您的bisect 调用将在列表中间的某个位置找到factor_min 的位置,可能靠近但不是在大部分的末尾时间。这不太适合分支预测。

如果没有更复杂的测试,我无法确定分支预测是主导因素,但似乎很有可能。

【讨论】:

  • 我可能是盲人,但我没有看到完全不同的论点。 bisect()insort() 调用都通过 products 列表。这是对产品始终增长的一个很好的观察,因为 nums 成员是正整数,但 insort() 不知道,它仍然必须执行 log2 步骤来确定结束是正确的位置,对吗?还是在进入二进制搜索模式之前进行快速开始/结束检查?这或许能说明问题!插入是即时的,因为我不断地添加到末尾,而搜索需要时间,因为搜索的 elem 在中间。
  • 再次考虑,我看到您的意思是插入的内容和搜索的内容完全不同。谢谢你的回答,我接受了。
猜你喜欢
  • 2021-04-12
  • 1970-01-01
  • 2017-07-23
  • 2015-02-20
  • 2017-07-26
  • 1970-01-01
  • 2016-12-23
  • 2012-10-14
  • 1970-01-01
相关资源
最近更新 更多