【问题标题】:Fastest way for Grouping and Selecting list of integers (top occurrences)分组和选择整数列表的最快方法(出现次数最多)
【发布时间】:2014-06-17 15:37:18
【问题描述】:

我有一个从 100 万到甚至 1 亿个整数的大型整数列表。

我想按出现次数对它们进行排名,并选择最热门的K(此处为K=10)结果。

我已经尝试了 4 种不同的方法,其中我的 Method1 是最快的。并行化并没有超过我自己在Method1 中的分组代码,并且由于线程竞争条件导致排名不准确。

Method1Method4 的结果准确无误,而 Method2Method3 可能由于比赛条件而排名不准确。

现在,我正在寻找比 Method1 更快的任何可能的代码,或者对并行化方法的修复,使其准确,然后比 Method1 更快。

class Benchmark
{
    static List<int> input = new List<int>();
    static void Main(string[] args)
    {
        int count = int.Parse(args[0]);

        Random rnd = new Random();

        for (int i = 0; i < count; i++)
            input.Add(rnd.Next(1, count));

        DoBench();

        Console.ReadKey();
    }

    private static void DoBench()
    {
        for (int i = 1; i <= 4; i++)
        {
            DateTime start = DateTime.Now;

            List<KeyValuePair<int, int>> results = null;

            switch (i)
            {
                case 1:
                    results = Method1();
                    break;
                case 2:
                    results = Method2();
                    break;
                case 3:
                    results = Method3();
                    break;
                case 4:
                    results = Method4();
                    break;
            }

            int resultsCount = 10;

            var topResults = results.Take(resultsCount).OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToArray();

            for (int j = 0; j < resultsCount; j++)
                Console.WriteLine("No {0,2}: {1,8}, Score {2,4}", j + 1, topResults[j].Key, topResults[j].Value);

            Console.WriteLine("Time of Method{0}: {1} ms", i, (long)DateTime.Now.Subtract(start).TotalMilliseconds);
            Console.WriteLine();
        }
    }

    private static List<KeyValuePair<int, int>> Method1()
    {
        Dictionary<int, int> dic = new Dictionary<int, int>();

        for (int i = 0; i < input.Count; i++)
        {
            int number = input[i];

            if (dic.ContainsKey(number))
                dic[number]++;
            else
                dic.Add(number, 1);
        }

        var sorted_results = dic.OrderByDescending(x => x.Value).ToList();

        return sorted_results;
    }

    private static List<KeyValuePair<int, int>> Method2()
    {
        var sorted_results = input.AsParallel().GroupBy(x => x)
               .Select(g => new KeyValuePair<int, int>(g.Key, g.Count()))
               .OrderByDescending(x => x.Value).ToList();

        return sorted_results;
    }

    private static List<KeyValuePair<int, int>> Method3()
    {
        ConcurrentDictionary<int, int> dic = new ConcurrentDictionary<int, int>();

        input.AsParallel<int>().ForAll((number) =>
        {
            dic.AddOrUpdate(number, 1, new Func<int, int, int>((key, oldValue) => oldValue + 1));
        });

        var sorted_results = dic.OrderByDescending(x => x.Value).ToList();

        return sorted_results;
    }

    private static List<KeyValuePair<int, int>> Method4()
    {
        var sorted_results = input.GroupBy(x => x)
               .Select(g => new KeyValuePair<int, int>(g.Key, g.Count()))
               .OrderByDescending(x => x.Value).ToList();

        return sorted_results;
    }
}

【问题讨论】:

  • 所以?你的Method1 对你来说慢吗?您是否正在寻找 2/3 的修复程序?您是否有目标要如何改进直方图+排序方法? (我认为您现在无法获得像 1 中那样紧凑和可读的任何东西,.Net 中没有可以帮助排序的堆,并且将源拆分为范围以修复并行版本将需要更多的行和可能不会加快速度)
  • @AlexeiLevenkov,对不起,我忘了说我的目标。是的,我的 Method1 对于我的场景来说很慢,因为它将被执行数千次。以及使并行版本更快的任何可能的修复
  • 我会尝试 map-reduce... 按 CPU 数量(即 1-999,1000-1999)将数组拆分为连续范围,计算单独的直方图(无锁定),然后合并生成的直方图在单线程中。比排序(或找到 heap 实现以获得 Top(N) 结果)...请注意,只有当您的数据集相对较小(如您的情况)并且访问数据没有额外成本时才有意义(例如它已经在内存中,没有磁盘/网络 IO)。
  • 如果您使用没有 AsParallel() 的方法 2,是否有积极的区别?在我看来,AsParallel 只是造成了开销。
  • @CSharpie,你是对的。我正在寻找一个准确的并行化版本,但在实践中还没有工作......

