递归方法的IEnumerable比用foreach构造的相同IEnumerable慢10倍

时间:2019-01-08 16:50:30

标签: 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<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倍多的时间?

2 个答案:

答案 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