LockBits性能关键代码

时间:2009-04-11 18:10:31

标签: c# performance image-processing image-manipulation unsafe

我有一个方法需要尽可能快,它使用不安全的内存指针,这是我第一次尝试这种类型的编码,所以我知道它可能更快。

    /// <summary>
    /// Copies bitmapdata from one bitmap to another at a specified point on the output bitmapdata
    /// </summary>
    /// <param name="sourcebtmpdata">The sourcebitmap must be smaller that the destbitmap</param>
    /// <param name="destbtmpdata"></param>
    /// <param name="point">The point on the destination bitmap to draw at</param>
    private static unsafe void CopyBitmapToDest(BitmapData sourcebtmpdata, BitmapData destbtmpdata, Point point)
    {
        // calculate total number of rows to draw.
        var totalRow = Math.Min(
            destbtmpdata.Height - point.Y,
            sourcebtmpdata.Height);


        //loop through each row on the source bitmap and get mem pointers
        //to the source bitmap and dest bitmap
        for (int i = 0; i < totalRow; i++)
        {
            int destRow = point.Y + i;

            //get the pointer to the start of the current pixel "row" on the output image
            byte* destRowPtr = (byte*)destbtmpdata.Scan0 + (destRow * destbtmpdata.Stride);
            //get the pointer to the start of the FIRST pixel row on the source image
            byte* srcRowPtr = (byte*)sourcebtmpdata.Scan0 + (i * sourcebtmpdata.Stride);

            int pointX = point.X;
            //the rowSize is pre-computed before the loop to improve performance
            int rowSize = Math.Min(destbtmpdata.Width - pointX, sourcebtmpdata.Width);
            //for each row each set each pixel
            for (int j = 0; j < rowSize; j++)
            {
                int firstBlueByte = ((pointX + j)*3);

                int srcByte = j *3;
                destRowPtr[(firstBlueByte)] = srcRowPtr[srcByte];
                destRowPtr[(firstBlueByte) + 1] = srcRowPtr[srcByte + 1];
                destRowPtr[(firstBlueByte) + 2] = srcRowPtr[srcByte + 2];
            }


        }
    }

那么有什么办法可以加快速度吗?暂时忽略待办事项,一旦我进行了一些基准性能测量,就会解决问题。

更新:很抱歉,应该提到我使用此而不是Graphics.DrawImage的原因是因为我实现了多线程,因此我无法使用DrawImage。

更新2:我对性能仍然不满意,我确信还有几个ms可以用。

10 个答案:

答案 0 :(得分:4)

代码中存在一些根本性的错误,我认为直到现在我还没注意到。

byte* destRowPtr = (byte*)destbtmpdata.Scan0 + (destRow * destbtmpdata.Stride);

这会获得一个指向目标行的指针,但它不会获取它正在复制的列,旧代码中的列是在rowSize循环中完成的。它现在看起来像:

byte* destRowPtr = (byte*)destbtmpdata.Scan0 + (destRow * destbtmpdata.Stride) + pointX * 3;

所以现在我们有了正确的目标数据指针。现在我们可以摆脱for循环。使用Vilx-Rob中的建议,现在代码如下:

        private static unsafe void CopyBitmapToDestSuperFast(BitmapData sourcebtmpdata, BitmapData destbtmpdata, Point point)
    {
        //calculate total number of rows to copy.
        //using ternary operator instead of Math.Min, few ms faster
        int totalRows = (destbtmpdata.Height - point.Y < sourcebtmpdata.Height) ? destbtmpdata.Height - point.Y : sourcebtmpdata.Height;
        //calculate the width of the image to draw, this cuts off the image
        //if it goes past the width of the destination image
        int rowWidth = (destbtmpdata.Width - point.X < sourcebtmpdata.Width) ? destbtmpdata.Width - point.X : sourcebtmpdata.Width;

        //loop through each row on the source bitmap and get mem pointers
        //to the source bitmap and dest bitmap
        for (int i = 0; i < totalRows; i++)
        {
            int destRow = point.Y + i;

            //get the pointer to the start of the current pixel "row" and column on the output image
            byte* destRowPtr = (byte*)destbtmpdata.Scan0 + (destRow * destbtmpdata.Stride) + point.X * 3;

            //get the pointer to the start of the FIRST pixel row on the source image
            byte* srcRowPtr = (byte*)sourcebtmpdata.Scan0 + (i * sourcebtmpdata.Stride);

            //RtlMoveMemory function
            CopyMemory(new IntPtr(destRowPtr), new IntPtr(srcRowPtr), (uint)rowWidth * 3);

        }
    }

将500x500图像复制到网格中的5000x5000图像50次:00:00:07.9948993秒。现在上面的更改需要00:00:01.8714263秒。好多了。

答案 1 :(得分:2)

嗯......我不确定.NET位图数据格式是否完全与Windows的GDI32功能兼容......

但我学到的前几个Win32 API之一是BitBlt:

BOOL BitBlt(
  HDC hdcDest, 
  int nXDest, 
  int nYDest, 
  int nWidth, 
  int nHeight, 
  HDC hdcSrc, 
  int nXSrc, 
  int nYSrc, 
  DWORD dwRop
);

如果我没记错的话,这是复制数据的最快方式。

这是用于C#和相关使用信息的BitBlt PInvoke签名,对于在C#中使用高性能图形的任何人来说都是一个很好的读物:

绝对值得一看。

答案 2 :(得分:1)

内循环是您想要集中大量时间的地方(但是,要进行测量以确保)

for  (int j = 0; j < sourcebtmpdata.Width; j++)
{
    destRowPtr[(point.X + j) * 3] = srcRowPtr[j * 3];
    destRowPtr[((point.X + j) * 3) + 1] = srcRowPtr[(j * 3) + 1];
    destRowPtr[((point.X + j) * 3) + 2] = srcRowPtr[(j * 3) + 2];
}
  1. 摆脱乘法和数组索引(这是引擎盖下的乘法)并替换为正在递增的指针。

  2. 同上+1,+ 2,增加一个指针。

  3. 可能你的编译器不会继续计算point.X(check),但为了以防万一,请创建一个局部变量。它不会在单次迭代中执行,但可能每次迭代都会执行。

答案 3 :(得分:1)

您可能需要查看Eigen

它是一个C ++模板库,它使用 SSE(2及更高版本)和AltiVec指令集,优雅地回退到非向量化代码

  

快速。 (见基准)    表达式模板允许智能地移除临时值并启用延迟评估,在适当的时候 - 在大多数情况下,Eigen会自动处理这种情况并处理混叠。
  对SSE(2和更高版本)和AltiVec指令集执行显式矢量化,优雅地回退到非矢量化代码。表达式模板允许对整个表达式全局执行这些优化    对于固定大小的对象,可以避免动态内存分配,并在有意义时展开循环    对于大型矩阵,特别注意缓存友好性。

您可以在C ++中实现函数,然后从C#

调用它

答案 4 :(得分:1)

您并不总是需要使用指针来获得良好的速度。这应该在原始的几毫秒内:

        private static void CopyBitmapToDest(BitmapData sourcebtmpdata, BitmapData destbtmpdata, Point point)
    {
        byte[] src = new byte[sourcebtmpdata.Height * sourcebtmpdata.Width * 3];
        int maximum = src.Length;
        byte[] dest = new byte[maximum];
        Marshal.Copy(sourcebtmpdata.Scan0, src, 0, src.Length);
        int pointX = point.X * 3;
        int copyLength = destbtmpdata.Width*3 - pointX;
        int k = pointX + point.Y * sourcebtmpdata.Stride;
        int rowWidth = sourcebtmpdata.Stride;
        while (k<maximum)
        {
            Array.Copy(src,k,dest,k,copyLength);
            k += rowWidth;

        }
        Marshal.Copy(dest, 0, destbtmpdata.Scan0, dest.Length);
    }

答案 5 :(得分:1)

不幸的是我没有时间编写完整的解决方案,但我会考虑使用平台 RtlMoveMemory()函数来整行移动行,而不是逐字节移动。这应该快得多。

答案 6 :(得分:0)

我认为可以提前计算步幅大小和行数限制。

我预先计算了所有乘法,得到以下代码:

private static unsafe void CopyBitmapToDest(BitmapData sourcebtmpdata, BitmapData destbtmpdata, Point point)
{
    //TODO: It is expected that the bitmap PixelFormat is Format24bppRgb but this could change in the future
    const int pixelSize = 3;

    // calculate total number of rows to draw.
    var totalRow = Math.Min(
        destbtmpdata.Height - point.Y,
        sourcebtmpdata.Height);

    var rowSize = Math.Min(
        (destbtmpdata.Width - point.X) * pixelSize,
        sourcebtmpdata.Width * pixelSize);

    // starting point of copy operation
    byte* srcPtr = (byte*)sourcebtmpdata.Scan0;
    byte* destPtr = (byte*)destbtmpdata.Scan0 + point.Y * destbtmpdata.Stride;

    // loop through each row
    for (int i = 0; i < totalRow; i++) {

        // draw the entire row
        for (int j = 0; j < rowSize; j++)
            destPtr[point.X + j] = srcPtr[j];

        // advance each pointer by 1 row
        destPtr += destbtmpdata.Stride;
        srcPtr += sourcebtmpdata.Stride;
    }

}

没有彻底测试过,但你应该能够让它发挥作用。

我已经从循环中删除了乘法运算(改为预先计算)并删除了大多数分支,因此它应该更快一些。

请告诉我这是否有帮助: - )

答案 7 :(得分:0)

我正在查看您的C#代码,我无法识别任何熟悉的内容。这一切看起来都像是一大堆C ++。顺便说一句,看起来DirectX / XNA需要成为你的新朋友。只需2美分。不要杀死信使。

如果你必须依靠CPU来做到这一点:我自己做了一些24位布局优化,我可以告诉你,内存访问速度应该是你的瓶颈。使用SSE3指令进行最快的逐字节访问。这意味着C ++和嵌入式汇编语言。在纯C中,你在大多数机器上的速度会慢30%。

请记住,在这种操作中,现代GPU比CPU快得多。

答案 8 :(得分:0)

我不确定这是否会带来额外的性能,但我在Reflector中看到了很多模式。

所以:

int srcByte = j *3;
destRowPtr[(firstBlueByte)] = srcRowPtr[srcByte];
destRowPtr[(firstBlueByte) + 1] = srcRowPtr[srcByte + 1];
destRowPtr[(firstBlueByte) + 2] = srcRowPtr[srcByte + 2];

变为:

*destRowPtr++ = *srcRowPtr++;
*destRowPtr++ = *srcRowPtr++;
*destRowPtr++ = *srcRowPtr++;

可能需要更多支撑。

如果宽度固定,您可以将整行展开为几百行。 :)

<强>更新

您也可以尝试使用更大的类型,例如Int32或Int64以获得更好的性能。

答案 9 :(得分:0)

好吧,这将非常接近你可以从算法中获得多少ms的行,但是除去对Math.Min的调用并将其替换为三元运算符。

通常,进行库调用需要的时间比自己做的事情要长,我制作了一个简单的测试驱动程序来确认Math.Min.

using System;
using System.Diagnostics;

namespace TestDriver
{
    class Program
    {
        static void Main(string[] args)
        {
            // Start the stopwatch
            if (Stopwatch.IsHighResolution)
            { Console.WriteLine("Using high resolution timer"); }
            else
            { Console.WriteLine("High resolution timer unavailable"); }
            // Test Math.Min for 10000 iterations
            Stopwatch sw = Stopwatch.StartNew();
            for (int ndx = 0; ndx < 10000; ndx++)
            {
                int result = Math.Min(ndx, 5000);
            }
            Console.WriteLine(sw.Elapsed.TotalMilliseconds.ToString("0.0000"));
            // Test trinary operator for 10000 iterations
            sw = Stopwatch.StartNew();
            for (int ndx = 0; ndx < 10000; ndx++)
            {
                int result = (ndx < 5000) ? ndx : 5000;
            }
            Console.WriteLine(sw.Elapsed.TotalMilliseconds.ToString("0.0000"));
            Console.ReadKey();
        }
    }
}

在我的电脑上运行上述内容时的结果,Intel T2400 @ 1.83GHz。此外,请注意结果有一些变化,但通常trinay运算符更快约0.01 ms。这并不多,但是在一个足够大的数据集上它会加起来。

  

使用高分辨率计时器
  0.0539
  0.0402