衡量一段代码的运行时复杂性的最佳实践

时间:2014-05-29 14:29:59

标签: time-complexity

我有一段代码,我想测量它的时间效率。由于从代码本身估计这种复杂性很难,我想把它放在循环中并对结果计时。一旦收集到足够的数据点(大小 - >时间),我就可以看到哪条曲线最适合。

使用给定大小的随机输入数据多次重复操作可以消除由于OS决定在不良时刻进行多任务而导致的波动,从而产生更精确的时间。增加问题的大小可以提供更多的点,理想情况下间距很大。

我的测试代码工作正常(初始,非定时预热循环以减少加载时间;然后,从10的大小开始,以10%的增量扩展到1000000,重复运行直到5s已经过去或5次完整运行完成)。但是,我通过猜测来得出这些数字。

是否已接受,"科学"如何扩展重复和问题大小以实现更快,更准确的时间尺寸图?是否存在可以支撑所有无聊位的代码(或库),我应该知道之前的代码滚动我,自己的?特别是,我可以认为,当发现时间上的颠簸时,可以采取更多措施 - 虽然可以简单地考虑相对平稳的读数"足够好"。

修改

我知道计算大O复杂度的经典方法。它适用于具有良好代表性操作的自包含算法(例如,"比较"或"交换")。当不满足这些条件时,它不像宣传的那样工作(例如:LLVM的编译时C ++模板实例化成本,这是一个庞大而复杂的,我不知道相关的代表操作是什么)。这就是为什么我把它当作一个黑盒子,并试图从外面测量时间而不是代码检查。

5 个答案:

答案 0 :(得分:5)

测量时间复杂度可能非常困难(如果可能的话),我从未在算法论文中看到过这种情况。如果您无法根据(伪)代码或算法描述计算时间复杂度,那么也许您可以使用启发式来简化分析。

也许您还可以计算算法某些部分的复杂性,如果它们的复杂性明显小得多,则忽略其他部分。

如果没有任何帮助,通常的方法是显示算法如何在机器上缩放,就像你写的那样。 但是有许多因素会影响结果。只是注意其中一些:

  • 内存类型:如果您的输入足够小以适应L1缓存,您的算法运行速度非常快,因为内存很快。如果您的输入变大,那么它不再适合L1缓存,它存储在L2缓存中,如果它变得更大,它就会存储在RAM中。每次你的程序减慢一个巨大的因素(除了增加输入的因素)。最糟糕的是,当它变得如此之大以至于算法必须在你的硬盘上存储一些薄输入。
  • 多任务处理:如果您的操作系统决定将CPU移交给其他程序,您的算法似乎会变慢。这也很难处理。
  • 硬件:在big-O中,每个操作都计为1个单位时间。如果您的算法执行了大量操作,那么您的CPU已经过优化,这也会影响您的测量。
  • 软件:软件可以像硬件一样影响您的测量。例如。如果你有很多使用库的大整数操作,你可以使用GMP大规模加速程序。
  • 预热:如果开始测量,则必须先预热CPU。首先在更大的输入上运行算法(不测量)。
  • 输入案例:您只能在某些特定长度的选定或随机生成的输入案例上运行您的程序。在大多数情况下,很难说(或者只是不可能)输入是否会导致更短或更长的运行时间。所以也许你测试错误的例子。如果您使用随机输入,您会得到更多不同的结果。

总而言之:我认为你只能得到一个想法,你的算法如何扩展,但你无法通过测量运行时来精确地获得复杂性的上限。也许这适用于非常小的例子,但对于较大的例子,你将得不到正确的结果。

你能做的最好的事情是:

  • 记下用于测量的计算机的确切硬件和软件。
  • 多次重复测试(按不同顺序)
  • 如果你改变硬件或软件,你应该从头开始。
  • 仅使用全部存储在同一内存类型中的输入,因此请跳过适合缓存的所有情况。

通过这种方式,您可以查看更改是否改进了算法,其他人可以验证您的结果。

关于输入:

  • 如果可能,您应该使用最坏情况输入。如果你不能说输入是否是最坏的情况,你应该使用许多不同的情况或随机输入(如果可能的话)。
  • 您必须运行测试(针对每个输入长度),直到运行时间的平均值稳定。

答案 1 :(得分:2)

