GC принудительно работает при работе с маленькими изображениями (<=4k пикселей данных)?
Я вижу, что счетчик производительности "# Induced GC" (который должен оставаться на нуле в идеальном приложении) быстро увеличивается при обработке небольших файлов (<= 32x32) через WriteableBitmap
,
Хотя это не является существенным узким местом внутри небольшого приложения, оно становится очень большой проблемой (приложение зависает со скоростью 99,75% "% времени в ГХ" на несколько секунд на каждом шаге), когда в памяти существует несколько тысяч объектов (например: 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);
Я могу запустить этот тест, используя "const int Size = 48
"дюжина раз: он всегда возвращается через ~1,5 с, а" # Induced GC "иногда увеличивается на 1 или 2.
Когда я переоденусьconst int Size = 48
"в"const int Size = 32
"Затем происходит нечто очень и очень плохое: "# Induced GC"увеличивается на 10 в секунду, а общее время выполнения составляет более минуты: ~80 с!" [Проверено на Win7x64 Core-i7-2600 с 8 ГБ ОЗУ //.NET 4.0.30319.237]
WTF!?
Либо у Framework очень плохая ошибка, либо я делаю что-то совершенно не так.
Кстати:
Я решил эту проблему не путем обработки изображений, а просто с помощью всплывающей подсказки, содержащей изображение, для некоторых объектов базы данных с помощью DataTemplate: это работало нормально (быстро), хотя в ОЗУ не было много объектов - но когда они существовали несколько миллионов других объектов (совершенно не связанных), показывающих всплывающую подсказку, всегда задерживались на несколько секунд, тогда как все остальное работало нормально.
4 ответа
Под все SafeMILHandleMemoryPressure
а также SafeMILHandle
ерунда это вызов метода на 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> .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
) вы увидите, что он использует только GC.AddMemoryPressure
если добавляемое давление <= 8192. В противном случае он использует собственную систему для отслеживания нагрузки на память и запуска сборов. Размер растрового изображения 32x32 с 32-битными пикселями подпадает под этот предел, потому что WriteableBitmap
оценивает использование памяти как 32 * 32 * 4 * 2 (я не уверен, почему там есть дополнительный фактор 2).
Таким образом, похоже, что поведение, которое вы видите, является результатом эвристики в рамках, которая не работает так хорошо для вашего случая. Возможно, вам удастся обойти это, создав растровое изображение с большими размерами или большим форматом пикселя, чем вам нужно, чтобы предполагаемый объем памяти растрового изображения> 8192.
Задумка: я думаю, это также предполагает, что коллекции сработали в результате GC.AddMemoryPressure
считаются под "# Индуцированные GC"?
Выполнение кода Маркуса на Win7 x86 (T4300, 2,1 ГГц, 3 ГБ):
(обратите внимание на огромную разницу между 33 и 32)
Is64BitOperatingSystem: False
Is64BitProcess: False
Версия: 4.0.30319.237
Бегущий тест с 40: 3,20 с
Бегущий тест с 34: 1,14 с
Бегущий тест с 33: 1,06 с
Бегущий тест с 32: 64,41 с
Бегущий тест с 30: 53,32 с
Бегущий тест с 24: 29,01 с
Другая машина Win7 x64 (Q9550, 2, 8 ГГц, 8 ГБ):
Is64BitOperatingSystem: True
Is64BitProcess: False
Версия: 4.0.30319.237
Бегущий тест с 40: 1,41 с
Беговой тест с 34: 1,24 с
Бегущий тест с 33: 1,19 с
Бегущий тест с 32: 1.554,45 с
Бегущий тест с 30: 1.489,31 с
Беговой тест с 24: 842,66 с
Еще раз с 40: 7,21 с
Процессор Q9550 обладает гораздо большей мощностью, чем T4300, но работает на 64-битной ОС.
Это, кажется, замедляет все это.
Попробуйте этот простой обходной путь:
Вызов GC.AddMemoryPressure(128 * 1024)
однажды, это заглушит механизм давления памяти.
Если это не достаточно оцепенело, дайте большее число.