时间复杂度单循环与多个顺序循环

时间:2017-09-12 08:13:51

标签: algorithm performance loops time-complexity asymptotic-complexity

今天,我和我的同事就一个特定的代码片段进行了一个小小的争论。代码看起来像这样。至少,这是他想象的那样。

    for(int i = 0; i < n; i++) {
        // Some operations here
    }

    for (int i = 0; i < n; i++) {
        // Some more operations here
    }

他希望我删除第二个循环,因为它会导致性能问题。

但是,我确信因为我这里没有任何嵌套循环,所以复杂性总是 O(n),无论我放了多少个连续循环(我们只有2个循环) )。

他的论点是,如果 n 为1,000,000并且循环需要5秒,我的代码将需要10秒,因为它有2个for循环。这句话后我很困惑。

我从DSA课程中记得的是,在计算Big Oh时我们会忽略这些常量。

我在这里缺少什么?

的SideNote
在实际代码中,第二个循环只会运行 m 次,而m远小于n。因此,对于非常大的 n 值,m总是非常小。

我的同事没有注意到它,因此指出了性能问题。

3 个答案:

答案 0 :(得分:2)

是,
复杂性理论可能有助于比较[?TIME][?SPACE]中的两种不同的计算方法 的

不要将[PTIME]复杂性用作效率低下的论据

事实#1: O( f(N) ) 与比较N ~ INFTY附近区域的复杂性相关,因此可以使用流程主要限制比较“那里”

事实#2:鉴于N ~ { 10k | 10M | 10G },此类案件均未达到上述条件

事实#3:如果进程(算法)允许循环合并而没有任何副作用(在资源/阻塞/等上)到单个传递中,则单循环处理可能始终受益于减少的循环开销。

微基准决定,而不是O( f( N ) )

N ~ INFTY

随着许多其他影响产生更强的影响 - 更高或更差的缓存行对齐以及可能的L1 / L2 / L3缓存重用次数,智能利用更多/更少的CPU寄存器 - 所有这些都是由可能的编译器优化,并可能进一步提高小 N -s的代码执行速度,超出上述任何期望。

所以,
在诉诸O( f( N ) )

的限制之前,请执行几个与比例相关的微基准测试

始终如此。

答案 1 :(得分:0)

在渐近符号中,您的代码具有时间复杂度O(n + n)= O(2n)=

  

O(n)的

旁注:
如果第一个循环需要n个迭代,第二个循环需要m,那么时间复杂度将为O(n + m)。

PS:我认为for循环的主体不够重,不足以影响整体复杂性,正如你所提到的那样。

答案 2 :(得分:0)

您可能会混淆时间复杂度和性能。这是两件不同(但相关)的事情。

时间复杂度处理比较算法的增长率,并忽略恒定因素和混乱的现实世界条件。这些简化使其成为推理算法可扩展性的有价值的理论框架。

性能是代码在实际计算机上运行的速度。与 Big O-land 不同,恒定因素存在并且通常在确定执行时间方面起主导作用。您的同事有理由承认这一点。很容易忘记 O(1000000n) 和 Big O-land 中的 O(n) 是一样的,但是对于实际的计算机来说,常数因子是非常真实的。

Big O 提供的鸟瞰图还是很有价值的;它可以帮助确定您的同事是否迷失在细节中并追求微观优化。

此外,您的同事认为简单的指令计数是比较这些循环安排的实际性能的一个步骤,但这仍然是一个主要的简化。考虑缓存特性;乱序执行潜力;对预取、循环展开、向量化、分支预测、寄存器分配和其他编译器优化的友好性;垃圾收集/分配开销和堆内存访问与堆栈内存访问只是导致执行时间产生巨大差异的几个因素,而不仅仅是在分析中包含简单的操作。

例如,如果您的代码类似于

for (int i = 0; i < n; i++) {
    foo(arr[i]);
}

for (int i = 0; i < m; i++) {
    bar(arr[i]);
}

n 足够大以至于 arr 不能整齐地放入缓存中(也许 arr 的元素本身就是大的、堆分配的对象),您可能会发现第二个循环由于不得不将被逐出的块重新带回到缓存中而具有显着的有害影响。重写为

for (int i = 0, end = max(n, m); i < end; i++) {
    if (i < n) {
        foo(arr[i]);
    }

    if (i < m) {
        bar(arr[i]);
    }
}

可能效率提高不成比例,因为来自 arr 的块被带入缓存一次。 if 语句似乎会增加开销,但分支预测可能会使影响忽略不计,从而避免管道刷新。

相反,如果 arr 适合缓存,则第二个循环对性能的影响可能可以忽略不计(特别是如果 m 是有界的,而且还很小)。

再说一次,foobar 中发生的事情可能是一个关键因素。这里没有足够的信息来通过查看这些片段来判断哪个可能运行得更快,虽然它们很简单,但同样适用于问题中的片段。

在某些情况下,编译器可能有足够的信息来为这两个示例生成相同的代码。

最终,解决此类争论的唯一希望是编写一个准确的基准测试(不一定是一项容易的任务),在正常工作条件下(并非总是可能)测量代码,并根据您的其他约束和指标评估结果应用可能需要(时间、预算、可维护性、客户需求、能源效率等...)。

如果应用以任何一种方式满足其目标或业务需求,那么讨论性能可能还为时过早。分析是确定讨论中的代码是否有问题的好方法。参见 Eric Lippert 的 Which is Faster?,它为(通常)不要担心这类事情提供了强有力的理由。

这是 Big O 的一个好处——如果两段代码仅相差一个小的常数因子,那么在通过分析证明值得关注之前,很有可能不值得担心。