非常简短的问题。我有一个随机排序的大字符串数组(100K +条目),我想找到所需字符串的第一次出现。我有两个解决方案。
从阅读完我可以猜到的是'for循环'目前会提供略微更好的性能(但这个余量总是会改变),但我也发现linq版本更具可读性。总的来说,哪种方法通常被认为是目前最好的编码实践?为什么?
string matchString = "dsf897sdf78";
int matchIndex = -1;
for(int i=0; i<array.length; i++)
{
if(array[i]==matchString)
{
matchIndex = i;
break;
}
}
或
int matchIndex = array.Select((r, i) => new { value = r, index = i })
.Where(t => t.value == matchString)
.Select(s => s.index).First();
答案 0 :(得分:47)
最佳做法取决于您的需求:
LINQ确实会在所有间接情况下减慢速度。不要担心,因为99%的代码都不会影响最终用户的性能。
我从C ++开始,真正学会了如何优化一段代码。 LINQ不适合充分利用CPU。因此,如果您测量LINQ查询是一个问题,那就放弃它。但只有这样。
对于您的代码示例,我估计会减少3倍的速度。分配(以及随后的GC!)和通过lambdas的间接确实受到了伤害。
答案 1 :(得分:28)
稍微更好的表现?一个循环会给出更好的性能!
请考虑以下代码。在我的系统上进行RELEASE(而不是调试)构建,它给出了:
Found via loop at index 999999 in 00:00:00.2782047
Found via linq at index 999999 in 00:00:02.5864703
Loop was 9.29700432810805 times faster than linq.
故意设置代码,以便找到的项目在最后。如果一开始就是对的,情况会有很大不同。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Demo
{
public static class Program
{
private static void Main(string[] args)
{
string[] a = new string[1000000];
for (int i = 0; i < a.Length; ++i)
{
a[i] = "Won't be found";
}
string matchString = "Will be found";
a[a.Length - 1] = "Will be found";
const int COUNT = 100;
var sw = Stopwatch.StartNew();
int matchIndex = -1;
for (int outer = 0; outer < COUNT; ++outer)
{
for (int i = 0; i < a.Length; i++)
{
if (a[i] == matchString)
{
matchIndex = i;
break;
}
}
}
sw.Stop();
Console.WriteLine("Found via loop at index " + matchIndex + " in " + sw.Elapsed);
double loopTime = sw.Elapsed.TotalSeconds;
sw.Restart();
for (int outer = 0; outer < COUNT; ++outer)
{
matchIndex = a.Select((r, i) => new { value = r, index = i })
.Where(t => t.value == matchString)
.Select(s => s.index).First();
}
sw.Stop();
Console.WriteLine("Found via linq at index " + matchIndex + " in " + sw.Elapsed);
double linqTime = sw.Elapsed.TotalSeconds;
Console.WriteLine("Loop was {0} times faster than linq.", linqTime/loopTime);
}
}
}
答案 2 :(得分:4)
LINQ,根据声明范式,表达了计算的逻辑而没有描述其控制流程。该查询是面向目标的,自我描述的,因此易于分析和理解。也很简洁。而且,使用LINQ,很大程度上取决于数据结构的抽象。这涉及高维护率和可重用性。
迭代aproach解决了命令式范式。它提供细粒度控制,从而轻松获得更高的性能。代码也更容易调试。有时,构造良好的迭代比查询更具可读性。
答案 3 :(得分:2)
嗯,你自己给出了问题的答案。
如果您想获得最佳效果,请使用For
循环;如果您想要可读性,请使用Linq
。
也许还要记住使用Parallel.Foreach()的可能性,它将受益于内联lambda表达式(因此,更接近Linq),并且比“手动”进行并行化更具可读性。
答案 4 :(得分:2)
性能和可维护性之间总是存在两难。通常(如果没有关于性能的具体要求)可维护性应该获胜。只有当您遇到性能问题时,才应该对应用程序进行概要分析,找到问题源并提高性能(同时降低可维护性,是的,这就是我们生活的世界)。
关于您的样本。 Linq在这里不是很好的解决方案,因为它不会在代码中添加匹配可维护性。实际上,对于我来说,投影,过滤和再次投影看起来比简单的循环更糟糕。你需要的是简单的Array.IndexOf,它比loop更易于维护,并且具有几乎相同的性能:
Array.IndexOf(array, matchString)
答案 5 :(得分:1)
我不认为任何一种人更喜欢看LINQ,有些人不喜欢。
如果性能是一个问题,我会为您的场景分析两段代码,如果差异可以忽略不计,那么请选择您认为更符合的代码,毕竟很可能是维护代码的人。< / p>
您是否考虑过使用PLINQ或使循环并行运行?
答案 6 :(得分:0)
最好的选择是使用数组类的IndexOf方法。由于它专门用于数组,因此它将比Linq和For Loop都快得多。 改进Matt Watsons答案。
using System;
using System.Diagnostics;
using System.Linq;
namespace PerformanceConsoleApp
{
public class LinqVsFor
{
private static void Main(string[] args)
{
string[] a = new string[1000000];
for (int i = 0; i < a.Length; ++i)
{
a[i] = "Won't be found";
}
string matchString = "Will be found";
a[a.Length - 1] = "Will be found";
const int COUNT = 100;
var sw = Stopwatch.StartNew();
Loop(a, matchString, COUNT, sw);
First(a, matchString, COUNT, sw);
Where(a, matchString, COUNT, sw);
IndexOf(a, sw, matchString, COUNT);
Console.ReadLine();
}
private static void Loop(string[] a, string matchString, int COUNT, Stopwatch sw)
{
int matchIndex = -1;
for (int outer = 0; outer < COUNT; ++outer)
{
for (int i = 0; i < a.Length; i++)
{
if (a[i] == matchString)
{
matchIndex = i;
break;
}
}
}
sw.Stop();
Console.WriteLine("Found via loop at index " + matchIndex + " in " + sw.Elapsed);
}
private static void IndexOf(string[] a, Stopwatch sw, string matchString, int COUNT)
{
int matchIndex = -1;
sw.Restart();
for (int outer = 0; outer < COUNT; ++outer)
{
matchIndex = Array.IndexOf(a, matchString);
}
sw.Stop();
Console.WriteLine("Found via IndexOf at index " + matchIndex + " in " + sw.Elapsed);
}
private static void First(string[] a, string matchString, int COUNT, Stopwatch sw)
{
sw.Restart();
string str = "";
for (int outer = 0; outer < COUNT; ++outer)
{
str = a.First(t => t == matchString);
}
sw.Stop();
Console.WriteLine("Found via linq First at index " + Array.IndexOf(a, str) + " in " + sw.Elapsed);
}
private static void Where(string[] a, string matchString, int COUNT, Stopwatch sw)
{
sw.Restart();
string str = "";
for (int outer = 0; outer < COUNT; ++outer)
{
str = a.Where(t => t == matchString).First();
}
sw.Stop();
Console.WriteLine("Found via linq Where at index " + Array.IndexOf(a, str) + " in " + sw.Elapsed);
}
}
}
输出:
Found via loop at index 999999 in 00:00:01.1528531
Found via linq First at index 999999 in 00:00:02.0876573
Found via linq Where at index 999999 in 00:00:01.3313111
Found via IndexOf at index 999999 in 00:00:00.7244812
答案 7 :(得分:0)
有点无答案,实际上只是对https://stackoverflow.com/a/14894589的扩展,但是一段时间以来,我一直在研究API兼容的Linq-to-Objects替代品。它仍然不能提供手动编码循环的性能,但是对于许多(大多数?)linq方案来说,它都更快。它确实会产生更多的垃圾,并且前期成本会稍微增加一些。
该代码可用https://github.com/manofstick/Cistern.Linq
有可用的nuget软件包https://www.nuget.org/packages/Cistern.Linq/(我不能说这是经过战斗加固的,后果自负)
从Matthew Watson的答案(https://stackoverflow.com/a/14894589)中进行一些细微的调整,得到的代码比手工编码的循环差了3.5倍。在我的计算机上,它花费的时间约为原始System.Linq版本的1/3。
要替换的两个更改:
using System.Linq;
...
matchIndex = a.Select((r, i) => new { value = r, index = i })
.Where(t => t.value == matchString)
.Select(s => s.index).First();
具有以下内容:
// a complete replacement for System.Linq
using Cistern.Linq;
...
// use a value tuple rather than anonymous type
matchIndex = a.Select((r, i) => (value: r, index: i))
.Where(t => t.value == matchString)
.Select(s => s.index).First();
因此,图书馆本身正在开发中。它在corefx的System.Linq测试套件中失败了几个极端情况。它还需要转换一些功能(它们目前具有corefx System.Linq实现,从性能角度来看,从API角度来看是兼容的)。但是如果您想提供帮助,发表评论等等,将不胜感激。...
答案 8 :(得分:0)
一个有趣的发现。 LINQ Lambda查询肯定会增加LINQ Where查询或For循环的代价。在下面的代码中,它将使用1000001个多参数对象填充一个列表,然后使用LINQ Lamba,LINQ Where Query和For Loop搜索在该测试中始终是最后一个的特定项目。每次测试都会迭代100次,然后取平均时间以获得结果。
LINQ Lambda查询平均时间:0.3382秒
LINQ查询平均时间:0.238秒
循环平均时间:0.2266秒
我已经一遍又一遍地运行了该测试,甚至增加了迭代次数,并且从统计上讲,传播范围几乎相同。当然,对于一百万个项目的搜索,我们正在说的是1/10秒。因此,在现实世界中,除非有那么密集的内容,否则甚至不确定您会注意到什么。但是,如果执行LINQ Lambda与LINQ,则查询的性能确实有所不同。 LINQ Where与For循环几乎相同。
private void RunTest()
{
try
{
List<TestObject> mylist = new List<TestObject>();
for (int i = 0; i <= 1000000; i++)
{
TestObject testO = new TestObject(string.Format("Item{0}", i), 1, Guid.NewGuid().ToString());
mylist.Add(testO);
}
mylist.Add(new TestObject("test", "29863", Guid.NewGuid().ToString()));
string searchtext = "test";
int iterations = 100;
// Linq Lambda Test
List<int> list1 = new List<int>();
for (int i = 1; i <= iterations; i++)
{
DateTime starttime = DateTime.Now;
TestObject t = mylist.FirstOrDefault(q => q.Name == searchtext);
int diff = (DateTime.Now - starttime).Milliseconds;
list1.Add(diff);
}
// Linq Where Test
List<int> list2 = new List<int>();
for (int i = 1; i <= iterations; i++)
{
DateTime starttime = DateTime.Now;
TestObject t = (from testO in mylist
where testO.Name == searchtext
select testO).FirstOrDefault();
int diff = (DateTime.Now - starttime).Milliseconds;
list2.Add(diff);
}
// For Loop Test
List<int> list3 = new List<int>();
for (int i = 1; i <= iterations; i++)
{
DateTime starttime = DateTime.Now;
foreach (TestObject testO in mylist)
{
if (testO.Name == searchtext)
{
TestObject t = testO;
break;
}
}
int diff = (DateTime.Now - starttime).Milliseconds;
list3.Add(diff);
}
float diff1 = list1.Average();
Debug.WriteLine(string.Format("LINQ Lambda Query Average Time: {0} seconds", diff1 / (double)100));
float diff2 = list2.Average();
Debug.WriteLine(string.Format("LINQ Where Query Average Time: {0} seconds", diff2 / (double)100));
float diff3 = list3.Average();
Debug.WriteLine(string.Format("For Loop Average Time: {0} seconds", diff3 / (double)100));
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
private class TestObject
{
public TestObject(string _name, string _value, string _guid)
{
Name = _name;
Value = _value;
GUID = _guid;
}
public string Name;
public string Value;
public string GUID;
}