I'm seeing the performance counter "# Induced GC" (which should stay at zero in a perfect app) increasing rapidly when processing small files (<= 32x32) via WriteableBitmap
.
While this isn't a significant bottleneck inside a small app, it becomes a very huge problem (app freezing at 99.75% "% Time in GC" for several seconds at each step) when there exist some thousand objects in memory (ex: EntityFramework
context loaded with many entities and relationships).
Synthetic test:
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);
I can run this test using "const int Size = 48
" a dozen times: It always returns in ~1.5s and "# Induced GC" sometimes increases by 1 or 2.
When I change "const int Size = 48
" into "const int Size = 32
" then something very very bad is happening: "# Induced GC" increases by 10 per second and the overall runtime now is more than a minute: ~80s !
[Tested on Win7x64 Core-i7-2600 with 8GB RAM // .NET 4.0.30319.237 ]
WTF!?
Either the Framework has a very bad bug or I'm doing something entirely wrong.
BTW:
I came around this problem not by doing image processing but by just using a Tooltip containing an Image against some database entities via a DataTemplate:
This worked fine (fast) while there didn't exist very much objects in RAM -- but when there existed some million other objects (totally unrelated) then showing the Tooltip always delayed for several seconds, while everything else just was working fine.
return (long) ((((pixelWidth * pixelHeight) * pixelFormat.InternalBitsPerPixel) / 8) * 2);
-> so it is exactly 0x2000 (8kb) for 32x32. Further down the chain the ctor of SafeMILHandleMemoryPressure decides to do something different at > 0x2000 -- below or equal to 0x2000 it always calls GC.AddMemoryPressure(long) which always seems to trigger a GC.Collect(2); even if it's happening a thousand times per second. – ShamanismGC.AddMemoryPressure
call with aGC.RemoveMemoryPressure
. Thus the memory pressure continues to rise and once it exceeds a certain level every call toAddMemoryPressure
triggers a full GC. – GalliganFreeze
call in there. Thus I suspect that freeze either allocates a lot of managed memory, or forgets manages to leak aAddMemoryPressure
call(i.e. it misses the pairedRemoveMemoryPressure
call. And if those don't match up bad things happen. – GalliganFreeze
helps seems to be coincidence. Interestingly the number of concurrent threads has a huge influence on the number of collections. Perhaps the memory held by one thread causes GCs triggered by other threads to be promoted and thus causes higher generation GCs. – Galligan