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

Optimizing C# large dataset iterations - External code in profiler and weird behavior

目前的任务是迭代海量字典,很头疼。我无法在此处查明高 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];
            });

总时间猛增,更奇怪的是,第一组迭代需要 30 毫秒,第二组 40+,第三个 100+ 和第四个可能需要 500 毫秒+...然后我停止测试性能,睡觉调用线程几秒钟,一旦我再次开始迭代,它会在 30 毫秒处随意开始,然后像以前一样上升,直到我再次给它 "time to breathe"。

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

有什么我可以做的吗?是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:

            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;
                }
            }

最后是启动迭代的代码,此代码 运行 来自每个传入 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");
            }

编辑2: 字典使用

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

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

条目结构如下:

T |红绿灯 | {销售点 |销售点 |销售点 |销售点 |伦 |伦} | {数据字节}

哪里|表示单独的字节

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

然后{data bytes}是数据负载

对于那些可能想知道的人来说,最大的性能提升是实际使用热旋转而不是 sleeping/delaying/WaitHandles。即使有大量并行请求,CPU 命中也可以忽略不计。对于非常密集的操作有一个回退实现,如果旋转时间超过 3 毫秒,它会回退到线程等待。代码现在 运行 在相当恒定的 24 毫秒/1000 万条目。此外,从代码中删除任何 GC 收集并尽可能多地回收变量也是有益的。

这是我使用的旋转器代码:

    private static void spin(ref Stopwatch sw, double spinSeconds)
    {
        sw.Start();
        while (sw.ElapsedTicks < spinSeconds) { }
        sw.Stop();
    }

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

编辑: 另外值得注意的是,出于某种原因,以某种方式重写 for 循环以使其计数为 0 会对性能产生重大影响。我不知道具体的机制,但我认为与零比较会更快。

我也修改了字典,现在是Dictionary(Guid,Int)。我添加了一个 byte[][] 数组,字典 int 指向这个数组中的一个索引。迭代这个数组比枚举字典元素并迭代它们要快得多。不过,我需要实施一些机制来确保一致性。