Подтвердить что ты не робот

GC работает при работе с маленькими изображениями (<= 4k пикселей)?

Я вижу счетчик производительности "# Induced GC" (который должен оставаться на нуле в идеальном приложении) быстро растет при обработке небольших файлов (< = 32x32) через WriteableBitmap.

Хотя это небольшое препятствие внутри небольшого приложения, это становится очень большой проблемой (приложение замораживается при 99,75% "% времени в GC" в течение нескольких секунд на каждом шаге), когда в памяти имеется около тысячи объектов ( ex: 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.5s, а "# 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 имеет очень плохую ошибку, либо я делаю что-то совершенно неправильное.

BTW:
Я столкнулся с этой проблемой не с помощью обработки изображений, а просто с помощью всплывающей подсказки, содержащей изображение с некоторыми объектами базы данных через DataTemplate: Это работало отлично (быстро), в то время как в ОЗУ не было очень много объектов, но когда существовало несколько миллионов других объектов (совершенно не связанных), то показывая подсказку, всегда задерживалась на несколько секунд, а все остальное просто отлично работало.

4b9b3361

Ответ 1

При всех ошибках 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);

Ответ 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 - это более простой способ имитировать использование памяти). Это не объясняет внезапное изменение поведения при переходе от 32 до 32.

ILSpy помогает здесь. В частности, если вы посмотрите на конструктор для SafeMILHandleMemoryPressure (вызывается SafeMILHandle.UpdateEstimatedSize), вы увидите, что он использует только GC.AddMemoryPressure, если давление для добавления равно <= 8192. В противном случае он использует свою собственную систему для отслеживание давления памяти и запуск сборок. Растровый размер 32x32 с 32-битными пикселями подпадает под этот предел, потому что WriteableBitmap оценивает использование памяти как 32 * 32 * 4 * 2 (я не уверен, почему существует дополнительный коэффициент 2).

В целом, похоже, что поведение, которое вы видите, является результатом эвристики в рамках, которая не так хорошо работает для вашего дела. Возможно, вы сможете обойти это, создав растровое изображение с большими размерами или более крупным размером пикселей, чем вам нужно, чтобы предполагаемый размер памяти растрового изображения был > 8192.

Последующая мысль: я думаю, это также предполагает, что коллекции, вызванные в результате GC.AddMemoryPressure, подсчитываются под "# Induced GC"?

Ответ 3

Запуск кода Markus на Win7 x86 (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 с

Запуск теста с 30: 53,32 сек
Пробег с 24: 29,01 с

Другая машина Win7 x64 (Q9550, 2.8GHz, 8GB):

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 s
Еще раз с 40: 7,21 с

Процессор Q9550 обладает гораздо большей мощностью, чем T4300, но работает на 64-битной ОС.
Это, кажется, замедляет все это.

Ответ 4

Попробуйте это обходное решение:

Вызов GC.AddMemoryPressure(128 * 1024) один раз, это затмит механизм давления памяти.

Если он недостаточно оцепенел, укажите большее число.