优化C#大型数据集的迭代-探查器中的外部代码和怪异的行为

时间:2018-09-01 14:54:58

标签: c# performance collections .net-core cpu

当前的任务,遍历大量词典,使我头疼。我无法在此处查明CPU使用率高的确切来源,因此希望这里的一些C#专家可以给我一些提示和技巧。

设置是10个预分配的Guid-byte []字典,每个字典包含一百万个条目。整个过程都在迭代,每个字典都有自己的线程。简单地遍历所有变量,并将byte []引用传递给迭代委托,产生随机结果所需的时间不到2ms,但是实际上访问包含条目中的任何字节都会导致该数字增加到300 + ms。

注意:迭代委托是在任何迭代之前构造的,然后我仅传递引用。

如果我对接收到的字节引用不做任何事情,那一切都非常快:

            var iterationDelegate = new Action<byte[]>((bytes) =>
            {
                var x = 5 + 10;
            });

但是一旦我尝试访问第一个字节(实际上包含指向其他地方该行的元数据的指针)

            var iterationDelegate = new Action<byte[]>((bytes) =>
            {
                var b = (int)bytes[0];
            });

总时间猛增,甚至更奇怪,第一组迭代花费30ms,第二组40+,第三组100+,第四组花费500ms + ...然后我停止测试性能,让调用线程休眠持续几秒钟,一旦我再次开始迭代,它会在30ms内随便开始,然后像以前一样上升,直到我再次给它“呼吸时间”。

当我在VS CPU调用树中查看它时,[外部代码]占用了93%的CPU,我无法查看或至少看不到它是什么。

有什么我可以做些帮助的吗? GC的时间很艰难吗?

编辑1:我要运行的实际代码是:

            var iterationDelegate = new Action<byte[]>((data) =>
            {
                //compare two bytes, ensure the row belongs to desired table
                if (data[0] != table.TableIndex)
                    return;

                //get header length
                var headerLength = (int)data[1];

                //process the header info and retrieve the desired column data position:

                var columnInfoPos = (key * 6) + 2;

                var pointers = new int[3] {
                    //data position
                BitConverter.ToInt32(new byte[4] {
                    data[columnInfoPos],
                    data[columnInfoPos + 1],
                    data[columnInfoPos + 2],
                    data[columnInfoPos + 3] }),
                    //data length
                BitConverter.ToUInt16(new byte[2] {
                    data[columnInfoPos + 4],
                    data[columnInfoPos + 5] }),
                //column info position
                columnInfoPos };


            });

但是这段代码甚至更慢,迭代时间分别为〜150,〜300,〜600、700 +

这是在各个线程中为每个商店保持活动的工作程序类:

            class PartitionWorker
            {
                private ManualResetEvent waitHandle = new ManualResetEvent(true);
                private object key = new object();
                private bool stop = false;
                private List<Action> queue = new List<Action>();

                public void AddTask(Action task)
                {
                    lock (key)
                        queue.Add(task);
                    waitHandle.Set();
                }

                public void Run()
                {
                    while (!stop)
                    {
                        lock (key)
                            if (queue.Count > 0)
                            {
                                var task = queue[0];
                                task();
                                queue.Remove(task);
                                continue;
                            }
                        waitHandle.Reset();
                        waitHandle.WaitOne();
                    }
                }

                public void Stop()
                {
                    stop = true;
                }
            }

最后是一个启动迭代的代码,该代码从Task中为每个传入的TCP请求运行。

            for (var memoryPartition = 0; memoryPartition < partitions; memoryPartition++)
            {
                var memIndex = memoryPartition;
                mem[memIndex].AddJob(() =>
                {
                    try
                    {
                        //... to keep it shor i have excluded readlock and try/finally
                        foreach (var obj in mem[memIndex].innerCache.Values)
                        {
                            iterationDelegate(obj.bytes);
                        }
                        //release readlock in finally..
                    }
                    catch
                    {

                    }
                    finally
                    {
                        latch.Signal();
                    }
                });
            }
            try
            {
                latch.Wait(50);
                sw.Stop();
                Console.WriteLine("Found " + result.Count + " in " + sw.Elapsed.TotalMilliseconds + "ms");
            }
            catch
            {
                Console.WriteLine(">50");
            }

Edit2: 字典是使用

预先分配的
private Dictionary<Guid, byte[]> innerCache = new Dictionary<Guid, byte[]>(part_max_entries);

,对于条目,它们平均为70个字节。该过程占用了大约2Gb的内存,其中有10000000个条目分配在10个词典中。

条目的结构如下:

T | HL | {POS | POS | POS | POS | LEN | LEN} | {数据字节}

其中|表示单独的字节

  • T是指向表元数据字典的字节指针
  • HL是标头部分(如果有条目)的字节长度

POS和LEN对条目中的每个数据值重复:

  • POSx4 = int,指示此数据在条目中的位置
  • POSx2 =条目中此数据的超短长度

然后{data bytes}是数据有效载荷

1 个答案:

答案 0 :(得分:-1)

对于那些可能想知道的人来说,最大的性能提升是实际使用热纺而不是休眠/延迟/ WaitHandles。即使有大量并行请求,CPU的损失也可以忽略不计。对于非常密集的操作,有一个回退实现,即如果旋转花费的时间超过3毫秒,则回退到线程等待。代码现在以恒定的24ms / 10mil条目运行。另外,从代码中删除所有GC集合并回收尽可能多的变量也是有益的。

这是我使用的微调代码:

drop user admin@localhost;
flush privileges;
create user admin@localhost identified by 'admins_password'

注意:这只能与在其自己的线程中运行的代码一起使用!如果在单线程应用程序中使用它,则将在此处阻止所有代码。

编辑: 同样值得注意的是,由于某种原因,以某种方式重写for循环(使其计数为0)会对性能产生重大影响。我不知道原因的确切机制,但我认为与零进行比较会更快。

我还修改了词典,现在是Dictionary(Guid,Int)。我添加了一个byte [] []数组,而字典int指向此数组中的索引。与枚举字典元素并对其进行迭代相比,遍历此数组要快得多。不过,我需要实施一些机制来确保一致性。

相关问题