【问题标题】:Fastest way to copy data from ReadOnlySpan to output with pixel conversion通过像素转换将数据从 ReadOnlySpan 复制到输出的最快方法
【发布时间】:2022-01-26 04:22:51
【问题描述】:

当我使用 (Loops like 'for') 将数据从输入 (ReadOnlySpan) 复制到输出 (Span) 时遇到性能问题

Span.CopyTo,非常完美而且速度非常快 但是现在如果不转换像素就没用了。

下面是代码,我觉得有一些简短的方法可以代替当前的过程:

public unsafe void UpdateFromOutput(CanvasDevice device, ReadOnlySpan<byte> data, uint width, uint height, uint pitch)
{
  using (var renderTargetMap = new BitmapMap(device, RenderTarget))
  {
   var inputPitch = (int)pitch;
   var mapPitch = (int)renderTargetMap.PitchBytes;
                       
   var mapData = new Span<byte>(new  IntPtr(renderTargetMap.Data).ToPointer(), (int)RenderTarget.Size.Height * mapPitch);

   switch (CurrentPixelFormat)
   {
    case PixelFormats.RGB0555:
      FramebufferConverter.ConvertFrameBufferRGB0555ToXRGB8888(width, height, data, inputPitch, mapData, mapPitch);
    break;

    case PixelFormats.RGB565:
      FramebufferConverter.ConvertFrameBufferRGB565ToXRGB8888(width, height, data, inputPitch, mapData, mapPitch);
    break; 
   }

  }
}

然后像 ConvertFrameBufferRGB0555ToXRGB8888

之类的内部函数

我会像下面这样遍历宽度和高度:

var castInput = MemoryMarshal.Cast<byte, ushort>(input);
var castInputPitch = inputPitch / sizeof(ushort);
var castOutput = MemoryMarshal.Cast<byte, uint>(output);
var castOutputPitch = outputPitch / sizeof(uint);
castOutput.Fill(0);

 for (var i = 0; i < height;i++)
 {
   var inputLine = castInput.Slice(i * castInputPitch, castInputPitch);
   var outputLine = castOutput.Slice(i * castOutputPitch, castOutputPitch);

    for (var j = 0; j < width;j++)
    {
     outputLine[j] = ConverToRGB888(inputLine[j]);
    }
 }

上面的代码可以运行,但在某些情况下速度很慢。

请注意:我正在修改一个项目,所以上面的代码是由原始开发人员编写的,我需要帮助,因为我不明白这个过程是如何工作的,仍然很困惑..特别是在 Slice 部分。

尝试仅测试将输入直接复制到输出data.CopyTo(mapData);,我得到了这个(如预期的那样):

希望有一些使用 Marshal 和 Span 函数的解决方案

非常感谢。

关于 (ConverToRGB888) 的更新

至于ConverToRGB888,原代码包含RGB565LookupTable

private const uint LookupTableSize = ushort.MaxValue + 1;
private static uint[] RGB565LookupTable = new uint[LookupTableSize];

public static void SetRGB0565LookupTable()
{
  uint r565, g565, b565;

  double red = 255.0;
  double green = 255.0;
  double blue = 255.0;

  for (uint i = 0; i < LookupTableSize; i++)
  {
     //RGB565
     r565 = (i >> 11) & 0x1F;
     g565 = (i >> 5) & 0x3F;
     b565 = (i & 0x1F);

     r565 = (uint)Math.Round(r565 * red / 31.0);
     g565 = (uint)Math.Round(g565 * green / 63.0);
     b565 = (uint)Math.Round(b565 * blue / 31.0);

     RGB565LookupTable[i] = (0xFF000000 | r565 << 16 | g565 << 8 | b565);
   }
}

private static uint ConverToRGB888(ushort x)
{
  return RGB565LookupTable[x];
}

SetRGB0565LookupTable() 只会被调用一次来填充值。

结论:

  • Fill(0) 不重要,导致延迟
  • 不安全的版本(接受的答案)对我来说很清楚,而且速度有点快
  • 部分避免 For 甚至更快,例如 Here [已测试]
  • 预查找表非常有用,可以加快转换速度
  • Span.CopyToBuffer.MemoryCopy 等内存助手可用源 Here
  • UnsafeMemory 的帮助下,在某些情况下使用 Parallel.For 会更快
  • 如果您使用 Win2D 支持的(像素类型)输入,则可以避免以下循环:
byte[] dataBytes = new byte[data.Length];
fixed (byte* inputPointer = &data[0])
Marshal.Copy((IntPtr)inputPointer, dataBytes, 0, data.Length);
RenderTarget = CanvasBitmap.CreateFromBytes(renderPanel, dataBytes, (int)width, (int)height, DirectXPixelFormat.R8G8UIntNormalized, 92, CanvasAlphaMode.Ignore);

但从最后一点不确定,因为我无法在 565,555 上进行测试。

感谢DekuDesu他提供的解释和简化版帮助我做更多的测试。

【问题讨论】:

  • 请尝试缩进您的代码。它会更容易阅读。
  • 可以看看ConverToRGB888的定义和实现吗?
  • @DekuDesu 我有 RGB565/RGB555 输入,我需要以任何可能的方式将其作为 RGB888 发送到输出,但循环次数更少。
  • @DekuDesu 我更新了代码并添加了 ConverToRGB888,我正在寻找使用 Marshal 和 Span.CopyTo 的解决方案,如果可能的话,最多只有一个“循环”。跨度>
  • 看起来您只是将每个像素从 RGB565/555 转换为 RGB88,并且您有两个 for 循环。这是 O(n),您的查找是 O(1)。除非您跳过像素,否则那里没有任何改进。下一个可能的选项是避免全部切片,这充其量只会给您带来边际改进。您可以尝试手动处理字节而不是 Marshall.cast 位,这样做将使用 unsafe 字节操作。这可能最多可以为您节省一些内存副本。

标签: c# memory-management uwp


【解决方案1】:

当我使用(类似“for”的循环)将数据从输入 (ReadOnlySpan) 复制到输出 (Span) 时遇到性能问题

您提供的代码已经非常安全,并且具有您将获得的逐像素操作的最佳复杂性。嵌套的for 循环的存在不一定对应于性能问题或增加的复杂性。

我需要帮助,因为我不明白这个过程是如何工作的,仍然很困惑..特别是在 Slice 部分。

此代码看起来像是要将一种位图格式转换为另一种格式。位图有不同的大小和格式。因此,它们包含一条额外的信息以及宽度和高度、间距。

间距是两行像素信息之间以字节为单位的距离,用于说明不包含完整 32/64 位颜色信息的格式。

知道了这一点,我评论了有问题的方法,以帮助解释它的作用。

public static void ConvertFrameBufferRGB565ToXRGB8888(uint width, uint height, ReadOnlySpan<byte> input, int inputPitch, Span<byte> output, int outputPitch)
{
    // convert the span of bytes into a span of ushorts
    // so we can use span[i] to get a ushort
    var castInput = MemoryMarshal.Cast<byte, ushort>(input);

    // pitch is the number of bytes between the first byte of a line and the first byte of the next line
    // convert the pitch from bytes into ushort pitch
    var castInputPitch = inputPitch / sizeof(ushort);

    // convert the span of bytes into a span of ushorts
    // so we can use span[i] to get a ushort
    var castOutput = MemoryMarshal.Cast<byte, uint>(output);
    var castOutputPitch = outputPitch / sizeof(uint);

    for (var i = 0; i < height; i++)
    {
        // get a line from the input
        // remember that pitch is the number of ushorts between lines
        // so i * pitch here gives us the index of the i'th line, and we don't need the padding
        // ushorts at the end so we only take castInputPitch number of ushorts
        var inputLine = castInput.Slice(i * castInputPitch, castInputPitch);
                
        // same thing as above but for the output
        var outputLine = castOutput.Slice(i * castOutputPitch, castOutputPitch);

        for (var j = 0; j < width; j++)
        {
            // iterate through the line, converting each pixel and storing it in the output span
            outputLine[j] = ConverToRGB888(inputLine[j]);
        }
    }
}

通过像素转换将数据从 ReadOnlySpan 复制到输出的最快方法

老实说,您提供的方法很好,它既安全又快速。请记住,在 CPU 上线性复制位图等数据本质上是一个缓慢的过程。您可能希望的最大性能节省是避免冗余复制数据。除非这需要绝对超快的速度,否则我不建议进行除删除 .fill(0) 之外的更改,因为这可能没有必要,但您必须对其进行测试。

如果您绝对必须从中获得更多性能,您可能需要考虑类似我在下面提供的内容。但是我提醒你,像这样的不安全代码很好...... 不安全 并且容易出错。它几乎没有错误检查,并且做了很多假设,所以这由你来实现。

如果仍然不够快,可以考虑用 C 编写一个 .dll 并使用互操作。

public static unsafe void ConvertExtremelyUnsafe(ulong height, ref byte inputArray, ulong inputLength, ulong inputPitch, ref byte outputArray, ulong outputLength, ulong outputPitch)
{
    // pin down pointers so they dont move on the heap
    fixed (byte* inputPointer = &inputArray, outputPointer = &outputArray)
    {
        // since we have to account for padding we should go line by line
        for (ulong y = 0; y < height; y++)
        {
            // get a pointer for the first byte of the line of the input
            byte* inputLinePointer = inputPointer + (y * inputPitch);

            // get a pointer for the first byte of the line of the output
            byte* outputLinePointer = outputPointer + (y * outputPitch);

            // traverse the input line by ushorts
            for (ulong i = 0; i < (inputPitch / sizeof(ushort)); i++)
            {
                // calculate the offset for the i'th ushort,
                // becuase we loop based on the input and ushort we dont need an index check here
                ulong inputOffset = i * sizeof(ushort);

                // get a pointer to the i'th ushort
                ushort* rgb565Pointer = (ushort*)(inputLinePointer + inputOffset);

                ushort rgb565Value = *rgb565Pointer;

                // convert the rgb to the other format
                uint rgb888Value = ConverToRGB888(rgb565Value);

                // calculate the offset for i'th uint
                ulong outputOffset = i * sizeof(uint);

                // at least attempt to avoid overflowing a buffer, not that the runtime would let you do that, i would hope..
                if (outputOffset >= outputLength)
                {
                    throw new IndexOutOfRangeException($"{nameof(outputArray)}[{outputOffset}]");
                }

                // get a pointer to the i'th uint
                uint* rgb888Pointer = (uint*)(outputLinePointer + outputOffset);

                // write the bytes of the rgb888 to the output array
                *rgb888Pointer = rgb888Value;
            }
        }   
    }
}

免责声明:我是在手机上写的

【讨论】:

  • 出色的解决方案,不安全的功能提供了更好的性能,非常感谢您的解释,现在对我来说更清楚了,非常感谢。
  • 只是出于好奇,您获得了多少性能提升?我懒得实际设置它来转换位图,所以我估计这会快 10-15%,因为没有内存副本。
  • 我正在增强游戏模拟器,我意外地从 60fps 到 30~37fps 掉帧(在我实施像素缓存/跳过解决方案之后).. 不安全的版本恢复到正常的 60fps,您的方法是纯数学的,我从不信任 Microsoft 扩展 .CopyTo、Slice..etc。
  • 我很高兴听到您有非边际改善!然而,应该注意的是,.NET 运行时,尽管上面贴着 MS 的名字,但它只是由像我这样的小伙子开发的。您可以查看Spanhere 的源代码。有趣的是,看起来他们对.Slice() 的实现基本上是我在这个答案中已经写过的,我很好奇为什么会有性能差异。可能的堆栈帧开销或Mashall.As&lt;&gt; 的问题。
  • 没有线索!如果它有效并且速度更快,那么它在我的书中是可行的。只需确保其结果确定性即可。
猜你喜欢
  • 2018-06-09
  • 2010-10-01
  • 2021-06-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-07-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多