优化 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 |红绿灯 | {销售点 |销售点 |销售点 |销售点 |伦 |伦} | {数据字节}
哪里|表示单独的字节
- T 是指向 table 元数据字典的字节指针
- 如果条目
HL是头部分的一个字节长度
POS 和 LEN 对条目中的每个数据值重复:
- POSx4 = int 指示此数据在条目中的位置
- POSx2 = 条目中此数据的短长度
然后{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 指向这个数组中的一个索引。迭代这个数组比枚举字典元素并迭代它们要快得多。不过,我需要实施一些机制来确保一致性。
目前的任务是迭代海量字典,很头疼。我无法在此处查明高 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 |红绿灯 | {销售点 |销售点 |销售点 |销售点 |伦 |伦} | {数据字节}
哪里|表示单独的字节
- T 是指向 table 元数据字典的字节指针
- 如果条目 HL是头部分的一个字节长度
POS 和 LEN 对条目中的每个数据值重复:
- POSx4 = int 指示此数据在条目中的位置
- POSx2 = 条目中此数据的短长度
然后{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 指向这个数组中的一个索引。迭代这个数组比枚举字典元素并迭代它们要快得多。不过,我需要实施一些机制来确保一致性。