为什么我的不安全代码块比安全代码慢?

7

我正在尝试编写一些能够迅速处理视频帧的代码。我接收到的帧是System.Windows.Media.Imaging.WriteableBitmap类型的。为了测试目的,我只是应用了一个简单的阈值过滤器来处理BGRA格式的图像,并根据BGR像素的平均值将每个像素分配为黑色或白色。

以下是我的“安全”版本:

public static void ApplyFilter(WriteableBitmap Bitmap, byte Threshold)
{
    // Let's just make this work for this format
    if (Bitmap.Format != PixelFormats.Bgr24
        && Bitmap.Format != PixelFormats.Bgr32)
    {
        return;
    }

    // Calculate the number of bytes per pixel (should be 4 for this format). 
    var bytesPerPixel = (Bitmap.Format.BitsPerPixel + 7) / 8;

    // Stride is bytes per pixel times the number of pixels.
    // Stride is the byte width of a single rectangle row.
    var stride = Bitmap.PixelWidth * bytesPerPixel;

    // Create a byte array for a the entire size of bitmap.
    var arraySize = stride * Bitmap.PixelHeight;
    var pixelArray = new byte[arraySize];

    // Copy all pixels into the array
    Bitmap.CopyPixels(pixelArray, stride, 0);

    // Loop through array and change pixels to black/white based on threshold
    for (int i = 0; i < pixelArray.Length; i += bytesPerPixel)
    {
        // i=B, i+1=G, i+2=R, i+3=A
        var brightness =
               (byte)((pixelArray[i] + pixelArray[i+1] + pixelArray[i+2]) / 3);

        var toColor = byte.MinValue; // Black

        if (brightness >= Threshold)
        {
            toColor = byte.MaxValue; // White
        }

        pixelArray[i] = toColor;
        pixelArray[i + 1] = toColor;
        pixelArray[i + 2] = toColor;
    }
    Bitmap.WritePixels(
        new Int32Rect(0, 0, Bitmap.PixelWidth, Bitmap.PixelHeight),
        pixelArray, stride, 0
    );
}

我认为以下是使用不安全代码块和WriteableBitmap后缓冲区而非前缓冲区的直接翻译:

public static void ApplyFilterUnsafe(WriteableBitmap Bitmap, byte Threshold)
{
    // Let's just make this work for this format
    if (Bitmap.Format != PixelFormats.Bgr24
        && Bitmap.Format != PixelFormats.Bgr32)
    {
        return;
    }

    var bytesPerPixel = (Bitmap.Format.BitsPerPixel + 7) / 8;

    Bitmap.Lock();

    unsafe
    {
        // Get a pointer to the back buffer.
        byte* pBackBuffer = (byte*)Bitmap.BackBuffer;

        for (int i = 0;
             i < Bitmap.BackBufferStride*Bitmap.PixelHeight;
             i+= bytesPerPixel)
        {
            var pCopy = pBackBuffer;
            var brightness = (byte)((*pBackBuffer
                                     + *++pBackBuffer
                                     + *++pBackBuffer) / 3);
            pBackBuffer++;

            var toColor =
                    brightness >= Threshold ? byte.MaxValue : byte.MinValue;

            *pCopy = toColor;
            *++pCopy = toColor;
            *++pCopy = toColor;                    
        }
    }

    // Bitmap.AddDirtyRect(
    //           new Int32Rect(0,0, Bitmap.PixelWidth, Bitmap.PixelHeight));
    Bitmap.Unlock();

}

这是我第一次尝试不安全代码块和指针,所以逻辑可能不是最优的。

我已经使用相同的WriteableBitmaps测试了两个代码块:

var threshold = Convert.ToByte(op.Result);
var copy2 = copyFrame.Clone();
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
BinaryFilter.ApplyFilterUnsafe(copyFrame, threshold);
stopWatch.Stop();

var unsafesecs = stopWatch.ElapsedMilliseconds;
stopWatch.Reset();
stopWatch.Start();
BinaryFilter.ApplyFilter(copy2, threshold);
stopWatch.Stop();
Debug.WriteLine(string.Format("Unsafe: {1}, Safe: {0}",
                stopWatch.ElapsedMilliseconds, unsafesecs));

所以我正在分析同一张图像。这是一个传入视频帧流的测试运行:

