在处理小图像(< = 4k像素数据)时强制GC?

时间:2011-09-07 09:41:30

标签: .net wpf garbage-collection

当我通过{{1}处理文件(< = 32x32)时,我看到性能计数器“#Induced GC”(应该在完美应用中保持为零)迅速增加}。

虽然这不是一个小应用程序中的一个重要瓶颈,但是当内存中存在数千个对象时,它变成了一个非常大的问题(应用程序冻结在99.75%“GC中的时间”,每个步骤几秒钟)例如:WriteableBitmap上下文加载了许多实体和关系。)

综合测试:

EntityFramework

我可以使用“var objectCountPressure = ( from x in Enumerable.Range(65, 26) let root = new DirectoryInfo((char)x + ":\\") let subs = from y in Enumerable.Range(0, 100 * IntPtr.Size) let sub =new {DI = new DirectoryInfo(Path.Combine(root.FullName, "sub" + y)), Parent = root} let files = from z in Enumerable.Range(0, 400) select new {FI = new FileInfo(Path.Combine(sub.DI.FullName, "file" + z)), Parent = sub} select new {sub, files = files.ToList()} select new {root, subs = subs.ToList()} ).ToList(); const int Size = 32; Action<int> handler = threadnr => { Console.WriteLine(threadnr + " => " + Thread.CurrentThread.ManagedThreadId); for (int i = 0; i < 10000; i++)    { var wb = new WriteableBitmap(Size, Size, 96, 96, PixelFormats.Bgra32, null); wb.Lock(); var stride = wb.BackBufferStride; var blocks = stride / sizeof(int); unsafe { var row = (byte*)wb.BackBuffer; for (int y = 0; y < wb.PixelHeight; y++, row += stride) { var start = (int*)row; for (int x = 0; x < blocks; x++, start++) *start = i; } } wb.Unlock(); wb.Freeze();     } }; var sw = Stopwatch.StartNew(); Console.WriteLine("start: {0:n3} ms", sw.Elapsed.TotalMilliseconds); Parallel.For(0, Environment.ProcessorCount, new ParallelOptions{MaxDegreeOfParallelism = Environment.ProcessorCount}, handler); Console.WriteLine("stop : {0:n2} s", sw.Elapsed.TotalSeconds); GC.KeepAlive(objectCountPressure); ”运行此测试十几次:它总是在~1.5s内返回,“#Induced GC”有时会增加1或2。

当我将“const int Size = 48”更改为“const int Size = 48”时,发生了非常非常糟糕的事情:“#Induced GC”每秒增加10次,整个运行时间现在超过一分钟: 〜80年代! [在带有8GB RAM的Win7x64 Core-i7-2600上测试// .NET 4.0.30319.237]

WTF!?

要么框架有一个非常糟糕的错误,要么我做错了。

顺便说一句
我不是通过图像处理来解决这个问题,而是通过DataTemplate对一些数据库实体使用包含Image的Tooltip: 这工作得很好(快),而RAM中没有很多对象 - 但是当存在大约数百万个其他对象(完全不相关)时,显示工具提示总是延迟几秒钟,而其他一切工作正常。< / p>

4 个答案:

答案 0 :(得分:13)

TL; DR:可能最好的解决方案是创建一个WriteableBitmaps的小池并重复使用它们而不是创建它们并扔掉它们。

所以我开始使用WinDbg来了解导致集合发生的原因。

首先,我在Debugger.Break()的开头添加了对Main的调用,以简化操作。我还将自己的调用添加到GC.Collect()作为完整性检查,以确保我的断点正常工作。然后在WinDbg中:

