Skip to content

Conversation

@JimBobSquarePants
Copy link
Member

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

  • Inverts the call direction of all transforms to allow inlining and improve performance.
  • Fix invisible pixel bleed in transforms.
  • Converts IResampler implementations into readonly struct

Bug Fixes

The attached image shows the difference in output following the updates. RGB color information no longer leaks beyond the bounds of the transformed result.

rotate-compare

Benchmarks

Massive performance improvements in Affine and Projective transforms.

Rotate

Before

Method Runtime Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
DoRotate .NET 4.7.2 64.96 ms 47.74 ms 2.617 ms - - - 11.56 KB
DoRotate .NET Core 2.1 38.57 ms 14.38 ms 0.788 ms - - - 4.36 KB
DoRotate .NET Core 3.1 38.67 ms 50.52 ms 2.769 ms - - - 5.68 KB

After

Method Runtime Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
DoRotate .NET 4.7.2 28.77 ms 3.304 ms 0.181 ms - - - 6.5 KB
DoRotate .NET Core 2.1 16.27 ms 1.044 ms 0.057 ms - - - 5.25 KB
DoRotate .NET Core 3.1 17.12 ms 4.352 ms 0.239 ms - - - 6.57 KB

Skew

Before

Method Runtime Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
DoSkew .NET 4.7.2 42.78 ms 11.455 ms 0.628 ms - - - 9.33 KB
DoSkew .NET Core 2.1 27.71 ms 3.880 ms 0.213 ms - - - 4.34 KB
DoSkew .NET Core 3.1 25.06 ms 6.461 ms 0.354 ms - - - 5.63 KB

After

Method Runtime Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
DoSkew .NET 4.7.2 24.60 ms 33.971 ms 1.862 ms - - - 6.5 KB
DoSkew .NET Core 2.1 12.13 ms 2.256 ms 0.124 ms - - - 5.21 KB
DoSkew .NET Core 3.1 12.83 ms 1.442 ms 0.079 ms - - - 6.57 KB

Resize

Resize improvements are less dramatic which is expected. NET Core 3.1 shows a nice improvement though bringing it inline with NETFX. A slight improvement overall.

Before

Method Runtime SourceToDest Mean Error StdDev Ratio RatioSD Gen 0 Gen 1 Gen 2 Allocated
SystemDrawing .NET 4.7.2 3032-400 82.97 ms 5.478 ms 0.300 ms 1.00 0.00 - - - 1170 B
'ImageSharp, MaxDegreeOfParallelism = 1' .NET 4.7.2 3032-400 51.53 ms 3.713 ms 0.203 ms 0.62 0.00 - - - 16384 B
SystemDrawing .NET Core 2.1 3032-400 82.83 ms 2.049 ms 0.112 ms 1.00 0.00 - - - 96 B
'ImageSharp, MaxDegreeOfParallelism = 1' .NET Core 2.1 3032-400 62.95 ms 5.588 ms 0.306 ms 0.76 0.00 - - - 15560 B
SystemDrawing .NET Core 3.1 3032-400 86.34 ms 80.550 ms 4.415 ms 1.00 0.00 - - - 96 B
'ImageSharp, MaxDegreeOfParallelism = 1' .NET Core 3.1 3032-400 57.54 ms 86.545 ms 4.744 ms 0.67 0.08 - - - 15536 B

After

Method Runtime SourceToDest Mean Error StdDev Ratio RatioSD Gen 0 Gen 1 Gen 2 Allocated
SystemDrawing .NET 4.7.2 3032-400 80.61 ms 11.068 ms 0.607 ms 1.00 0.00 - - - 1365 B
'ImageSharp, MaxDegreeOfParallelism = 1' .NET 4.7.2 3032-400 49.10 ms 3.663 ms 0.201 ms 0.61 0.00 - - - 21372 B
SystemDrawing .NET Core 2.1 3032-400 80.30 ms 13.570 ms 0.744 ms 1.00 0.00 - - - 96 B
'ImageSharp, MaxDegreeOfParallelism = 1' .NET Core 2.1 3032-400 61.99 ms 20.643 ms 1.132 ms 0.77 0.01 - - - 19816 B
SystemDrawing .NET Core 3.1 3032-400 81.54 ms 49.267 ms 2.701 ms 1.00 0.00 - - - 96 B
'ImageSharp, MaxDegreeOfParallelism = 1' .NET Core 3.1 3032-400 49.75 ms 0.764 ms 0.042 ms 0.61 0.02 - - - 19868 B

@codecov
Copy link

codecov bot commented Feb 21, 2020

Codecov Report

Merging #1118 into master will increase coverage by 0.01%.
The diff coverage is n/a.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1118      +/-   ##
==========================================
+ Coverage   82.21%   82.22%   +0.01%     
==========================================
  Files         678      678              
  Lines       29118    29117       -1     
  Branches     3300     3300              
