C# 递归方法生成的IEnumerable比用foreach构造的IEnumerable慢10倍

C# 递归方法生成的IEnumerable比用foreach构造的IEnumerable慢10倍,c#,.net-core,ienumerable,C#,.net Core,Ienumerable,我不明白为什么在下面的代码段中,一个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&l

我不明白为什么在下面的代码段中,一个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();
}
该代码段模拟了分布在1000组中的10000个用户,每组10个用户

每个组可以有两种类型的成员:用户(字符串)或其他组(表示该组ID的int)

每个组都有上一个组作为成员。因此,组0有10个用户,组1有10个用户和组0的用户,组2有10个用户和组1的用户。。这里开始递归

搜索的目的是确定用户“user0”(靠近列表末尾)是否是组999(通过组关系包含所有10000个用户)的成员


问题是,为什么通过foreach构造的IEnumerable进行搜索只需要3毫秒,而对于使用递归方法构造的同一IEnumerable进行搜索则需要10倍以上的时间?这是一个有趣的问题。当我在.NETFramework中编译它时,执行时间大致相同(我必须将TryAdd Dictionary方法更改为Add)

在.NETCore中,我得到了与您观察到的相同的结果

我相信答案是推迟执行。您可以在调试器中看到

IEnumerable<string> users = Group.groups[999].AggregateUsers();
IEnumerable users=Group.groups[999].AggregateUsers();
分配给用户变量将产生Concat2Iterator实例和第二个实例

users = Enumerable.Empty<string>();
foreach (Group group in Group.groups.Values.Reverse())
    users = users.Concat(group.Users);
users=Enumerable.Empty();
foreach(Group.groups.Values.Reverse()中的组)
users=users.Concat(group.users);
将导致冷凝器的损坏

从concat的文档中:

此方法通过使用延迟执行来实现。直接的 返回值是一个对象,它存储了所有需要的信息 执行操作所需的。此方法表示的查询 在通过调用其 GetEnumerator方法直接使用或使用Visual C#中的foreach或For 每一个都在visualbasic中

您可以查看concat的代码。concatnitor和Concat2Iterator的GetEnumerable实现是不同的

因此,我的猜测是,由于使用concat构建查询的方式,第一个查询的计算时间更长。如果尝试对其中一个枚举项使用ToList(),如下所示:

IEnumerable<string> users = Group.groups[999].AggregateUsers().ToList();
IEnumerable users=Group.groups[999].AggregateUsers().ToList();

您将看到,所用的时间将减少到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();
}

两者都是解决这个问题的糟糕方法。若要展平序列,请使用
SelectMany
,它是专门为解决此问题而设计的,并且效率最高。@Servy但是在代码段中调用Contains()的两个IEnumerable之间有什么区别?为什么它们在查找匹配项时有如此大的不同?Enumerable.Contains扩展方法只需在整个集合中循环,直到找到第一个匹配项。如果您正在搜索的用户位于集合的开头,则会更快地找到它。@AlexanderDerck,但这不是问题所在。第二个查询中的IEnumerable与第一个查询中的IEnumerable相反。@两个集合中的AlexanderDerck“user0”都位于索引9990处。反正我想出来了。这是因为嵌套连接。我将发布更新后的剪报,以展示差异。很好的发现,没想到内部实现会有所不同
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