0:000> .loadby sos clr
0:000> !bpmd mscorlib.dll System.GC.Collect
Found 3 methods in module 000007feee811000...
MethodDesc = 000007feee896cb0
Setting breakpoint: bp 000007FEEF20E0C0 [System.GC.Collect(Int32)]
MethodDesc = 000007feee896cc0
Setting breakpoint: bp 000007FEEF20DDD0 [System.GC.Collect()]
MethodDesc = 000007feee896cd0
Setting breakpoint: bp 000007FEEEB74A80 [System.GC.Collect(Int32, System.GCCollectionMode)]
Adding pending breakpoints...
0:000> g
Breakpoint 1 hit
mscorlib_ni+0x9fddd0:
000007fe`ef20ddd0 4154            push    r12
0:000> !clrstack
OS Thread Id: 0x49c (0)
Child SP         IP               Call Site
000000000014ed58 000007feef20ddd0 System.GC.Collect()
000000000014ed60 000007ff00140388 ConsoleApplication1.Program.Main(System.String[])

所以断点工作正常,但是当我让程序继续时,它再也没有被击中。似乎从更深层的某个地方调用GC例程。接下来,我进入GC.Collect()函数,看看它在调用什么。为了更容易地做到这一点,我在第一次呼叫后立即向GC.Collect()添加了第二次呼叫并进入第二次呼叫。这避免了逐步完成所有JIT编译:

Breakpoint 1 hit
mscorlib_ni+0x9fddd0:
000007fe`ef20ddd0 4154            push    r12
0:000> p
mscorlib_ni+0x9fddd2:
000007fe`ef20ddd2 4155            push    r13
0:000> p
...
0:000> p
mscorlib_ni+0x9fde00:
000007fe`ef20de00 4c8b1d990b61ff  mov     r11,qword ptr [mscorlib_ni+0xe9a0 (000007fe`ee81e9a0)] ds:000007fe`ee81e9a0={clr!GCInterface::Collect (000007fe`eb976100)}

经过一番踩踏后,我注意到clr!GCInterface::Collect的提法听起来很有希望。不幸的是,它的断点从未触发过。进一步深入GC.Collect()我发现clr!WKS::GCHeap::GarbageCollect被证明是真正的方法。关于此的断点揭示了触发集合的代码:

0:009> bp clr!WKS::GCHeap::GarbageCollect
0:009> g
Breakpoint 4 hit
clr!WKS::GCHeap::GarbageCollect:
000007fe`eb919490 488bc4          mov     rax,rsp
0:006> !clrstack
OS Thread Id: 0x954 (6)
Child SP         IP               Call Site
0000000000e4e708 000007feeb919490 [NDirectMethodFrameStandalone: 0000000000e4e708] System.GC._AddMemoryPressure(UInt64)
0000000000e4e6d0 000007feeeb9d4f7 System.GC.AddMemoryPressure(Int64)
0000000000e4e7a0 000007fee9259a4e System.Windows.Media.SafeMILHandle.UpdateEstimatedSize(Int64)
0000000000e4e7e0 000007fee9997b97 System.Windows.Media.Imaging.WriteableBitmap..ctor(Int32, Int32, Double, Double, System.Windows.Media.PixelFormat, System.Windows.Media.Imaging.BitmapPalette)
0000000000e4e8e0 000007ff00141f92 ConsoleApplication1.Program.<Main>b__c(Int32)

因此WriteableBitmap的构造函数间接调用GC.AddMemoryPressure,最终导致集合(顺便说一下,GC.AddMemoryPressure是一种模拟内存使用的简单方法)。这并不能解释从33岁到32岁时行为的突然变化。

ILSpy在这里提供帮助。特别是,如果您查看SafeMILHandleMemoryPressure的构造函数(由SafeMILHandle.UpdateEstimatedSize调用),如果要添加的压力为&lt; = 8192,您将看到它仅使用GC.AddMemoryPressure。否则它使用自己的自定义系统来跟踪内存压力和触发集合。具有32位像素的32x32的位图大小属于此限制,因为WriteableBitmap估计内存使用为32 * 32 * 4 * 2(我不确定为什么会有额外因子2)。

总而言之,您看到的行为看起来就像是框架中的启发式结果,对您的案例来说效果不佳。 您可以通过创建比您需要更大尺寸或更大像素格式的位图来解决这个问题,以便估计位图的内存大小> 8192。

事后补充:我想这也表明由GC.AddMemoryPressure引发的集合会被计入“#Induced GC”?

答案 1 :(得分:8)

在所有SafeMILHandleMemoryPressureSafeMILHandle废话下,调用MS.Internal.MemoryPressure上的方法,该方法使用静态字段“_totalMemory”来跟踪多少内存WPF认为是分配的。当它达到(相当小的)极限时,诱导的GC开始并且永远不会结束。

你可以完全使用一点点反射魔法阻止WPF这样做;只需将_totalMemory设置为适当的负值,以便永远不会达到极限并且诱导的GC永远不会发生:

typeof(BitmapImage).Assembly.GetType("MS.Internal.MemoryPressure")
    .GetField("_totalMemory", BindingFlags.NonPublic | BindingFlags.Static)
    .SetValue(null, Int64.MinValue / 2);

答案 2 :(得分:1)

在Win7 x86上运行Markus代码(T4300,2.1GHz,3GB):
(注意33和32之间的巨大差异)

Is64BitOperatingSystem:False
Is64BitProcess:False
版本:4.0.30319.237

40:3,20秒的跑步测试 34:1,14秒运行测试 以33:1,06秒运行测试 运行测试32:64,41 s
运行测试30:53,32 s 以24:29,01 s运行测试

另一台机器Win7 x64(Q9550,2.8GHz,8GB):

Is64BitOperatingSystem:是的 Is64BitProcess:False
版本:4.0.30319.237

以40:1,41秒运行测试 34:1,24 s的运行测试 以33:1,19秒运行测试 32:1.554,45 s运行测试
运行测试30:1.489,31 s 24:842,66 s的跑步测试 再次使用40:7,21 s

Q9550 CPU比T4300拥有更多功率,但它运行在64位操作系统上。
这似乎减缓了整个事情。

答案 3 :(得分:0)

尝试这个简单的解决方法:

呼叫GC.AddMemoryPressure(128 * 1024)一次,这将使记忆压力机制麻木。

如果它不够麻木,请提供更大的数字。