-
-
Notifications
You must be signed in to change notification settings - Fork 860
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Unreclaimed ArrayPool<byte>.Shared memory after processing heavy GIF animations #2889
Comments
@antonfirsov tagging you here as you're the expert. The best solution IMO would be either:
|
@hexawyz could you help us by sharing a minmal runnable repro with a specific gif, just in case?
Reclaim should happen at buffer/image disposal. If the user code disposes things correctly and the library is still leaking arraypool arrays, it's a bug. |
I think what they mean is that the |
Sorry for the misunderstanding! There is a trimming mechanism in the We can expose knobs to control pool tresholds on @hexawyz does this make sense to you? Do you experience issues beyond just seeing an overly large memory retention? |
My issues are actually the large retention of memory 😅
Yeah, but that's exactly my problem here, this is not a server app 🙂 From my undestanding, I was doing some tests again a few minutes ago, and here's what I got: In this case, I used a 400x400 image that is about 3MB compressed and applied my transformations. (this GIF and the transformation code is here in case you want to have a look) Moreover, in the previous days when I did some non-memory-related tests, transforming multiple images in succession led to the memory growing higher and higher. For example at one point, I rose to more than 700MB of RAM usage before stopping the app. There's also another thing to consider. I don't know what is the reasonable limit for requesting an array from the shared pool, but I feel like 1MB is a huge amount to request of a pool that is likely to be used for mostly smallish things. It also seems unwise to request more than a handful of shared buffers per operation, which is a very specific problem of GIF images here. So, I'm actually honestly wondering if there is really any value in using the array pool at all. At least, I believe it would be helful to be able to opt out of the usage of the shared |
ArrayPool lowers the GC (or in general, allocation/deallocation) pressure and somewhat improves data locality which shows up on throughput benchmarks. I would need to look up and re-run benchmarks to be more specific.
That's a reasonable goal for a desktop app.
Since your buffers are small enough to not hit the unmanaged pool treshold, the behavior would be basically equivalent to what you get with SimpleGCMemoryAllocator.
It's relatively simple, I may put up a gist tomorrow. Would it make sense to benchmark the default pooling allocator against SimpleGCMemoryAllocator and an unmanaged one for scenarios you care? (Then based on the data we could see if we can add something to the lib to help fellow desktop devs.)
Gifs are stressing any memory management mechanism to the extreme. I'm getting ideas like a layer for multi-frame images that slices down bigger contigous buffers to smaller ones whenever beneficial (for very large images we do the other way around -- composing them out of smaller 4MB buffers). Or even better, some facility that allows frame-by-frame stream-processing of gifs. But these ideas lead too far and beyond the team's current capacity to build things :( |
So, I actually tried to implement a full native unpooled memory allocator, and it seemed to work quite well (maybe I've got a slight performance degradation because of this ? Would be hard to tell, honestly)
Hmm good point about data locality, I didn't think about that. I'm wondering though if in that case, it still wouldn't be worth it to maintain a specialized ImageSharp-only pool of buffers, with maybe some kind of hard limit on the number of pooled items.
I think I'll take low memory usage over ultimate performance for now 😂
Hmm I was thinking about directly falling back to unmanaged memory allocations rather than managed arrays, but yeah d |
Hmm wait no, it seems I actually never So I had in fact two problems. First was indeed that I had too many allocations in the ArrayPool for a single Image. Might be worth considering re-adding finalizers somewhere ? |
There is a finalizer in the lowest-level thingie that is responsible for managing the buffer ownership which is our equivalent for
Pity I forgot to mention it earlier, but we have a diagnostic utility class
Your code seems to be correct at first glance, but if you want to be 100% on the safe side, consider delegating the finalization and the refcounting concerns to
IMO 256 bytes is too low which might hurt throughput. You can definitely go higher, likely into the ranges of tens of KBs. Only benchmarks can tell the exact number. |
You likely have hit this actually: ImageSharp/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs Lines 70 to 76 in e20e47f
|
Oh yeah, thanks, I clearly missed that 😄
Well, I found the hard way by implementing a similar thing in my native allocator. Was also useful to identify what were the different buffer sizes that were requested.
Hmm yeah, I may trade the "useful" finalizer for a debug-logging finalizer. This might be good enough.
My worry is still with the factual reusability of such buffers. There should be potential for reuse of buffers of up to 1kB or possibly 4kB, but I think as the buffer become larger, the overhead of the MemoryManager becomes less significant anyway. (And throughput is not that important to me currently) |
Prerequisites
DEBUG
andRELEASE
modeImageSharp version
3.1.6
Other ImageSharp packages and versions
None
Environment (Operating system, version and so on)
Windows 11 23H2
.NET Framework version
9.0.200
Description
When processing GIF animations that have generally relatively "small" dimensions (e.g. about 500x500 maximum, often less), mainly for cropping and resizing, I noticed a significant increase in memory consumption after these operations. This was using the default MemoryAllocator.
Memory was noot freed by calling
ReleaseRetainedResources
.From investigations, this seems to come from the fact that the
UniformUnmanagedMemoryPoolMemoryAllocator
will defer to the standard ArrayPool for all buffers below 1MB, and sadly, these arrays will never be reclaimed:ImageSharp/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs
Line 130 in e20e47f
(I also just found this issue which seems to mention a similar problem: #2139)
In my case, because the dimensions of images are relatively small, it turns out that frames will almost always fit in less than 1MB, but there can be many frames.
A workaround is to use the
SimpleGcMemoryAllocator
, but it comes with other problems and will also likely introduce fragmentation in the LOH.As an easy improvement, it seems like it would be easy to allow customizing the
sharedArrayPoolThresholdInBytes
parameter, which is always defaulted to 1MB.Steps to Reproduce
Images
No response
The text was updated successfully, but these errors were encountered: