DotNet Bitmap Creation Optimization

Dot Net Bitmap Creation

In one of my side project, I am playing with fractals algotrithms to generage images. I am using with this project to explore Async/Await use and parrallelism. The ultimate goal is to use GPU ressources to offload some of the compute extensive tasks. While playing with generation of 500x500 images and 2.000x2.000 images, I realized that the code to load the result into a bitmap was way more expensive than the computation of the underlying matrix. Here is how I optimized the Bitmap creation

First try

My algorithm generates a grid of double. I am then using the double to select a color in an array.

1
2
3
4
5
6
7
    ...
    Bitmap bmp = new Bitmap(width, height);

    for (int x = 0; x < width; x++)
        for (int y = 0; y < height; y++)
            bmp.SetPixel(x, y, myColors[(int)(256 * fractal.Grid[x, y])]);
    ...

Optimized version

This requires the activation of the Unsage compilation in the properties of the project. Parallel.For is used to exploit the parrallelism. The complexity is to process the grid as an array of values. This is easing using the modulo (%) and divide (/) operators.

Code is documented for a better lecture.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
   ...
   Bitmap bmp = new Bitmap(width, height);        
   
   //Create a new instance of BitmapData from the bitmap using LockBits method
   //This gives us read and write access pixel-by-pixel in unsafe mode
   BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadWrite, bmp.PixelFormat);

   //Get a pointer to the first pixel of the bitmap 
   IntPtr ptr = bmpData.Scan0;

   //Use unsafe code to move through the pixels of the bitmap and set each pixel's color 

   unsafe
   {
       //Get a pointer to the first pixel of the bitmap as bytes 
       byte* p = (byte*)ptr.ToPointer();

       //Loop through the pixels in parallel (splitting up work among multiple threads)
       Parallel.For(0, bmpData.Stride * (bmpData.Height) / 4, i =>
       {
           //Calculate the x and y pixel positions based on the index value
           int x = i % width;
           int y = i / width;

           //Get the color index for this pixel from the plasma instance and use it to get the corresponding color from the myColors array
           int index = (int)(256 * plasma.Grid[x, y]);
           Color c = myColors[index];

           //Set the color values for the current pixel in the byte array
           p[i * 4] = c.B;
           p[i * 4 + 1] = c.G;
           p[i * 4 + 2] = c.R;
           p[i * 4 + 3] = c.A;
       });
   }

   //Unlock the bitmap data
   bmp.UnlockBits(bmpData);
   ...

Results

To have a better appreciation, I took the measures 3 times for each tests. The first tests results in 1x1K are not too bad. But the result for 4x4K at the bottom are showing that the improved version is 16 times faster ! And staying below 0.5 s !

AlgoSizeTime GenTime BPM
Not Opti1kx1k18ms 154262ms 218
Not Opti1kx1k13ms 925259ms 556
Not Opti1kx1k15ms 192262ms 743
Opti1kx1k14ms 18159ms 246
Opti1kx1k14ms 78161ms 056
Opti1kx1k14ms 01538ms 031
Not Opti2kx2k61ms 5711s 49ms 074
Not Opti2kx2k63ms 2881s 66ms 635
Not Opti2kx2k62ms 4191s 56ms 018
Opti2kx2k61ms 11987ms 442
Opti2kx2k64ms 18765ms 165
Opti2kx2k62ms 79899ms 169
Not Opti4kx4k237ms 6604s 348ms 471
Not Opti4kx4k237ms 2554s 244ms 536
Not Opti4kx4k237ms 4414s 343ms 525
Opti4kx4k183ms 192255ms 756
Opti4kx4k247ms 003249ms 913
Opti4kx4k231ms 056170ms 567