- 
          
 - 
                Notifications
    
You must be signed in to change notification settings  - Fork 888
 
WIP: swappable memory pools #431
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
Conversation
- Add a null memory manager that doesn't do any actual memory management
| 
           @JimBobSquarePants check out #225 and Gitter conversations if you want (+have some time) to keep track of this! :)  | 
    
| 
           Will do! 😄  | 
    
code just before going to sleep
need it (aside from a few exceptions)
          Codecov Report
 @@            Coverage Diff             @@
##           master     #431      +/-   ##
==========================================
- Coverage   80.72%   80.68%   -0.04%     
==========================================
  Files         512      514       +2     
  Lines       20152    20151       -1     
  Branches     2196     2200       +4     
==========================================
- Hits        16267    16259       -8     
- Misses       3210     3212       +2     
- Partials      675      680       +5
 Continue to review full report at Codecov. 
  | 
    
| 
               | 
          ||
| /// <inheritdoc /> | ||
| internal override Buffer<T> Allocate<T>(int size, bool clear = false) | ||
| internal override Buffer<T> Allocate<T>(int itemCount, bool clear = false) | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use two distinct methods here with overloads. I'm not a big fan of using optional parameters in inherited methods; it can often lead to breaking changes since this could well become a public API.
| Guard.MustBeGreaterThanOrEqualTo(data.Length, count, nameof(data)); | ||
| 
               | 
          ||
| var image = new ImageFrame<TPixel>(width, height); | ||
| var image = new ImageFrame<TPixel>(Configuration.Default.MemoryManager, width, height); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this usage of Configuration.Default makes sense here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree. We should pass the MemoryManager from ImageFrameCollection<TPixel> to this method!
This is the MemoryManager responsible for the allocation of all the Image<T> pixel data!
| /// <param name="height">The height of the image represented by the pixel buffer.</param> | ||
| public PixelAccessor(int width, int height) | ||
| : this(width, height, Buffer2D<TPixel>.CreateClean(width, height), true) | ||
| : this(width, height, Configuration.Default.MemoryManager.Allocate2D<TPixel>(width, height, true), true) | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit unfortunate, but parameterizing this spreads pretty wide. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately we need to propagate that parameter here :(
It's possible to do this incrementally by defining a temporal construct with an optional memoryManager = null parameter and calling memoryManager ?? Configuration.Default.MemoryManager.Allocate2D<TPixel>(width, height, true).
We should get rid of PixelAccessor anyways, so leave it for now if you want
| this.Length = this.RowStride * height; | ||
| 
               | 
          ||
| this.byteBuffer = MemoryManager.Current.Allocate<byte>(this.Length, true); | ||
| this.byteBuffer = Configuration.Default.MemoryManager.Allocate<byte>(this.Length, true); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above -- taking this as a ctor dependency has far-reaching consequences.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you feel the chore work is just too much, leave it then :)
We should get rid of both PixelAccessor<T> and PixelArea<T> anyways. They are legacy constructs.
| Guard.MustBeGreaterThanOrEqualTo(amount.Length, destination.Length, nameof(amount.Length)); | ||
| 
               | 
          ||
| using (Buffer<Vector4> buffer = MemoryManager.Current.Allocate<Vector4>(destination.Length * 3)) | ||
| using (Buffer<Vector4> buffer = Configuration.Default.MemoryManager.Allocate<Vector4>(destination.Length * 3)) | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the hardest usage to change, because while I could take this in as a ctor parameter, I'd have to pass in something for the singleton instances, and the only thing available... is Configuration.Default.MemoryManager.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this is actully forcing us to make API design decisions. I think it should be a parameter of Blend(). @tocsoft thoughts?
| /// <returns>The current operations class to allow chaining of operations.</returns> | ||
| IImageProcessingContext<TPixel> ApplyProcessor(IImageProcessor<TPixel> processor); | ||
| 
               | 
          ||
| MemoryManager GetMemoryManager(); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In order to be able to pass the currently-used memory manager to the processors that need it, the easiest way I could think up was to expose the MemoryManager from the processing context.
| 
               | 
          ||