Unsafe: 110, Safe: 53
Unsafe: 136, Safe: 42
Unsafe: 106, Safe: 36
Unsafe: 95, Safe: 43
Unsafe: 98, Safe: 41
Unsafe: 88, Safe: 36
Unsafe: 129, Safe: 65
Unsafe: 100, Safe: 47
Unsafe: 112, Safe: 50
Unsafe: 91, Safe: 33
Unsafe: 118, Safe: 42
Unsafe: 103, Safe: 80
Unsafe: 104, Safe: 34
Unsafe: 101, Safe: 36
Unsafe: 154, Safe: 83
Unsafe: 134, Safe: 46
Unsafe: 113, Safe: 76
Unsafe: 117, Safe: 57
Unsafe: 90, Safe: 41
Unsafe: 156, Safe: 35

为什么我的不安全版本总是更慢?这是因为使用了后备缓冲区吗?还是我做错了什么?
谢谢。

你是在 Release 构建中运行这些测试吗? - Dan Bryant
不,现在这是在调试器构建中。 - Jon Comtois
3
我建议在发布版本中运行性能测试,以获得有意义的结果。如果你让优化器根据代码进行优化,你可能会发现性能差距更小,无法证明使用“不安全”版本的必要性。 - Dan Bryant
3个回答

9
也许是因为您的不安全版本正在进行乘法和属性访问:
Bitmap.BackBufferStride*Bitmap.PixelHeight

在每次循环迭代中,将结果存储在一个变量中。

不确定这是否是问题的根本原因,但它肯定会对其产生影响。 - McAden
1
你说得对!我太专注于尝试弄清楚不安全块和指针的工作原理,以至于忽略了我的迭代器条件逻辑操作。将其存储在变量中确实使我降到了“安全”版本以下,尽管这并没有真正足够成为一个改变游戏规则的因素。如果我还有其他做法是次优的,我非常愿意听取建议,如果没有,再次感谢您的快速修复! - Jon Comtois
2
主要问题不在于乘法,而在于访问属性(BackBufferStride和PixelHeight),这才是真正的瓶颈。我知道你正在使用WPF,但对于Silverlight,我创建了一个包装器来缓存位图的所有属性值,以获得所需的性能。 - Chris Taylor
@Chris - 你说得对。我也修改了我的答案以反映这一点。 - Keltex

5

在安全或不安全的代码中,还有一种优化方法: 停止在循环内部除以3。将阈值乘以3,放在循环外面。您需要使用一些类型而不是byte,但这不应该是问题。实际上,您已经在使用比byte更大的数据类型 :)


+1:这也有所帮助!在您的建议下,我将单个乘法操作移出循环后,执行时间从之前的26-30毫秒降至17-20毫秒范围内。不确定能否再进一步降低执行时间。 - Jon Comtois
@jomtois 我尝试了几种方法...但如果我的测试证明了什么,那就是微优化通常不值得。我尝试使用一些位掩码和类似于 while (pBackBuffer < end) 而不是计数循环的东西。实际上所有这些都降低了性能 :P - Thorarin

0

很难不经过对代码进行分析就下结论,特别是由于代码非常不同(尽管一开始看起来相似),以下是一些关键点(它们只是推测):

如果计算出 if 语句的停止条件,则在 unsafe 版本中停止,而在 safe 中则不计算

  • 数组 pixelArray 的索引可能只被计算一次,尽管它们使用了两次。
  • 即使它们没有“缓存”,将数字相加而不存储它们(与 ++p 相反)仍然会更快(较少的指令和较少的内存访问)
  • 您没有锁定安全版本中的位图
  • pixelArray[i]、pixelArray[i+1] 和 pixelArray[i+2] 可能会存储在局部变量中,使得第二次访问它们比再次迭代指针速度更快。
  • 在 unsafe 代码中有多余的赋值(pCOpy = pBackBuffer)和多余的递增(pBackBuffer++;)

这是我能想到的所有想法。希望这有所帮助。


我怀疑停止条件是最重要的因素,就像上面提到的那样。我会检查一些你提出的其他想法和@Thorarin上面提出的想法。基本上,我想将指针沿着代表像素的4字节块移动。我需要查询和更改块中的前三个字节并跳过第四个字节。我不知道哪些微观优化对此最好。 - Jon Comtois

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接