4

I am unable to get any speed gains when scaling several Bitmap in parallel.

Please review the following code that demonstrate the issue:

class Program
{
    static void Main(string[] args)
    {
        Bitmap[] images = Enumerable.Range(0, 8).Select(_ => new Bitmap(6000, 4000, PixelFormat.Format24bppRgb)).ToArray();
        Bitmap[] scaledImages = Enumerable.Range(0, 8).Select(_ => new Bitmap(600, 400, PixelFormat.Format24bppRgb)).ToArray();

        // Sequential image scaling:
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        for(int i = 0; i < images.Length; i++)
        {
            ImageScaler(images[i], scaledImages[i]);
        }
        stopwatch.Stop();
        Console.WriteLine($"Sequential scaling: {images.Length} images in {stopwatch.ElapsedMilliseconds} ms.");

        // Parallel image scaling:
        stopwatch.Restart();
        Parallel.For(0, images.Length, i => ImageScaler(images[i], scaledImages[i]));
        stopwatch.Stop();
        Console.WriteLine($"Parallel scaling: {images.Length} images in {stopwatch.ElapsedMilliseconds} ms.");

        Console.ReadKey();
    }

    private static void ImageScaler(Bitmap source, Bitmap destination)
    {
        using(Graphics g = Graphics.FromImage(destination))
        {
            g.InterpolationMode = InterpolationMode.HighQualityBicubic;
            g.DrawImage(source, 0, 0, destination.Width, destination.Height);
        }
    }
}

I get the following results on my system (a recent quad-core i7):

Sequential scaling: 8 images in 1774 ms.
Parallel scaling: 8 images in 1792 ms.

This is an unexpected result for two reasons:

  • The images are fairly large (24 megapixel). The overhead of starting multiple threads should not have a significant impact.
  • Image scaling is definitely a CPU bound operation.

To verify that, I replaced ImageScaler() in the code above by the following basic image scaling algorithm:

private static unsafe void NaiveImageScaler(Bitmap src, Bitmap dst)
{
    BitmapData srcBd = src.LockBits(new Rectangle(0, 0, src.Width, src.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
    BitmapData dstBd = dst.LockBits(new Rectangle(0, 0, dst.Width, dst.Height), ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
    unsafe
    {
        byte* srcBgr = (byte*)srcBd.Scan0.ToPointer();
        byte* dstBgr = (byte*)dstBd.Scan0.ToPointer();
        for(int yd=0; yd<dst.Height; yd++)
        {
            for(int xd = 0; xd < dst.Width; xd++)
            {
                int bSum = 0, gSum = 0, rSum = 0;
                for(int ys = 10*yd; ys < 10*yd+10; ys++)
                {
                    for(int xs = 10*xd; xs < 10*xd+10; xs++)
                    {
                        bSum += srcBgr[ys * srcBd.Stride + 3 * xs];
                        gSum += srcBgr[ys * srcBd.Stride + 3 * xs + 1];
                        rSum += srcBgr[ys * srcBd.Stride + 3 * xs + 2];
                    }
                }
                dstBgr[yd * dstBd.Stride + 3 * xd] = (Byte)(bSum / 100);
                dstBgr[yd * dstBd.Stride + 3 * xd + 1] = (Byte)(bSum / 100);
                dstBgr[yd * dstBd.Stride + 3 * xd + 2] = (Byte)(bSum / 100);
            }
        }
    }
    dst.UnlockBits(dstBd);
    src.UnlockBits(srcBd);
}

With this code, I get the following results:

Sequential scaling: 8 images in 660 ms.
Parallel scaling: 8 images in 184 ms.

The parallel implementation is now 3.6 time faster than the sequential one.

Why the discrepancy between the two approach that accomplish essentially the same work? Is there a limitation in System.Drawing that prevents running parallel code?

Comments about the "Marked as duplicate":

I do not think that Multithreading System.Windows.Graphics is an exact duplicate. The referred question is about drawing, this one is about image scaling. The subjects are related but are not exactly the same.

I do agree that the underlying explanation (critical regions in GDI) applies to the two questions.

I believe that I have found a closer duplicate to my own question(!) : Parallelizing GDI+ Image Resizing .net

AntoineC
  • 460
  • 4
  • 11
  • This migh interest you (while not directly being an answer to your question): [Rendering fast with GDI+ - What to do and what not to do!](https://www.codeproject.com/Tips/66909/Rendering-fast-with-GDI-What-to-do-and-what-not-to) – Olivier Jacot-Descombes Nov 26 '17 at 17:58
  • This SO thread is probably the answer you are looking for: [Multithreading System.Windows.Graphics](https://stackoverflow.com/q/18298525/880990). – Olivier Jacot-Descombes Nov 26 '17 at 18:01

0 Answers0