标签: c# performance list


【解决方案1】:

我为分组部分找到了一个更快的字典实现。我使用了 Adam Horvath (link) 编写的 MapReduce.NET 项目中的 FastDictionary 类。

使用FastDictionary 我更新了我的Method1 并写了一个更快的新Method6。然后我添加了来自@Mattew Watson 的快速排序并写了Method7。最后,我把它们全部放在了板凳上。

编辑

我刚刚想出了一个绝妙的主意,可以在Method8 中进一步加快分组部分的速度。这个想法是使用一个具有最大输入数组大小的预定义数组,这样我就可以用数组替换字典。当然,当Maximum-Element-of-Input/Size-Of-Input 的比率接近1Maximum-Element-of-Input 的整数个数适合内存时,Method8 是有效的。

编辑 2

我写了另外 3 个方法,分别命名为 Method9Method10Method11

我注意到收集分组结果并将它们添加到Method8 中的列表需要大量不必要的内存,因此我稍微修改了partialSort 并消除了内存开销。

再次,我注意到我在 Int32 的 4 个字节中持有相对较少的金额。为什么?!所以假设输入列表中没有元素出现超过65535 (short.MaxValue),我只是将Int32 数组替换为占用一半内存的short 数组。

再一次,我注意到如果我们有一个内存关键场景,并再次假设输入数组中的大多数元素出现的次数少于 255 (byte.MaxValue),我们可以使用 byte 和普通字典来存储具有发生率高。由于在分组操作期间进行了多次检查,这种技术有点慢,并且还需要在最后合并结果,但是正如您在图表中看到的那样,它的内存使用量比其他方法要少得多,甚至比我基于字典的初始方法还要少。

时间安排:

内存使用情况:

时间对比

内存比较

方法六:

    private static List<KeyValuePair<int, int>> Method6()
    {            
        FastDictionary<int, int> dic = new FastDictionary<int, int>();

        for (int i = 0; i < input.Count; i++)
        {
            int number = input[i];

            int pos = dic.InitOrGetPosition(number);
            int curr = dic.GetAtPosition(pos);
            dic.StoreAtPosition(pos, ++curr);
        }

        var sorted_results = dic.OrderByDescending(x => x.Value).ToList();

        return sorted_results;
    }

方法七:

    private static List<KeyValuePair<int, int>> Method7()
    {            
        FastDictionary<int, int> dic = new FastDictionary<int, int>();

        for (int i = 0; i < input.Count; i++)
        {
            int number = input[i];

            int pos = dic.InitOrGetPosition(number);
            int curr = dic.GetAtPosition(pos);
            dic.StoreAtPosition(pos, ++curr);
        }

        var result = dic.ToList();
        partialSort(result, 10);

        return result;
    }

方法8

    private static List<KeyValuePair<int, int>> Method8()
    {
        int[] dic = new int[input.Max() + 1];

        for (int i = 0; i < input.Count; i++)
        {
            dic[input[i]]++;
        }

        List<KeyValuePair<int, int>> list = new List<KeyValuePair<int, int>>();
        for (int i = 0; i < dic.Length; i++)
        {
            if (dic[i] > 0)
                list.Add(new KeyValuePair<int, int>(i, dic[i]));
        }            

        partialSort(list, 10);

        return list;
    }

方法9

    private static List<KeyValuePair<int, int>> Method9()
    {
        int[] dic = new int[input.Max() + 1];

        for (int i = 0; i < input.Count; i++)
        {
            dic[input[i]]++;
        }

        List<KeyValuePair<int, int>> list = partialSort(dic, 10);

        return list;
    }

方法10

    private static List<KeyValuePair<int, int>> Method10()
    {
        short[] dic = new short[input.Max() + 1];

        for (int i = 0; i < input.Count; i++)
        {
            dic[input[i]]++;
        }

        List<KeyValuePair<int, int>> list = partialSort(dic, 10);

        return list;
    }