==========================================
+ Hits        23938    23942       +4     
+ Misses       4484     4480       -4     
+ Partials      696      695       -1
Flag Coverage Δ
#unittests 82.22% <ø> (+0.01%) ⬆️
Impacted Files Coverage Δ
src/ImageSharp/Formats/Png/PngDecoderCore.cs 89.12% <ø> (+1.02%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update b25e102...c1b4bbd. Read the comment docs.

Copy link
Member

@antonfirsov antonfirsov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did not have the chance for an extensive review yet, sharing a few comments and API-related concerns for now.

public int StartIndex { get; }
public int StartIndex
{
[MethodImpl(InliningOptions.ShortMethod)]
Copy link
Member

@antonfirsov antonfirsov Feb 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the JIT is smart enough to do this without our intervention. I see the BCL code pushing back a lot on applications for explicit AggressiveInlining these days, I suppose the JIT does it more reliably with really short methods now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's more about expressing intent than anything. The JIT can ignore our suggestion but I believe we should still declare that we want the method inlined.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm that makes sense, but in trivial cases like property getters it gets overly verbose and hurts readability IMO. (Technically all autoproperies should be marked then.)

@antonfirsov
Copy link
Member

I think I have an idea, will prototype it in a few days (and also do a more comprehensive review here).

@JimBobSquarePants
Copy link
Member Author

@antonfirsov I just had another crack at this.

I've introduced a new interface IResamplingImageProcessor<TPixel>

    /// <summary>
    /// Implements an algorithm to alter the pixels of an image via a resampling transforms.
    /// </summary>
    /// <typeparam name="TPixel">The pixel format.</typeparam>
    public interface IResamplingImageProcessor<TPixel> : IImageProcessor<TPixel>
        where TPixel : struct, IPixel<TPixel>
    {
        /// <summary>
        /// Applies a resampling transform with the given sampler.
        /// </summary>
        /// <typeparam name="TResampler">The type of sampler.</typeparam>
        /// <param name="sampler">The sampler to use.</param>
        void ApplyTransform<TResampler>(in TResampler sampler)
            where TResampler : struct, IResampler;
    }

IResampler now looks like this.

    /// <summary>
    /// Encapsulates an interpolation algorithm for resampling images.
    /// </summary>
    public interface IResampler
    {
        /// <summary>
        /// Gets the radius in which to sample pixels.
        /// </summary>
        float Radius { get; }

        /// <summary>
        /// Gets the result of the interpolation algorithm.
        /// </summary>
        /// <param name="x">The value to process.</param>
        /// <returns>
        /// The <see cref="float"/>
        /// </returns>
        float GetValue(float x);

        /// <summary>
        /// Applies a transformation upon an image.
        /// </summary>
        /// <typeparam name="TPixel">The pixel format.</typeparam>
        /// <param name="processor">The transforming image processor.</param>
        void ApplyTransform<TPixel>(IResamplingImageProcessor<TPixel> processor)
            where TPixel : struct, IPixel<TPixel>;
    }

With implementations like this.

    /// <summary>
    /// The function implements the nearest neighbor algorithm. This uses an unscaled filter
    /// which will select the closest pixel to the new pixels position.
    /// </summary>
    public readonly struct NearestNeighborResampler : IResampler
    {
        /// <inheritdoc/>
        public float Radius => 1;

        /// <inheritdoc/>
        [MethodImpl(InliningOptions.ShortMethod)]
        public float GetValue(float x) => x;

        /// <inheritdoc/>
        [MethodImpl(InliningOptions.ShortMethod)]
        public void ApplyTransform<TPixel>(IResamplingImageProcessor<TPixel> processor)
            where TPixel : struct, IPixel<TPixel>
            => processor.ApplyTransform(in this);
    }

Everything else is now internal. I moved things about a little to reorganize also.

Performance remains the same as the benchmarks.

@hypeartist
Copy link

    /// <summary>
    /// Implements an algorithm to alter the pixels of an image via a resampling transforms.
    /// </summary>
    /// <typeparam name="TPixel">The pixel format.</typeparam>
    public interface IResamplingImageProcessor<TPixel> : IImageProcessor<TPixel>
        where TPixel : struct, IPixel<TPixel>
    {
        /// <summary>
        /// Applies a resampling transform with the given sampler.
        /// </summary>
        /// <typeparam name="TResampler">The type of sampler.</typeparam>
        /// <param name="sampler">The sampler to use.</param>
        void ApplyTransform<TResampler>(in TResampler sampler)
            where TResampler : struct, IResampler;
    }

Btw, if I am not mistaken, the method void ApplyTransform<TResampler> won't be inlined even in case the interface IResamplingImageProcessor<TPixel> is implemented by struct due some JIT limitations. I guess @jkotas could clarify this.

@JimBobSquarePants
Copy link
Member Author

I’m not so concerned by that method being inlined (it would be nice). Much more important is that the TResampler struct is concrete as that’s what all the actual convolution hot paths use.

@antonfirsov
Copy link
Member

@JimBobSquarePants the IResamplingImageProcessor<TPixel> trick is great, actually better than the stuff I wanted, we should apply it to the ditherers as well in a follow-up PR.

@JimBobSquarePants
Copy link
Member Author

Imma gonna merge this.

@Sergio0694
Copy link
Member

1287286260059

Copy link
Member

@antonfirsov antonfirsov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Looks like I spotted everything in during my previous lazy reviews.

@JimBobSquarePants JimBobSquarePants merged commit 9f59241 into master Feb 26, 2020
@JimBobSquarePants JimBobSquarePants deleted the js/faster-transforms branch February 26, 2020 22:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants