1

I am running an application which is working with bitmap images. Now I am looking for a fast way to swap the "Red" and "Blue" values of a "Format24bppRgb" bitmap image. In my C# code my first try was to use an unsafe code fragment:

var bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height),
    ImageLockMode.ReadWrite, bmp.PixelFormat);
unsafe
{
    byte* array = (byte*)bmpData.Scan0.ToPointer();
    byte temp;
    for (int x = 0; x < bmp.Width * bmp.Height * 3; x = x + 3) {
        temp = *(array + x + 2);
        *(array + x + 2) = *(array + x);
        *(array + x) = temp;
    }
}

for my used bitmap sizes this takes around 50-70ms. Now I tried to do the work in an external library (based on C++) with a pinvoke call:

[DllImport("ByteSwitch.dll")] 
public static extern IntPtr ChangeRB(IntPtr data, int width, int height);

data = ChangeRB(bmpData.Scan0, bmp.Width, bmp.Height);

Which is defined as follows:

extern "C" __declspec(dllexport) void* ChangeRB(void* xArray, int xHeight, int xWidth);

void* ChangeRB(void* array, int height, int width)
{
    unsigned char* _array = (unsigned char*)array;
    char temp;
    for (int x = 0; x < height * width * 3; x = x + 3)
    {
        temp = _array[x + 2];
        _array[x + 2] = _array[x];
        _array[x] = temp;
    }
    return _array;
}

and this call takes around 1ms! So I cannot explain the huge performance difference here - or is it really the case that the unmanaged pinvoke is so much faster than the "unsafe" code fragment?

DarthQuack
  • 1,254
  • 3
  • 12
  • 22
Franz Gsell
  • 1,425
  • 2
  • 13
  • 22
  • Think of the Managed/unmanaged boundary as just that, a boundary. Crossing that boundary has a cost. In your first case, you are crossing that boundary over and over in a loop. In the P/Invoke case, you are crossing that boundary, doing the work on the other side and then returning back. – Flydog57 Dec 22 '20 at 19:05
  • You surely don't need unsafe or pinvoke here. There must be a good way to do this in pure C#. – David Heffernan Dec 22 '20 at 19:12
  • @DavidHeffernan: If there is a good way to do this in pure C# I would appreciate any suggestion. – Franz Gsell Dec 22 '20 at 20:29
  • @Flydog57: I guess in the first case there is only one boundary to cross, because the loop is inside the unsafe block? – Franz Gsell Dec 22 '20 at 20:30
  • Take a look at https://stackoverflow.com/questions/1563038/fast-work-with-bitmaps-in-c-sharp – Flydog57 Dec 22 '20 at 21:03
  • @Flydog57: And that's exactly what I am using – Franz Gsell Dec 22 '20 at 22:36
  • 1
    Are you sure you're measuring the right thing? You have to run the c# version in release and without the debugger attached. – MineR Dec 22 '20 at 23:00
  • 1
    Are you testing without the debugger attached (ctrl f5 in visual studio)? It slows quite much the execution of .net code – xanatos Dec 23 '20 at 01:33
  • @MineR: I did all my testing without debugger attached just Trace.Writeline with DebugView and running the application without Visual Studio running – Franz Gsell Dec 23 '20 at 15:56

1 Answers1

6

The performance problem doesn't come from interop nor from C#, it comes from the fact you use bitmap's Width and Height in the loop. Both internally call a GDI Plus API:

public int Width {
    get {
        int width; 
 
        int status = SafeNativeMethods.Gdip.GdipGetImageWidth(new HandleRef(this, nativeImage), out width);
 
        if (status != SafeNativeMethods.Gdip.Ok)
            throw SafeNativeMethods.Gdip.StatusException(status);
 
        return width;
    }
}

And note you don't do this in the C/C++ case... you pass precomputed height and width. So, if you change the C# version for this:

unsafe
{
    byte* array = (byte*)bmpData.Scan0.ToPointer();
    byte temp;
    var max = bmp.Width * bmp.Height * 3;
    for (int x = 0; x < max; x = x + 3) {
        temp = *(array + x + 2);
        *(array + x + 2) = *(array + x);
        *(array + x) = temp;
    }
}

It may run globally even faster. You can also use a safe version like this:

private static void ChangeSafe(Bitmap bmp, BitmapData bmpData)
{
    var array = bmpData.Scan0;
    byte temp;
    var max = bmp.Width * bmp.Height * 3;
    for (var x = 0; x < max; x = x + 3)
    {
        temp = Marshal.ReadByte(array + x + 2);
        Marshal.WriteByte(array + x + 2, Marshal.ReadByte(array + x));
        Marshal.WriteByte(array + x, temp);
    }
}

It's marginally slower but avoids the need for unsafe code.

Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
  • You are the man - it was actually the width and height. And in addition - thanks for the "safe" version as well – Franz Gsell Dec 23 '20 at 20:20