Featured image of post Benchmarking .NET libraries for image resizing: which performs best?

Benchmarking .NET libraries for image resizing: which performs best?

Which .NET library is fastest at image resizing? Which one uses the least memory? A benchmark comparison of ImageSharp, Magick.NET, NetVips, and SkiaSharp.

In this article, we will compare the performance of four .NET libraries for image resizing and determine which one is the fastest and most efficient for this task. The libraries we will compare are:

First, I will explain the benchmark setup. Then, I will present the code and results obtained for each library. Before concluding, I will provide more details about each library to help you make an informed decision for your project.

The goal of this article is to compare the performance of these libraries for the specific task of image resizing. The best-performing library here might not be the best for other image processing tasks.

The complete code is available on GitHub.

Benchmark setup

The benchmarks are implemented using BenchmarkDotNet v0.14.0. Here are the constraints and parameters used for the benchmarks:

  • We will resize a 1374x1374 pixel image of Mona Lisa to 256x256 pixels.
  • The input image format is JPEG. The output will also be in JPEG with a quality level of 75.
  • To reduce byte array allocations, we will reuse pre-allocated MemoryStream objects for inputs and outputs.
  • To minimize external factors, certain library-specific variables will be reused for each benchmark. No file system access will be measured.
  • The project will use .NET 8.0, and the garbage collector will be set to server mode, representing the majority of production use cases.
  • We will measure execution time as well as the memory allocated for each library.
  • We will remove metadata from the images to preserve consistency across libraries.

I’m using Windows:

BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.4169/23H2/2023Update/SunValley3)
AMD Ryzen 5 3600, 1 CPU, 12 logical and 6 physical cores
.NET SDK 8.0.400
  [Host]   : .NET 8.0.8 (8.0.824.36612), X64 RyuJIT AVX2
  .NET 8.0 : .NET 8.0.8 (8.0.824.36612), X64 RyuJIT AVX2

Preparing the benchmark data

We will use streams to prevent the allocation of byte arrays resulting from resizing. To avoid these streams being marked as closed, we will wrap them in a NonClosableMemoryStream type that does not implement the Dispose method:

private sealed class NonClosableMemoryStream : Stream
{
    private readonly MemoryStream _underlyingStream;

    public NonClosableMemoryStream()
    {
        this._underlyingStream = new MemoryStream();
    }

    public NonClosableMemoryStream(int capacity)
    {
        this._underlyingStream = new MemoryStream(capacity);
    }

    // [...] Delegate all methods and properties to the underlying memory stream

    protected override void Dispose(bool disposing)
    {
        // No-op
    }
}

The Mona Lisa image is included in the project as an embedded resource. It is loaded into an instance of NonClosableMemoryStream. The destination stream is also initialized with enough capacity to hold the resized image:

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnostics.Windows.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;
using ImageMagick;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Jpeg;
using SkiaSharp;

BenchmarkRunner.Run<ImageResizeBenchmarks>(args: args);

[ShortRunJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
[NativeMemoryProfiler]
[HideColumns("Error", "StdDev", "RatioSD")]
public class ImageResizeBenchmarks
{
    // Keep the underlying streams open for reuse in consecutive benchmarks
    private static readonly NonClosableMemoryStream SourceStream = new NonClosableMemoryStream();

    // Pre-allocate a large buffer to avoid resizing during the benchmark
    private static readonly NonClosableMemoryStream DestinationStream = new NonClosableMemoryStream(capacity: 4 * 1024 * 1024);

    // Ensure consistent output quality across all libraries
    private const int OutputQuality = 75;

    private static readonly DecoderOptions ImageSharpDecoderOptions = new DecoderOptions
    {
        TargetSize = new Size(OutputWidth, OutputHeight),
    };

    private static readonly JpegEncoder ImageSharpJpegEncoder = new JpegEncoder
    {
        Quality = OutputQuality,
    };

    private const int OutputWidth = 256;
    private const int OutputHeight = 256;

    private static readonly MagickGeometry MagickOutputSize = new MagickGeometry(OutputWidth, OutputHeight);

    [GlobalSetup]
    public async Task GlobalSetup()
    {
        await using var stream = typeof(Program).Assembly.GetManifestResourceStream("Benchmarks.mona_lisa_square.jpg");

        if (stream == null)
        {
            throw new InvalidOperationException("Resource not found.");
        }

        await stream.CopyToAsync(SourceStream);

        // Disable libvips operations cache
        // https://www.libvips.org/API/current/How-it-works.html#:~:text=Operation%20cache
        NetVips.Cache.Max = 0;
    }

    // [...] Benchmark methods

    private static void ResetStreams()
    {
        SourceStream.Seek(0, SeekOrigin.Begin);
        DestinationStream.Seek(0, SeekOrigin.Begin);
    }
}

Installing the libraries

The benchmark project references the following NuGet packages:

<PackageReference Include="BenchmarkDotNet" Version="0.14.0"/>
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.14.0" />
<PackageReference Include="Magick.NET-Q8-x64" Version="14.0.0"/>
<PackageReference Include="NetVips" Version="2.4.1"/>
<PackageReference Include="NetVips.Native" Version="8.15.3"/>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5"/>
<PackageReference Include="SkiaSharp" Version="2.88.8"/>

ImageSharp benchmark method

We will start with ImageSharp, which will be our baseline. Here is the benchmark code:

[Benchmark(Baseline = true)]
public async Task ImageSharpResize()
{
    ResetStreams();

    using var image = await Image.LoadAsync(ImageSharpDecoderOptions, SourceStream);

    image.Metadata.ExifProfile = null;
    image.Metadata.IptcProfile = null;
    image.Metadata.XmpProfile = null;

    await image.SaveAsync(DestinationStream, ImageSharpJpegEncoder);
}

Magick.NET benchmark method

Here is the benchmark code for Magick.NET:

public async Task MagickNetResize()
{
    ResetStreams();

    using var image = new MagickImage(SourceStream);

    image.Quality = OutputQuality;
    image.Resize(MagickOutputSize);
    image.Strip();

    await image.WriteAsync(DestinationStream);
}

NetVips benchmark method

Here is the benchmark code for NetVips:

[Benchmark]
public void NetVipsResize()
{
    ResetStreams();

    using var resized = NetVips.Image.ThumbnailStream(SourceStream, width: OutputWidth, height: OutputHeight);
    resized.JpegsaveStream(DestinationStream, q: OutputQuality, keep: NetVips.Enums.ForeignKeep.Icc);
}

SkiaSharp benchmark method

Finally, here is the benchmark code for SkiaSharp:

[Benchmark]
public void SkiaSharpResize()
{
    ResetStreams();

    using var image = SKBitmap.Decode(SourceStream);
    using var resized = new SKBitmap(OutputWidth, OutputHeight);
    using var canvas = new SKCanvas(resized);

    canvas.DrawBitmap(image, new SKRect(0, 0, OutputWidth, OutputHeight));

    resized.Encode(DestinationStream, SKEncodedImageFormat.Jpeg, OutputQuality);
}

Benchmark results

Without further ado, here are the benchmark results:

| Method           | Mean      | Ratio | Allocated native memory | Native memory leak | Allocated | Alloc Ratio |
|----------------- |----------:|------:|------------------------:|-------------------:|----------:|------------:|
| ImageSharpResize |  78.72 ms |  1.00 |                       - |                  - | 138.85 KB |        1.00 |
| MagickNetResize  | 122.31 ms |  1.55 |               19,343 KB |               0 KB |   50.5 KB |        0.36 |
| NetVipsResize    |  64.95 ms |  0.83 |                  680 KB |               1 KB |  14.91 KB |        0.11 |
| SkiaSharpResize  |  79.90 ms |  1.02 |               26,404 KB |               0 KB |   2.74 KB |        0.02 |

For a simple image resize, SkiaSharp shows the smallest managed memory allocation, though it does consume significant native memory. NetVips is the fastest, while SkiaSharp and ImageSharp have similar speed. Magick.NET is the slowest, and ImageSharp has the highest managed memory usage, which is still impressive considering it’s fully managed with no native dependencies or interop code.

Considerations

It is important to note that these benchmarks are not exhaustive, and the results may vary depending on the size and format of the images as well as the operations performed. It is recommended to test the libraries with real-world use cases before making a decision.

SkiaSharp is not an image processing library but rather a rendering library, using native code from Skia, the graphics engine used by Google Chrome, Android, and MAUI. SkiaSharp benefits from hardware acceleration for rendering, which explains the low managed memory usage and overall good performance.

ImageSharp, Magick.NET, and NetVips are image processing libraries that offer significantly more functionality and customization than SkiaSharp. They provide greater control over the final image quality, format, and metadata. For example, you can specify the interpolation method (e.g., Lanczos, Bicubic) when resizing images.

Since June 2022, ImageSharp requires a commercial license if your annual gross revenue is greater than or equal to 1M USD and you are using any of the libraries for Closed Source software as a “Direct Package Dependency.”

ImageSharp and Magick.NET use asynchronous methods for writing images, which can be advantageous if you are writing the results directly to a file system or an ASP.NET Core HTTP response.

NetVips and SkiaSharp are not asynchronous; it may be beneficial to run the transformations on the thread pool to avoid making applications unresponsive or to improve throughput.

Additionally, some libraries may preserve image metadata by default (EXIF, IPTC, and XMP), which should be considered for removal to protect privacy or reduce file size.

Conclusion

In conclusion, the best library depends on your needs. ImageSharp offers more control over output quality but may require a license, while SkiaSharp minimizes managed memory at the cost of native usage. NetVips is the fastest. Keep in mind that memory usage and speed may vary based on the operating system and hardware. Each library has trade-offs in performance, memory, and output control, so choose based on your priorities.

Special thanks

Thanks to Paulo Morgado for pointing out that I had initially not monitored unmanaged memory in the benchmarks. I have updated the article accordingly.

Thanks to Kleis Auke Wolthuizen, NetVips maintainer, for correcting my use of NetVips in the benchmarks.

Thanks to James Jackson-South, creator of ImageSharp, for providing a fastest benchmark implementation.

Licensed under CC BY 4.0
Ko-fi donations Buy me a coffee