3

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

    /// <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:我仍然对性能不满意,而且我敢肯定还有更多毫秒的时间。

4

10 回答 10

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 秒。好多了。

于 2009-05-13T15:50:25.103 回答
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# 中的高性能图形的人来说都是一本很好的读物:

绝对值得一看。

于 2009-04-11T18:13:56.360 回答
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(检查),而是创建一个局部变量以防万一。它不会在单次迭代中执行此操作,但可能会在每次迭代中执行。

于 2009-04-11T22:51:21.263 回答
1

你可能想看看Eigen

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

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

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

于 2009-05-09T18:16:01.460 回答
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);
    }
于 2009-05-09T21:57:29.010 回答
1

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

于 2009-05-13T11:41:49.627 回答
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;
    }

}

还没有彻底测试它,但你应该能够让它工作。

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

让我知道这是否有帮助:-)

于 2009-04-11T18:54:26.263 回答
0

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

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

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

于 2009-05-09T23:03:20.940 回答
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 以获得更好的性能。

于 2009-05-11T14:44:44.050 回答
0

好吧,这将非常接近你可以从算法中得到多少毫秒的线,但是去掉对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

于 2009-05-13T13:27:16.583 回答