Dot-Net

處理小圖像(<=4k 像素數據)時強制使用 GC?

  • April 14, 2014

當通過. _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&lt;int&gt; handler = threadnr =&gt; {
   Console.WriteLine(threadnr + " =&gt; " + Thread.CurrentThread.ManagedThreadId);
   for (int i = 0; i &lt; 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 &lt; wb.PixelHeight; y++, row += stride)
           {
               var start = (int*)row;
               for (int x = 0; x &lt; 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);

我可以使用“ const int Size = 48”執行這個測試十幾次:它總是在大約 1.5 秒內返回,“# Induced GC”有時會增加 1 或 2。

當我將“ const int Size = 48”更改為“ const int Size = 32”時,發生了非常非常糟糕的事情:“#Induced GC”每秒增加 10 次,現在總執行時間超過一分鐘:~80s![在 8GB RAM 的 Win7x64 Core-i7-2600 上測試 // .NET 4.0.30319.237 ]

哇!?

要麼框架有一個非常糟糕的錯誤,要麼我做錯了什麼。

順便說一句

我不是通過進行圖像處理來解決這個問題,而是通過 DataTemplate 對某些數據庫實體使用包含圖像的工具提示:雖然 RAM 中不存在太多對象,但它工作得很好(快速)——但是當存在數百萬個其他對象(完全不相關)然後顯示工具提示總是延遲幾秒鐘,而其他一切都工作正常。

下面是SafeMILHandleMemoryPressureSafeMILHandleon 方法的呼叫,該方法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);

**TL;DR:**最好的解決方案可能是創建一個小池WriteableBitmaps並重用它們,而不是創建它們並丟棄它們。

所以我開始用 WinDbg 探索,看看是什麼導致了集合的發生。

Debugger.Break()首先,我在開頭添加了一個呼叫,Main以使事情變得更容易。我還添加了自己的呼叫GC.Collect()作為健全性檢查,以確保我的斷點工作正常。然後在 WinDbg 中:

0:000&gt; .loadby sos clr
0:000&gt; !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&gt; g
Breakpoint 1 hit
mscorlib_ni+0x9fddd0:
000007fe`ef20ddd0 4154            push    r12
0:000&gt; !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&gt; p
mscorlib_ni+0x9fddd2:
000007fe`ef20ddd2 4155            push    r13
0:000&gt; p
...
0:000&gt; 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&gt; bp clr!WKS::GCHeap::GarbageCollect
0:009&gt; g
Breakpoint 4 hit
clr!WKS::GCHeap::GarbageCollect:
000007fe`eb919490 488bc4          mov     rax,rsp
0:006&gt; !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.&lt;Main&gt;b__c(Int32)

SoWriteableBitmap的建構子間接呼叫GC.AddMemoryPressure,最終導致集合(順便說一句,這GC.AddMemoryPressure是一種更簡單的模擬記憶體使用的方法)。但是,這並不能解釋從 33 到 32 大小時行為的突然變化。

ILSpy在這裡提供幫助。特別是,如果您查看SafeMILHandleMemoryPressure(invoked by SafeMILHandle.UpdateEstimatedSize) 的建構子,您會發現它僅GC.AddMemoryPressure在添加的壓力 <= 8192 時使用。否則,它使用自己的自定義系統來跟踪記憶體壓力和触發集合。具有 32 位像素的 32x32 點陣圖大小低於此限制,因為WriteableBitmap估計記憶體使用為 32 * 32 * 4 * 2(我不確定為什麼會有額外的 2 因子)。

總而言之,您所看到的行為似乎是框架中啟發式的結果,該框架對您的情況不太適用。 您可以通過創建比您需要的更大尺寸或更大像素格式的點陣圖來解決它,以便點陣圖的估計記憶體大小> 8192。

事後思考:我想這也表明由於以下原因觸發的收集GC.AddMemoryPressure計入“#Induced GC”?

引用自:https://stackoverflow.com/questions/7331735