| if (this.minSizeBytes > 0 && bufferSizeInBytes < this.minSizeBytes) | ||
| { | ||
| return new Buffer<T>(new T[itemSize], itemSize); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had this one down correctly before I changed from Marshal.SizeOf to Unsafe.SizeOf -- and then, for whatever reason, I messed it up entirely and didn't re-test it. Oh well.
        
          
                src/ImageSharp/Configuration.cs
              
                Outdated
          
        
      | /// <summary> | ||
| /// Gets or sets the <see cref="MemoryManager"/> that is currently in use. | ||
| /// </summary> | ||
| public MemoryManager MemoryManager { get; set; } = new ArrayPoolMemoryManager(1024 * 80); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This threshold here is rather large (80 kB). @antonfirsov was of the opinion, that the threshold for allocating from the pool should be half a kilobyte at the most, so we're going to want to change this, at least.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rytmis ArrayPool is actually really useful in small-mid dynamic array allocation use cases, because it reduces GC pressure + improves data locality!
Check out it's usages in aspnet mvc.
An example: it's providing ArrayPool<char> to Newtonsoft.Json through it's own IArrayPool<T> abstraction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed the default value to 512 bytes, but obviously you can decide what you want to use for it -- I don't know what benchmarks to run to find the optimal values, and so on, so I'll just defer to your judgment. 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not yet a full review, tried to focus on the most important conceptual stuff for now. My main points:
- We need to agree on the minSize / maxSize issues. I strongly believe that by tuning 
maxSizeInBytesforArrayPoolMemoryManager, all users should be able to avoidOutOfMemoryException-s & optimize their application. You won't needNullMemoryManagerfor that, believe me! :) My biggest mistake was to set the maximum to a crazy value, now I regret it very much :( You are already using ArrayPool by using ASP.NET MVC! (and they have a sane maximum limit I suppose) Buffer<T>.Arrayis a timebomb/minefield in it's current form! We should deal with it ASAP even if not in this PR. (Should be done before releasing our next beta.)- Avoiding the usage of 
MemoryManager.Currentis absolutely necessary, but maybe we can leave it for now on less significant points of the code. I know, it's a lot of chore work, but I can join pushing to your branch, if you want! - We should provide test coverage for 
MemoryManager&ArrayPoolMemoryManager. I can do this. 
@JimBobSquarePants @tocsoft thoughts?
| /// </summary> | ||
| public class ArrayPoolMemoryManager : MemoryManager | ||
| { | ||
| private readonly int minSizeBytes; | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if we need minSizeBytes at all. The correct value should be found by benchmarking. I'd say it's an early optimization at this point, and would prefer to drop it for simplicity.
Also see my comment on Configuration.MemoryManager for an explanation about why is small-to-mid array pooling benefitical.
| { | ||
| this.minSizeBytes = minSizeBytes; | ||
| 
               | 
          ||
| this.pool = ArrayPool<byte>.Create(CalculateMaxArrayLength(), 50); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The maximum pool size should be configurable from the outside! (maximumPoolSizeInBytes)
I pretty sure, that lowering the default maximum to more sane value alone would solve most of the Out Of Memory errors users have!
| /// Heuristically calculates a reasonable maxArrayLength value for the backing <see cref="ArrayPool{T}"/>. | ||
| /// </summary> | ||
| /// <returns>The maxArrayLength value</returns> | ||
| internal static int CalculateMaxArrayLength() | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In our new design (backing everyting with byte[] arrays), this should be actually a constant and the default value for the  maximumPoolSizeInBytes parameter. (See my previous comment.)
| /// <returns>The maxArrayLength value</returns> | ||
| internal static int CalculateMaxArrayLength() | ||
| { | ||
| const int MaximumExpectedImageSize = 16384 * 16384; | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My biggest mistake was defining this value that high. It probably costs 100-s of €-s for Azure users :)
I guess a value like 4096 * 4096 would do for now. Should be determined by proper load testing benchmarks later.
| { | ||
| byte[] foo = ArrayPool<byte>.Shared.Rent(count); | ||
| try | ||
| byte[] foo = new byte[count]; | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a typical case, where using ArrayPool is better than newing up an array! See my comment on Configuration.MemoryManager.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed.
| /// <param name="height">The height of the image represented by the pixel buffer.</param> | ||
| public PixelAccessor(int width, int height) | ||
| : this(width, height, Buffer2D<TPixel>.CreateClean(width, height), true) | ||
| : this(width, height, Configuration.Default.MemoryManager.Allocate2D<TPixel>(width, height, true), true) | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately we need to propagate that parameter here :(
It's possible to do this incrementally by defining a temporal construct with an optional memoryManager = null parameter and calling memoryManager ?? Configuration.Default.MemoryManager.Allocate2D<TPixel>(width, height, true).
We should get rid of PixelAccessor anyways, so leave it for now if you want
| this.Length = this.RowStride * height; | ||
| 
               | 
          ||
| this.byteBuffer = MemoryManager.Current.Allocate<byte>(this.Length, true); | ||
| this.byteBuffer = Configuration.Default.MemoryManager.Allocate<byte>(this.Length, true); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you feel the chore work is just too much, leave it then :)
We should get rid of both PixelAccessor<T> and PixelArea<T> anyways. They are legacy constructs.
| Guard.MustBeGreaterThanOrEqualTo(amount.Length, destination.Length, nameof(amount.Length)); | ||
| 
               | 
          ||
| using (Buffer<Vector4> buffer = MemoryManager.Current.Allocate<Vector4>(destination.Length * 3)) | ||
| using (Buffer<Vector4> buffer = Configuration.Default.MemoryManager.Allocate<Vector4>(destination.Length * 3)) | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this is actully forcing us to make API design decisions. I think it should be a parameter of Blend(). @tocsoft thoughts?
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| public int GetMaxCode(int index) | ||
| { | ||
| fixed (int* maxCodesPtr = this.MaxCodes) | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GetMaxCode()  gonna pin/unpin this.MaxCodes several times inside a loop. We need to find a better way. Pinning outside the loop, or applying ref arithmetics with SRCS.Unsafe could do the job.
Do you want me to deal with this?
| /// <summary> | ||
| /// Implements <see cref="MemoryManager"/> by allocating new buffers on every call. | ||
| /// </summary> | ||
| public class NullMemoryManager : MemoryManager | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need to find a better name for this :) NullMemoryManager suggests me, that it's always returning null instead of a memory block with a valid memory address.
@JimBobSquarePants @tocsoft suggestions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SimpleMemoryManager maybe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about ManagedMemoryFactory and PooledMemoryFactory ?
| 
           As far as I understand how this configurable memory manager works, it is designed to set a global memory manager before any other call, and then it uses that memory manager on everything; from requesting memory for image buffers to small streaming arrays. My questions are: 
 If given the choice, I would like to configure ImageSharp like this: 
 Example: In this case, the image's internal buffer would be GC managed, but IImageProcessingContext would have a non static PooledMemoryManager used by all the processors, but GC'ed when the "mutation" is completed. For me this is the ideal behaviour, because in my case it's a combination of performance, ease of use and guaranteeing when I've finished, all memory used by ImageSharp will be GC'ed  | 
    
          
re: different memory usage scenariosThe hard part is the context-bound automatical release of pools after each operation, but maybe we can figure this out. It's very easy however to provide a safe API for manual release of these resources: re: MemoryManager consistence:
 My suggestion (could work with both  public class Configuration
{
    ...
    
    public IMemoryManager MemoryManager 
    {
    	get { return this.memoryManager; } 
    	set 
    	{ 
    		if (this.memoryManager.IsTouched) throw new InvalidOperationException();
    		this.memoryManager = value;
    	}
    }
} | 
    
| 
           @antonfirsov my concern with static configurations is that I'm working with a plugin based system (I've pointed you to it before) in which I can have more than one ImageSharp based plugin loaded at the same time. (In the scenarios I have in mind, there will be lots!) So I have these conflicting scenarios when I have two independently developed plugins being loaded at the same time: 
 This can also happen with third party libraries extending or wrapping imagesharp, and willing to use it with a specific configuration; whenever you try to use libraries from different sources, we can end having conflicting configurations. The only way I can think to solve this is to break the APIs and create objects through non static factory instances, but I guess it's a change too big to be considered.  | 
    
| 
           @vpenades your scenario is exactly the reason why usin   | 
    
| 
           Calling   | 
    
| 
           @antonfirsov maybe the singleton can be dropped if we rename things: instead of calling it "Configuration" let's called "Factory", so the new API would look like this: I think it's quite a huge change, but it's not that ugly. In fact, having the images created from factories would help future works when we require "interop" or file mapped images, which could be supported transparently.  | 
    
| 
           Hey @rytmis! Do you have interest/time to continue working on this? Gonna have time for this in the middle of the next week.  | 
    
| 
           Interest? Sure. Time? Not so sure. Skills? Probably not. :) If you need someone to do boring grunt work, then I'm your guy, obviously -- but I don't think pulling this to a separate branch is necessarily a problem for that, so do whatever is easiest for you. I'm just glad to have helped at least a little bit.  | 
    
| 
           You already helped a lot, thanks! I have some time, so I'll grab it from now then. In order to stick to my personal plans I need to finish it before the end of the next week. Probably gonna ask for some input. Don't talk nonsense about your skills, you're doing cool stuff according to your blog and GH contribution history! 😄  | 
    
| 
           Closed this in favor of #475  | 
    
Prerequisites
Description
This is very much a work in progress -- so far, I've moved Buffer construction to use a singleton MemoryPool with an ArrayPool backed implementation.