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.

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:

[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80)]
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 JpegEncoder JpegImageSharpEncoder = 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("ImageResizeBenchmarks.mona_lisa_square.jpg");

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

        await stream.CopyToAsync(SourceStream);
    }

    // [...] 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="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(SourceStream);
    image.Mutate(static x => x.Resize(OutputWidth, OutputHeight));

    await image.SaveAsync(DestinationStream, JpegImageSharpEncoder);
}

# 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);

    await image.WriteAsync(DestinationStream);
}

# NetVips benchmark method

Here is the benchmark code for NetVips:

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

    using var image = NetVips.Image.NewFromStream(SourceStream);
    image.ThumbnailImage(width: OutputWidth, height: OutputHeight)
        .JpegsaveStream(DestinationStream, q: OutputQuality);
}

# SkiaSharp benchmark method

Finally, here is the benchmark code for SkiaSharp:

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

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

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

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

# Benchmark results

Without further ado, here are the benchmark results:

| Method           | Mean      | Error    | StdDev   | Ratio | RatioSD | Allocated | Alloc Ratio |
|----------------- |----------:|---------:|---------:|------:|--------:|----------:|------------:|
| ImageSharpResize |  81.43 ms | 1.477 ms | 2.587 ms |  1.00 |    0.04 | 144.05 KB |        1.00 |
| MagickNetResize  | 121.25 ms | 1.973 ms | 1.749 ms |  1.49 |    0.05 |  99.27 KB |        0.69 |
| NetVipsResize    |  75.11 ms | 0.587 ms | 0.549 ms |  0.92 |    0.03 |  32.72 KB |        0.23 |
| SkiaSharpResize  |  80.91 ms | 1.316 ms | 1.351 ms |  0.99 |    0.03 |   2.62 KB |        0.02 |

For a simple image resize, SkiaSharp allocates by far the least memory. NetVips is the fastest, but SkiaSharp is not far behind. Magick.NET is the slowest, and ImageSharp consumes the most memory.

# 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 its 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.

# Conclusion

With such a significant difference in memory allocation, SkiaSharp is clearly the best library for this use case. If you need more features, ImageSharp, Magick.NET, and NetVips are viable alternatives, each with its own advantages and disadvantages.

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