我不知道有任何软件或之前完成的工作。而且,从根本上说,我认为你不能得到值得信赖的“O(无论如何)”形式的答案。您的测量结果很嘈杂,您可能正在尝试将n log(n)操作与n sqrt(n)操作区分开来,并且与一个干净的数学分析不同,所有丢弃的常量仍然在混乱中和你在一起。

那就是说,如果我想得出最好的估计,我会经历这个过程:

  1. 确保我在整个过程中记录尽可能多的信息,我会在无聊之前运行我想要测量的尽可能多的输入(和大小)。可能一夜之间。对每个输入和大小重复测量。
  2. 将输入大小转换为时间数据到Eureqa的试用版,然后查看弹出的内容。
  3. 如果我不满意,请获取更多数据,继续将其铲入Eureqa,看看情况是否有所改善。
  4. 假设Eureqa在我厌倦消耗所有CPU时间和功率之前没有给出我喜欢的答案,我会切换到贝叶斯方法。
  5. 使用像pymc这样的东西,我试图使用一堆看起来很复杂的函数来建模数据。 (n,n ^ 2,n ^ 3,n ^ 3,n * log(n),n ^ 2 * log(n)n ^ 2 * log(n)^ 2等,等等。)
  6. 比较每个模型的DIC(越小越好),寻找最好的几个。
  7. 绘制最好的几个,寻找数据和模型不一致的地方。
  8. 在分歧附近收集更多数据。重新计算模型。
  9. 重复5-8直到无聊。
  10. 最后,收集更大输入尺寸的新数据点,查看哪些模型最能预测这些数据点。
  11. 选择相信其中一个是真的。

答案 2 :(得分:2)

首先,我不知道一种可接受的,“科学的”扩展重复和问题大小的方法,以实现更快,更准确的时间尺寸图,所以我不能就此事发表任何意见。

除此之外,为了更好地测量时间复杂度,我建议测量固定大小的平均执行时间,并将其与上一周期中测量的平均执行时间进行比较。之后,增加输入数据的大小并重复测量。

这类似于Numerical Analysisestimate errors of numerical methods中使用的方法之一。您只需调整它来估算算法实现执行时间的平均误差。

所以,简而言之:

  1. 选择输入数据的大小。
  2. 运行算法的实现。
  3. 计算平均执行时间。
  4. 如果您之前计算过平均执行时间,请将其与当前执行时间进行比较,否则从第2点开始重复。
  5. 如果差异大于或等于某个(先前定义的)阈值数量,则从第2点开始重复。
  6. 增加输入数据的大小。
  7. 从第2点开始重复。
  8. 如果不清楚,请告诉我。

答案 3 :(得分:1)

假设您在循环中运行以下内容。在迭代 i = 0,1,2,.... ,对于某些固定的 n_0> 0 和非常大的 n ,您在 1的范围内 2 i + n_0 等距离(直到舍入)点对函数进行采样, ...,n 。然后,您可以执行以下任一操作或两者的组合:

  1. 使用偶数点训练样条曲线并在奇数点上进行测试(反之亦然)。如果 l2 错误低于某个阈值,则确定迭代就足够了。

  2. 使用所有点训练样条线,并在值上测试,例如 2n 。再次,如果 l2 错误低于某个阈值,则决定迭代是否足够。

  3. 第1点强调插值误差,第2点强调外推误差。实际上,我认为您最多能够识别样条函数描述的函数。

    根据您使用的拟合方法,您可能需要为样条方法拟合一些元参数。在这种情况下,您可能需要在每次迭代时使用多个 ~2 i 样本,因为您可能需要使用其中一些样本进行参数调整交叉验证。

答案 4 :(得分:0)

如果您想要获得复杂性的黑盒估计,请使用“比率法”。例如:如果您坐在紧密循环中执行固定长度的作业,例如将随机记录插入数据库,则会在每次迭代结束时记录时间戳。随着更多数据进入,时间戳将开始变得更远。因此,然后绘制连续时间戳之间的时间差。

如果你将该图除以lg [n]并继续上升,那么它比lg [n]更糟糕。尝试除以:lg [n],n,n lg [n],n n等。当您除以估计值过高的函数时,则曲线趋势为零。当你除以一个太低的函数时,情节将继续攀升。如果你有一个很好的估计,那么你的数据集中有一个点可以放置图形四处漂移的上限和下限,以便你可以检查。