这里的性能错误可能在哪里?

时间:2016-08-02 06:58:30

标签: c# algorithm performance linq optimization

许多测试用例都是超时的。我已经确定我在任何地方使用懒惰评估,线性(或更好)例程等等。我感到震惊的是,这仍然不符合性能基准。

table

1 个答案:

答案 0 :(得分:7)

懒惰评估不会使糟糕的算法表现更好。当这些性能问题对您产生影响时,它只会延迟。延迟评估可以帮助的是空间复杂性,即减少执行算法所需的内存量。由于数据是懒惰生成的,因此您不一定(必然)同时拥有内存中的所有数据。

然而,依靠懒惰的评估来修复你的空间(或时间)复杂性问题很容易让你陷入困境。查看以下示例代码:

var moves = Enumerable.Range(0, 5).Select(x => {
    Console.WriteLine("Generating");
    return x;
});

var aggregate = moves.Aggregate((p, c) => {
    Console.WriteLine("Aggregating");
    return p + c;
});

var newMoves = moves.Where(x => {
    Console.WriteLine("Filtering");
    return x % 2 == 0;
});

newMoves.ToList();

正如您所看到的,aggregatenewMoves都依赖于懒惰评估的moves可枚举。由于moves的原始计数为5,我们将在输出中看到4个“聚合”行,以及5个“过滤”行。但是你经常期望“生成”出现在控制台中吗?

答案是10。这是因为moves是一个生成器并且正在懒惰地进行评估。当多个地方请求它时,将为每个地方创建一个迭代器,这最终意味着生成器将多次执行(以生成独立的结果)。

这不一定是个问题,但在你的情况下,很快就会变成一个问题。假设我们通过另一轮聚合继续上面的示例代码。第二个聚合将消耗newMoves,而moves将消耗原始moves。因此,要进行汇总,我们将重新运行原始newMoves生成器和moves生成器。如果我们要添加另一级别的过滤,下一轮聚合将运行三个互锁生成器,再次重新运行原始moves生成器。

由于您的原始O(n²)生成器会创建一个二次大小的可枚举,并且实际时间复杂度moves,这是一个实际问题。在每次迭代中,您添加另一轮过滤,它将与O(n^2 + n^3 + n^4 + n^5 + …)可枚举的大小成线性关系,并且您实际上完全使用了可枚举的聚合。因此,您最终得到的n^j最终将是j 2的总和,从N-K开始到moves。这是非常糟糕的时间复杂性,而这一切只是因为你试图通过懒惰地评估这些动作来节省内存。

因此,改善这一点的第一步是避免懒惰的评估。你不断迭代O(n²)并过滤它,所以你应该把它放在内存中。是的,这意味着你有一个二次大小的数组,但实际上你不需要更多。这也限制了您的时间复杂性。是的,你仍然需要在线性时间内过滤列表(所以O(n³)因为大小是n²)并且你在循环中这样做,所以你最终会得到立方时间(Illuminate\Foundation\Bus\DispatchesJobs;)但是已经是你的上限了(在循环中迭代列表一定数量的时间只会使时间复杂度增加一个常数,这无关紧要。)

完成后,您应该考虑原始问题,考虑一下您实际在做什么。我相信如果你使用你拥有的更好的信息,你可能会进一步降低计算复杂性,并使用数据结构(例如散列集,或者已经存储了移动成本的图形)来帮助你进行过滤和聚合。因为我不知道你原来的问题,所以我无法给你一些确切的想法,但我确信你能做些什么。

最后,如果您遇到性能问题,请务必记住您的代码。分析器会告诉您代码的哪些部分是最昂贵的,因此您可以清楚地了解应该尝试优化的内容以及优化时不需要关注的内容(因为优化已经快速的部分对您没有帮助得到更快的。)