方法11

    private static List<KeyValuePair<int, int>> Method11()
    {
        byte[] dic = new byte[input.Max() + 1];

        Dictionary<int, int> largeDic = new Dictionary<int, int>();

        int index = 0;
        int val = 0;

        for (int i = 0; i < input.Count; i++)
        {
            index = input[i];
            val = dic[index];

            if (val < 255)
                dic[index] = (byte)(val + 1); // casting to byte
            else
            {
                if (largeDic.ContainsKey(index))
                {
                    largeDic[index]++;
                }
                else
                {
                    largeDic.Add(index, 255 + 1);
                }
            }
        }

        List<KeyValuePair<int, int>> list = new List<KeyValuePair<int, int>>();

        if (largeDic.Count == 0)
        {
            list = partialSort(dic, 10);
        }
        else
        {
            if (largeDic.Count < 10)
            {
                list.AddRange(largeDic.OrderByDescending(x => x.Value));

                var tempList = partialSort(dic, 50);

                list.AddRange(tempList.Except(list, new KeyValueComparer()).Take(10 - list.Count));
            }
            else
            {
                list = largeDic.OrderByDescending(x => x.Value).Take(10).ToList();
            }
        }

        return list;
    }

部分排序

不同的重载只有不同的参数类型(int[]short[]byte[]

    private static List<KeyValuePair<int, int>> partialSort(int[] list, int k)
    {
        int[] topIndexes = new int[k];

        for (int i = 0; i < k; ++i)
        {
            int maxIndex = i;
            int maxValue = list[i];

            for (int j = i + 1; j < list.Length; ++j)
            {
                if (list[j] > maxValue)
                {
                    maxIndex = j;
                    maxValue = list[j];
                }
            }

            var temp = list[i];
            list[i] = list[maxIndex];
            list[maxIndex] = temp;

            topIndexes[i] = maxIndex;
        }

        List<KeyValuePair<int, int>> top = new List<KeyValuePair<int, int>>();

        for (int i = 0; i < k; i++)
            top.Add(new KeyValuePair<int, int>(topIndexes[i], list[i]));

        return top;
    }

【讨论】:

    【解决方案2】:

    我认为你可以显着加快算法中找到 10 个最高计数的部分,但使用选择排序。

    最快的方法是使用QuickSelect 算法,但这对我来说有点太复杂了,无法在这里写。

    相反,我将使用Partial Selection Sort 进行演示。

    这是一个使用它的Method5()。与Method1()基本相同,只是在最后阶段使用部分排序而不是完全排序:

    private static List<KeyValuePair<int, int>> Method5()
    {
        Dictionary<int, int> dic = new Dictionary<int, int>();
    
        for (int i = 0; i < input.Count; i++)
        {
            int number = input[i];
    
            if (dic.ContainsKey(number))
                dic[number]++;
            else
                dic.Add(number, 1);
        }
    
        var result = dic.ToList();
        partialSort(result, 10);
    
        return result;
    }
    
    private static void partialSort(List<KeyValuePair<int, int>> list, int k)
    {
        for (int i = 0; i < k; ++i)
        {
            int maxIndex = i;
            int maxValue = list[i].Value;
    
            for (int j = i+1; j < list.Count; ++j)
            {
                if (list[j].Value > maxValue)
                {
                    maxIndex = j;
                    maxValue = list[j].Value;
                }
            }
    
            var temp = list[i];
            list[i] = list[maxIndex];
            list[maxIndex] = temp;
        }
    }
    

    我对 RELEASE(而非调试)构建进行了计时,当 K == 10 时,这在我的 PC 上似乎快了近三倍。

    相对于集合的大小,K 越小,它的速度就越快。对于较大的 K 值(特别是在 K 接近集合大小的情况下),此算法可能比普通排序慢。

    如果K 预计很大,

    不要使用此算法。它的效率很大程度上取决于K 的值相对于集合的大小是否较小。

    【讨论】:

    • 感谢@MatthewWatson。它加快了排序,因为它包含的元素比所有元素都少,但在我的测试中,分组代码占总操作时间的三分之二,这降低了排序改进的效果。
    猜你喜欢
    • 1970-01-01
    • 2012-09-09
    • 1970-01-01
    • 2017-03-17
    • 1970-01-01
    • 2014-06-12
    • 1970-01-01
    • 1970-01-01
    • 2019-04-27
    相关资源
    最近更新 更多