我不明白为什么在下面的代码片段中,一个IEnumerable.Contains()比另一个要快,即使它们相同。
public class Group
{
public static Dictionary<int, Group> groups = new Dictionary<int, Group>();
// Members, user and groups
public List<string> Users = new List<string>();
public List<int> GroupIds = new List<int>();
public IEnumerable<string> AggregateUsers()
{
IEnumerable<string> aggregatedUsers = Users.AsEnumerable();
foreach (int id in GroupIds)
aggregatedUsers = aggregatedUsers.Concat(groups[id].AggregateUsers());
return aggregatedUsers;
}
}
static void Main(string[] args)
{
for (int i = 0; i < 1000; i++)
Group.groups.TryAdd(i, new Group());
for (int i = 0; i < 999; i++)
Group.groups[i + 1].GroupIds.Add(i);
for (int i = 0; i < 10000; i++)
Group.groups[i/10].Users.Add($"user{i}");
IEnumerable<string> users = Group.groups[999].AggregateUsers();
Stopwatch stopwatch = Stopwatch.StartNew();
bool contains1 = users.Contains("user0");
Console.WriteLine($"Search through IEnumerable from recursive function was {contains1} and took {stopwatch.ElapsedMilliseconds} ms");
users = Enumerable.Empty<string>();
foreach (Group group in Group.groups.Values.Reverse())
users = users.Concat(group.Users);
stopwatch = Stopwatch.StartNew();
bool contains2 = users.Contains("user0");
Console.WriteLine($"Search through IEnumerable from foreach was {contains2} and took {stopwatch.ElapsedMilliseconds} ms");
Console.Read();
}
这是通过执行以下代码片段获得的输出:
Search through IEnumerable from recursive function was True and took 40 ms
Search through IEnumerable from foreach was True and took 3 ms
此代码段模拟了10,000个用户,分为1,000个组,每个组10个用户。
每个组可以具有2种类型的成员,用户(字符串)或其他组(一个int表示该组的ID)。
每个组都有上一个组作为成员。因此,第0组有10个用户,第1组有10个用户和第0组的用户,第2组有10个用户和第1组的用户..然后开始递归。
搜索的目的是确定用户“ user0”(接近列表的末尾)是否是组999(通过组关系包含所有10,000个用户)的成员。
问题是,为什么用foreach构造的IEnumerable进行搜索只花3毫秒,而用递归方法构造的IEnumerable却要花10倍多的时间?
答案 0 :(得分:2)
一个有趣的问题。当我在.NET Framework中进行编译时,执行时间几乎相同(我不得不将TryAdd Dictionary方法更改为Add)。
在.NET Core中,我得到的结果与您观察到的相同。
我相信答案是推迟执行。您可以在调试器中看到
IEnumerable<string> users = Group.groups[999].AggregateUsers();
分配给用户变量将导致Concat2Iterator实例和第二个实例
users = Enumerable.Empty<string>();
foreach (Group group in Group.groups.Values.Reverse())
users = users.Concat(group.Users);
将产生ConcatNIterator。
来自concat的文档:
此方法通过使用延迟执行来实现。立即 返回值是一个存储所有信息的对象 需要执行操作。此方法表示的查询 在调用该对象枚举该对象之前不会执行 直接或通过在Visual C#或For中使用foreach的GetEnumerator方法 每个都在Visual Basic中。
您可以签出concat here的代码。 ConcatNIterator和Concat2Iterator的GetEnumerable实现不同。
所以我的猜测是,由于使用concat构建查询的方式,第一个查询需要花费更长的时间进行评估。如果您尝试对这样的枚举之一使用ToList():
IEnumerable<string> users = Group.groups[999].AggregateUsers().ToList();
您将看到经过的时间几乎减少到0毫秒。
答案 1 :(得分:0)
阅读Mikołaj的回答和Servy的评论后,我想出了解决该问题的方法。谢谢!
public class Group
{
public static Dictionary<int, Group> groups = new Dictionary<int, Group>();
// Members, user and groups
public List<string> Users = new List<string>();
public List<int> GroupIds = new List<int>();
public IEnumerable<string> AggregateUsers()
{
IEnumerable<string> aggregatedUsers = Users.AsEnumerable();
foreach (int id in GroupIds)
aggregatedUsers = aggregatedUsers.Concat(groups[id].AggregateUsers());
return aggregatedUsers;
}
public IEnumerable<string> AggregateUsers(List<IEnumerable<string>> aggregatedUsers = null)
{
bool topStack = false;
if (aggregatedUsers == null)
{
topStack = true;
aggregatedUsers = new List<IEnumerable<string>>();
}
aggregatedUsers.Add(Users.AsEnumerable());
foreach (int id in GroupIds)
groups[id].AggregateUsers(aggregatedUsers);
if (topStack)
return aggregatedUsers.SelectMany(i => i);
else
return null;
}
}
static void Main(string[] args)
{
for (int i = 0; i < 1000; i++)
Group.groups.TryAdd(i, new Group());
for (int i = 0; i < 999; i++)
Group.groups[i + 1].GroupIds.Add(i);
for (int i = 0; i < 10000; i++)
Group.groups[i / 10].Users.Add($"user{i}");
Stopwatch stopwatch = Stopwatch.StartNew();
IEnumerable<string> users = Group.groups[999].AggregateUsers();
Console.WriteLine($"Aggregation via nested concatenation took {stopwatch.ElapsedMilliseconds} ms");
stopwatch = Stopwatch.StartNew();
bool contains = users.Contains("user0");
Console.WriteLine($"Search through IEnumerable from nested concatenation was {contains} and took {stopwatch.ElapsedMilliseconds} ms");
stopwatch = Stopwatch.StartNew();
users = Group.groups[999].AggregateUsers(null);
Console.WriteLine($"Aggregation via SelectMany took {stopwatch.ElapsedMilliseconds} ms");
stopwatch = Stopwatch.StartNew();
contains = users.Contains("user0");
Console.WriteLine($"Search through IEnumerable from SelectMany was {contains} and took {stopwatch.ElapsedMilliseconds} ms");
stopwatch = Stopwatch.StartNew();
users = Enumerable.Empty<string>();
foreach (Group group in Group.groups.Values.Reverse())
users = users.Concat(group.Users);
Console.WriteLine($"Aggregation via flat concatenation took {stopwatch.ElapsedMilliseconds} ms");
stopwatch = Stopwatch.StartNew();
contains = users.Contains("user0");
Console.WriteLine($"Search through IEnumerable from flat concatenation was {contains} and took {stopwatch.ElapsedMilliseconds} ms");
Console.Read();
}
以下是结果:
Aggregation via nested concatenation took 0 ms
Search through IEnumerable from nested concatenation was True and took 43 ms
Aggregation via SelectMany took 1 ms
Search through IEnumerable from SelectMany was True and took 0 ms
Aggregation via foreach concatenation took 0 ms
Search through IEnumerable from foreach concatenation was True and took 2 ms