Repository navigation
Dither and Quantize API and Performance Improvements. - #1138
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1138 +/- ##
==========================================
+ Coverage 82.23% 82.27% +0.03%
==========================================
Files 678 678
Lines 29192 29216 +24
Branches 3284 3278 -6
==========================================
+ Hits 24007 24038 +31
+ Misses 4488 4483 -5
+ Partials 697 695 -2
Continue to review full report at Codecov.
|
|
Is the "all memory in for speed" approach really what we want with Gif? Just pushed a change to also run the benchmark against Note that the theoretical maximum There are alternative approaches that would sacrifice some of the speed but allocate much less. The most naive thing I can think of: binary search, hopefully less than 10x slower, could be done without going down the rabbit hole researching literature. Do you remember how much slower was the linear search without caching? Can't find the PR introducing the cache, must be older than history itself :) |
|
Ok, I realized binary search won't be enough, so I guess we can't find a good solution in a reasonable time :( |
|
Looks like this could be the perfect use case scenario for a |
|
These micro-optimizations do not worth the efforts now IMO, we should rather focus on finding a better algorithm. An efficient k-d tree implementagion may do the job. This one looks promising. We can actually combine it with other techniques. I have an idea now, and I may be able to code it in a couple of hours. But in this PR I'll focus on API review from now on. |
|
Regarding locking vs ConcurrentDictionary: we can get rid of locking entirely, we just need a first pass to fill the dictionary. |
|
@antonfirsov @Sergio0694 I would personally love to see a better implementation. I deliberately kept the Looking forward to seeing what you can come up with! |
antonfirsov
left a comment
There was a problem hiding this comment.
Mostly good, only one remark
|
|
||
| [Theory] | ||
| [WithFile(TestImages.Bmp.Car, PixelTypes.Rgba32)] | ||
| public void EncodeAllocationCheck<TPixel>(TestImageProvider<TPixel> provider) |
There was a problem hiding this comment.
I usually prefer to put these under ImageSharp.Tests/ProfilingBenchmarks, and keep them skipped.
There was a problem hiding this comment.
I didn't actually mean to check that in!
| /// <returns>The <see cref="ReadOnlyMemory{TPixel}"/> palette.</returns> | ||
| ReadOnlyMemory<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds); | ||
| /// <returns>The <see cref="ReadOnlySpan{TPixel}"/> palette.</returns> | ||
| ReadOnlySpan<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds); |
There was a problem hiding this comment.
Can't find any related guideline point, but I have a bad feeling returning a Span as a "result" of an operation, because the ownership of the backing memory is not clear from the API.
I think we should return void here, and publish a separate Palette property instead.
There was a problem hiding this comment.
You've hit upon a point that led to me coding about 5 different version of this.
I struggled with a Palette property for a couple of reasons:
- The result is temporally coupled since it is not assigned on construction. The palette could be null (If using
ReadOnlyMemory<TPixel>) or emptyReadOnlySpan<TPixel> - Using
ReadOnlyMemory<TPixel>can lead to a larger palette length than actually required.
The OctreeFrameQuantizer doesn't return the final number of entries until you build the palette. You have to build it then slice the result reducing the length by the determined index count, returning ReadOnlySpan allowed this.
If you don't do this you can end up with a larger Gif than you actually need. The WuFrameQuantizer is similar, but at least you know the length early on.
I'll give everything on last look over and see if I can improve things.
There was a problem hiding this comment.
The palette could be null (If using
ReadOnlyMemory<TPixel>) or emptyReadOnlySpan<TPixel>
We can also throw an InvalidOperationException if the palette has not yet been built.
Using
ReadOnlyMemory<TPixel>can lead to a larger palette length than actually required. [...] You have to build it then slice the result reducing the length by the determined index count, returningReadOnlySpanallowed this.
You can also slice a ReadOnlyMemory<T>.
This is no strong opinion however. Alternatively we can clarify the ownership model in the docs of BuildPalette() without changing the API.
There was a problem hiding this comment.
You can also slice a ReadOnlyMemory.
Why oh why has that never occurred to me before?
There was a problem hiding this comment.
Quick question....
Would you consider is ok API wise for the QuantizedFrame to reference the FrameQuantizer palette rather than copying it? Currently I pass the span, copy it and use it for the lifetime of the frame.
There was a problem hiding this comment.
Scratch that. It leads to messy code in the Gif Encoder.
Prerequisites
Description
The
IDitherinterface contained implementation specific parameters in the method signatures. This has been simplified to the absolute bare minimum possible parameters. I also managed to remove boxing from the palette dither type.In addition I simplified the
IFrameQuantizerinterface to remove palette reference ambiguity in the API.I noticed some performance problems with the
GifEncoderduring development and as part of my refactoring I tackled some of those issues. Mean performance is around 3-4X faster with 25% less allocations on 2 out of 3 targets. (I have no idea what is so different in NET Core 2.1 memory usage)Note: Allocations are due to usage of
ConcurrentDictionary<TPixel,int>in the pixel map which cannot be avoided. I normalized the concurrency count across target frameworks to at least ensure allocations do not go wild on NETFX.I've introduces some
Unsafe.Addin the dithering and quantization loops but safe in the knowledge that bounds are carefully constrained long before those methods are called.Before
After
A slight fix to the color matching code was implemented also with led to more accurate matching and distribution of error.