MagickImage.Format/ExifProfile.GetValue() allocate 2KB/11KB of memory after each call
See original GitHub issueDescription
Hello Devs,
I’ve been profiling a website’s memory and detected an enormous amount of memory allocated by ImageMagick’s MagickImage.Format/ExifProfile.GetValue() methods - new 2KB/11KB added after each call. These members call EnumHelper.Parse() methods, which in its turn call Enum.GetValues()/Enum.GetNames methods. When invoked on MagickFormat enum, Enum.GetValues()/Enum.GetNames return arrays with 267 values which after method execution immediately become memory garbage and intensify GC pressure thus slowing down the website. The extensive memory allocation is also present when invoking EnumHelper.ConvertFlags() due to the same reason - Enum.GetValues().
Steps to Reproduce
The MagickImage.Format allocation can be reproduced on any image, while the ExifProfile.GetValue() allocation is reproducible when executing a benchmark below on any image with Exif data:
` using BenchmarkDotNet.Attributes; using ImageMagick;
namespace Benchmarks.ImageMagick { [MemoryDiagnoser] public class MagickImageBenchmark { private MagickImage _image;
[GlobalSetup]
public void GlobalSetup()
{
_image = new MagickImage(@"..\..\..\..\..\..\..\..\Data\Images\MagickImageBenchmark\station.jpg");
}
// result: mean 5.5 microseconds, allocated 2 KB
[Benchmark]
public MagickFormat BenchmarkImageMagickFormat() => _image.Format;
// result: mean 466.6 microseconds, allocated 11 KB
[Benchmark]
public IExifValue? BenchmarkExifProfileGetValue() => _image.GetExifProfile().GetValue(ExifTag.Orientation);
[GlobalCleanup]
public void GlobalCleanup()
{
_image.Dispose();
}
}
} `
System Configuration
- Magick.NET version: Magick.NET-Q16-x64, 9.0.0
- Environment: Windows 10
Solution
I’ve created a fix to the issue that you can use for free in Magick.NET as is, or modify it before applying. Here is the result of a benchmark that executes the original and fixed version of Parse(string, Enum), Parse(int, Enum), Parse(ushort, Enum), ConvertFlags(Enum) 10 times and measures the execution time and memory allocation.
As you can see the execution time and memory allocations decreased dramatically. The benchmark itself:
` using BenchmarkDotNet.Attributes; using ImageMagick; using ImageMagick.Formats;
namespace Benchmarks.ImageMagick { [MemoryDiagnoser] public class EnumHelperBenchmark { private static readonly string[] Formats = { “Jpg”, “Png”, “WebP”, “Tiff”, “Gif”, “Bmp”, “Svg”, “Avif”, “Psd”, “Pdf” }; private static readonly int[] FormatValues = { 111, 179, 250, 231, 78, 18, 225, 12, 193, 164 };
private static readonly JpegProfileTypes[] ProfileTypes = {
JpegProfileTypes.App, JpegProfileTypes.App | JpegProfileTypes.Exif, JpegProfileTypes.App | JpegProfileTypes.Xmp,
JpegProfileTypes.Icc, JpegProfileTypes.Icc | JpegProfileTypes.Xmp, JpegProfileTypes.Icc | JpegProfileTypes.Iptc,
JpegProfileTypes.Xmp, JpegProfileTypes.Xmp | JpegProfileTypes.Exif, JpegProfileTypes.Xmp | JpegProfileTypes.Iptc,
JpegProfileTypes.App | JpegProfileTypes.Xmp | JpegProfileTypes.Exif | JpegProfileTypes.Icc | JpegProfileTypes.Iptc
};
[Benchmark]
public void BenchmarkOriginalStringEnumParse()
{
foreach (string format in Formats)
{
EnumHelper.Parse(format, MagickFormat.Unknown);
}
}
[Benchmark]
public void BenchmarkImprovedStringEnumParse()
{
foreach (string format in Formats)
{
EnumHelperImproved.Parse(format, MagickFormat.Unknown);
}
}
[Benchmark]
public void BenchmarkOriginalIntEnumParse()
{
foreach (int formatValue in FormatValues)
{
EnumHelper.Parse(formatValue, MagickFormat.Unknown);
}
}
[Benchmark]
public void BenchmarkImprovedIntEnumParse()
{
foreach (int formatValue in FormatValues)
{
EnumHelperImproved.Parse(formatValue, MagickFormat.Unknown);
}
}
[Benchmark]
public void BenchmarkOriginalUshortEnumParse()
{
foreach (int formatValue in FormatValues)
{
EnumHelper.Parse((ushort)formatValue, MagickFormat.Unknown);
}
}
[Benchmark]
public void BenchmarkImprovedUshortEnumParse()
{
foreach (int formatValue in FormatValues)
{
EnumHelperImproved.Parse((ushort)formatValue, MagickFormat.Unknown);
}
}
[Benchmark]
public void BenchmarkOriginalConvertFlags()
{
foreach (JpegProfileTypes profileType in ProfileTypes)
{
EnumHelper.ConvertFlags(profileType);
}
}
[Benchmark]
public void BenchmarkImprovedConvertFlags()
{
foreach (JpegProfileTypes profileType in ProfileTypes)
{
EnumHelperImproved.ConvertFlags(profileType);
}
}
}
} `
Solution details
ConvertFlags() :
- Stores a copy of Enum.GetValues() in a thread-safe cache implemented as ConcurrentDictionary.
- Removes redundant ToArray() call in “string.Join(”,“, flags.ToArray())” code.
Parse(string, Enum) :
- Stores a copy of Enum.GetNames() in a thread-safe cache implemented as ConcurrentDictionary.
- Applies a binary search while seeking for an Enum name in the cache. Note that a cache item is getting sorted before populating the cache in order to be eligible for the binary search.
Parse(int, Enum)/Parse(ushort, Enum) :
- Stores a copy of Enum.GetValues() in a thread-safe cache implemented as ConcurrentDictionary.
- Applies a binary search while seeking for an Enum value in the cache. Note that a cache item is getting sorted before populating the cache in order to be eligible for the binary search.
The fixed version of EnumHelper is here: EnumHelper.cs.txt
And thanks for creating this awesome library!
Issue Analytics
- State:
- Created 2 years ago
- Comments:9 (5 by maintainers)
Those changes have been pushed yesterday and I will try to publish a new release later this week.
Turns out that I can use
ToString()
without removing the spaces works with ImageMagick so this will also be included in the next release.