【问题标题】:Marshal an array of floats to an array of structs without copying?将浮点数组编组为结构数组而不复制?
【发布时间】:2014-12-03 04:47:22
【问题描述】:

假设我有以下代码

public static class Main
{
    public struct Vec3
    {
        public float x, y, z;
    }

    public void Entry()
    {
        float[9] floats = new floats[9] { 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f };
        Vec3[3] vecs;
    }
}

我想将 float[] 转换为 Vec3[]。

有没有办法用 Marshaling 来做到这一点,这样所需的复制会比迭代 float[] 并将每个 float 复制到 Vec3[] 更快?假设 float[] 非常大。

【问题讨论】:

  • 您的 Vec3 实际上不仅仅是内存中的 3 个浮点数。有关对象本身的信息也存储在那里。这就是为什么我认为不可能做到这一点,但如果有人证明我错了,我会很高兴。
  • 我相信 c# 结构可以以允许将它们传递给 c++ API 函数的方式进行编组,因此这应该不是问题。

标签: c# arrays performance marshalling


【解决方案1】:

您可以使用System.Runtime.InteropServices.Marshal 类来执行此操作:

unsafe
{
    fixed (Vec3* vecsPointer = vecs)
    {
        Marshal.Copy(floats, 0, new IntPtr(vecsPointer), floats.Length);
    }
}

也就是说,我认为如果可能的话,通常最好避免使用unsafe,即使存在一些可衡量的性能差异。对我来说,选择上述内容而不是直接托管副本(在发布版本中可能非常快,尽管可能不如上述内容快),这对我来说必须是一个非常显着的性能改进。

当然,在使用 unsafe 方法之前,您应该仔细衡量每种方法的整体影响,并确保值得潜在的维护麻烦。


编辑:出于好奇,我继续做了一个简单的测试。我创建了一个大小为 1MB 的float[] 并复制到了Vec3[]。我使用了上述方法,并将其与如下所示的简单循环进行了比较:

for (int k = 0, l = 0; k + 2 < floats.Length; k += 3, l++)
{
    vecs[l].x = floats[k];
    vecs[l].y = floats[k + 1];
    vecs[l].z = floats[k + 2];
}

在我的测试中,我每次尝试执行每个副本 10,000 次,副本的总大小约为 10GB。

“仅”unsafe 版本比安全版本快大约(仅慢于)2 倍。对于unsafe,每个 10,000 次复制试用大约需要 1.5 秒,而对于安全则需要 3 秒。 2 倍的加速听起来不错,但当然,只有在您的程序什么都不做的情况下,您才能获得它。在现实世界的程序中,这可能转化为 5% 或更少的改进,具体取决于其他情况。

我看到的其他一些有趣的事情:

  • 使用 Marshal.Copy(),复制 2 的精确幂比其他方法快得多。例如。 2 ^ 20 字节与其他内容。在这种情况下,它的速度接近 3 倍。
  • 与上述相关,Marshal.Copy() 似乎对副本的确切大小更加敏感。使用安全代码可以获得更一致的性能,当然速度会慢一些。
  • 在足够小的缓冲区大小(似乎约为 100-200 字节)下,unsafe 代码的设置开销会阻止它,并且与安全版本一样慢。

【讨论】:

  • 感谢您提供出色的答案和分析数据。干的很好。这似乎是解决方案,即使我更喜欢不复制的东西。
  • 作为旁注。如果我有一个 Vec3[] 并且我想调用一个非托管函数,该函数采用一个 float*(浮点数组)和一个使用 PInvoke 的大小。有没有不复制的方法?做“var handle = GCHandle.Alloc(vecs, GCHandleType.Pinned); APICall(handle.AddrOfPinned(), sizeof(float) * 3 * vecs.Length);”有什么问题?
  • 是的。如果您不介意在调用期间固定对象(这将干扰 .NET 内存管理),您可以直接将 float[] 传递给非托管代码。您可以使用相同的 fixed 语法,我认为......只要您只需要将对象固定在有限的范围内,您就不必通过 GCHandle.Alloc() 方法。也就是说,10GB/s 对我来说似乎相当快;直接传递数组可能会获得多少额外的速度,但您可能会因为阻塞 GC 操作而丢失其他地方。至少,您需要进行彻底的测试。
  • @BrianReichle: Buffer.BlockCopy() (实际上,所有Buffer 成员都)仅限于与原始类型数组一起使用。它不适用于用户定义的结构Vec3
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多