在C#中,是否有一种更快/更清洁的方法将结构体复制到数组中?

5

我有一个float4x4结构体,其中包含16个浮点数:

struct float4x4
{
        public float M11; public float M12; public float M13; public float M14;
        public float M21; public float M22; public float M23; public float M24;
        public float M31; public float M32; public float M33; public float M34;
        public float M41; public float M42; public float M43; public float M44;
}

我希望能将这些结构体的数组复制到一个大的浮点数数组中。据我所知,这只是将一块内存完全复制过来。
我所知道的方法相当丑陋,而且速度也不快:
        int n = 0;
        for (int i = 0; i < Length; i++)
        {
            array[n++] = value[i].M11;
            array[n++] = value[i].M12;
            array[n++] = value[i].M13;
            array[n++] = value[i].M14;

            array[n++] = value[i].M21;
            array[n++] = value[i].M22;
            array[n++] = value[i].M23;
            array[n++] = value[i].M24;

            array[n++] = value[i].M31;
            array[n++] = value[i].M32;
            array[n++] = value[i].M33;
            array[n++] = value[i].M34;

            array[n++] = value[i].M41;
            array[n++] = value[i].M42;
            array[n++] = value[i].M43;
            array[n++] = value[i].M44;
        }

如果我使用的是低级语言,我会简单地使用memcpy,在C#中该如何实现等效功能?

好吧,你可以使用不安全代码。如果你获得了对 value[i] 的指针,那么你可以使用 Marshal.Copy 将其复制到 array 中。至于这是否更清晰,我不太确定。 - harold
作为一个结构体,它相当臃肿... 结构体确实有指导尺寸,顺便说一下。 - Marc Gravell
如果您使用StructLayout与Explicit并定义大小和布局,则可以使用一个简单的C++/CLI类,将固定指针重新解释为数组中的第一个条目并执行memcpy。这有各种警告,但非常快。然而,我认为,如果您无法自己编写这样的代码,那么在学会之前最好不要尝试。 - ShuggyCoUk
5个回答

4

您不能使用内存复制,因为您不能盲目地假设结构体成员的存储方式。如果这样做可以更快,JIT编译器决定在它们之间存储几个字节的填充。

您的结构体大小已经远远超出了推荐的结构体大小,所以您应该将其改为类。此外,结构体不应该是可变的,这也适用于类。

如果您在内部使用数组存储属性,则可以使用该数组来复制值:

class float4x4 {

  public float[] Values { get; private set; } 

  public float4x4() {
    Values = new float[16];
  }

  public float M11 { get { return Values[0]; } set { Values[0] = value; } }
  public float M12 { get { return Values[0]; } set { Values[0] = value; } }
  ...
  public float M43 { get { return Values[14]; } set { Values[14] = value; } }
  public float M44 { get { return Values[15]; } set { Values[15] = value; } }

}

现在,您可以从对象中获取Values数组,并使用Array.CopyTo方法将其复制到数组中:
int n = 0;
foreach (float4x4 v in values) {
  v.Values.CopyTo(array, n);
  n += 16;
}

2
选择将矩阵作为值类型是合理的,我个人认为。例如,XNA就选择了这样做。如果你选择一个类,你需要使它不可变,否则你将无法获得值语义,而我认为这对于矩阵来说是必要的。而且,如果你使它不可变,你将需要经常创建新实例。因此,我相信这是违反指南并使用较大结构体可以带来好处的情况之一。 - CodesInChaos
如果您自己指定布局,那么可以假设布局。例如 [StructLayout(LayoutKind.Sequential)]。将其转换为类会得到额外的加分,因为这样的结构是低效的(这也是 DirectX/XNA 等通过引用传递它们的原因)。 - Jonathan Dickinson
@CodeInChaos:由于该结构是可变的,因此无论如何您都没有值语义... - Guffa
尽管与之相关的问题存在,但即使是可变结构体也具有值语义,因为它们在适当的位置被复制。通过仔细设计,大多数与之相关的问题都可以避免(特别是避免使用突变方法,除非显式地传入值)。 - CodesInChaos
@Guffa,我同意CodeInChaos的观点-它仍然表现为可变值,具有复制语义。虽然这不是最容易理解的设置,但却是令人困惑的常见原因。 - Marc Gravell
@CodeInChaos: 有些人选择在XNA中使用结构体,因为垃圾收集在XNA中的惩罚性更大。收集器不如桌面CLR GC精细,并且会频繁地暂停您的游戏进行收集。因此,人们试图通过避免收集压力来避免GC惩罚。他们通过低效的结构体复制的运行时成本以及处理可变值类型引起的错误的开发成本来支付代价。不幸的是,这通常是一个好的权衡。我希望我们可以改善GC,而不是这样做。 - Eric Lippert

2

这可能同样不太美观,但非常快速。

using System.Runtime.InteropServices;

namespace ConsoleApplication23 {
  public class Program {
    public static void Main() {
      var values=new[] {
        new float4x4(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16),
        new float4x4(-1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -11, -12, -13, -14, -15, -16)
      };
      var result=Transform(values);
    }

    public static unsafe float[] Transform(float4x4[] values) {
      var array=new float[values.Length*16];
      fixed(float* arrayStart=array) {
        var destp=arrayStart;
        fixed(float4x4* valuesStart=values) {
          int count=values.Length;
          for(var valuesp=valuesStart; count>0; ++valuesp, --count) {
            var sourcep=valuesp->data;
            for(var i=0; i<16/4; ++i) {
              *destp++=*sourcep++;
              *destp++=*sourcep++;
              *destp++=*sourcep++;
              *destp++=*sourcep++;
            }
          }
        }
        return array;
      }
    }

    [StructLayout(LayoutKind.Explicit)]
    public unsafe struct float4x4 {
      [FieldOffset(0)] public float M11;
      [FieldOffset(4)] public float M12;
      [FieldOffset(8)] public float M13;
      [FieldOffset(12)] public float M14;
      [FieldOffset(16)] public float M21;
      [FieldOffset(20)] public float M22;
      [FieldOffset(24)] public float M23;
      [FieldOffset(28)] public float M24;
      [FieldOffset(32)] public float M31;
      [FieldOffset(36)] public float M32;
      [FieldOffset(40)] public float M33;
      [FieldOffset(44)] public float M34;
      [FieldOffset(48)] public float M41;
      [FieldOffset(52)] public float M42;
      [FieldOffset(56)] public float M43;
      [FieldOffset(60)] public float M44;

      //notice the use of "fixed" keyword to make the array inline
      //and the use of the FieldOffset attribute to overlay that inline array on top of the other fields
      [FieldOffset(0)] public fixed float data[16];

      public float4x4(float m11, float m12, float m13, float m14,
        float m21, float m22, float m23, float m24,
        float m31, float m32, float m33, float m34,
        float m41, float m42, float m43, float m44) {
        M11=m11; M12=m12; M13=m13; M14=m14;
        M21=m21; M22=m22; M23=m23; M24=m24;
        M31=m31; M32=m32; M33=m33; M34=m34;
        M41=m41; M42=m42; M43=m43; M44=m44;
      }
    }
  }
}

相对于什么来衡量,才能称之为非常快? - Ritch Melton
好的,我在进行一些测量,发现有8.5%的改进。所以从一方面来说这是“显著的”,但是从另一方面来说,我不应该称之为“非常快”。如果有人感兴趣,我可以发布代码。 - Corey Kosak
你的测试用例是什么?是一百万次迭代的紧密循环吗? - Ritch Melton
我并不是要听起来很攻击性,只是持怀疑态度。 - Ritch Melton
变得越来越有趣了。我在X64上获得了非常快的性能。如果我将我的测试工具作为单独的答案发布到这个问题中(以便获得格式),这样可以吗? - Corey Kosak
显示剩余3条评论

1

好的,这是我的测试工具。我的项目属性是发布构建,“优化代码”和“允许不安全代码”都已选中。

令人惊讶的是(至少对我来说),IDE内部和外部的性能差异非常大。从IDE运行时有明显的差异(而x64的差异非常大)。在IDE外部运行时,结果相当。

所以这有点奇怪,我无法解释IDE+x64的结果。也许这对某些人来说很有趣,但因为它不再提供答案给发帖者的原始问题,也许应该将其移动到其他主题?

在IDE内部,平台设置为x86

pass 1: old 00:00:09.7505625 new 00:00:08.6897013 percent 0.1088

在IDE中,平台设置为x64

pass 1: old 00:00:14.7584514 new 00:00:08.8835715 percent 0.398068858362741

从命令行运行,平台设置为x86

pass 1: old 00:00:07.6576469 new 00:00:07.2818252 percent 0.0490779615341104

从命令行运行,平台设置为x64

pass 1: old 00:00:07.2501032 new 00:00:07.3077479 percent -0.00795087992678504

这是代码:

using System;
using System.Runtime.InteropServices;

namespace ConsoleApplication23 {
  public class Program {
    public static void Main() {
      const int repeatCount=20;
      const int arraySize=5000000;

      var values=MakeValues(arraySize);

      for(var pass=0; pass<2; ++pass) {
        Console.WriteLine("Starting old");
        var startOld=DateTime.Now;
        for(var i=0; i<repeatCount; ++i) {
          var result=TransformOld(values);
        }
        var elapsedOld=DateTime.Now-startOld;

        Console.WriteLine("Starting new");
        var startNew=DateTime.Now;
        for(var i=0; i<repeatCount; ++i) {
          var result=TransformNew(values);
        }
        var elapsedNew=DateTime.Now-startNew;

        var difference=elapsedOld-elapsedNew;
        var percentage=(double)difference.TotalMilliseconds/elapsedOld.TotalMilliseconds;

        Console.WriteLine("pass {0}: old {1} new {2} percent {3}", pass, elapsedOld, elapsedNew, percentage);
      }
      Console.Write("Press enter: ");
      Console.ReadLine();
    }

    private static float4x4[] MakeValues(int count) {
      var result=new float4x4[count];
      for(var i=0; i<count; ++i) {
        result[i]=new float4x4(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15);
      }
      return result;
    }

    public static float[] TransformOld(float4x4[] value) {
      var array=new float[value.Length*16];
      int n = 0;
      for(int i = 0; i < value.Length; i++) {
        array[n++] = value[i].M11;
        array[n++] = value[i].M12;
        array[n++] = value[i].M13;
        array[n++] = value[i].M14;

        array[n++] = value[i].M21;
        array[n++] = value[i].M22;
        array[n++] = value[i].M23;
        array[n++] = value[i].M24;

        array[n++] = value[i].M31;
        array[n++] = value[i].M32;
        array[n++] = value[i].M33;
        array[n++] = value[i].M34;

        array[n++] = value[i].M41;
        array[n++] = value[i].M42;
        array[n++] = value[i].M43;
        array[n++] = value[i].M44;
      }
      return array;
    }

    public static unsafe float[] TransformNew(float4x4[] values) {
      var array=new float[values.Length*16];
      fixed(float* arrayStart=array) {
        var destp=arrayStart;
        fixed(float4x4* valuesStart=values) {
          int count=values.Length;
          for(var valuesp=valuesStart; count>0; ++valuesp, --count) {
            var sourcep=valuesp->data;
            for(var i=0; i<16/4; ++i) {
              *destp++=*sourcep++;
              *destp++=*sourcep++;
              *destp++=*sourcep++;
              *destp++=*sourcep++;
            }
          }
        }
        return array;
      }
    }

    [StructLayout(LayoutKind.Explicit)]
    public unsafe struct float4x4 {
      [FieldOffset(0)] public float M11;
      [FieldOffset(4)] public float M12;
      [FieldOffset(8)] public float M13;
      [FieldOffset(12)] public float M14;
      [FieldOffset(16)] public float M21;
      [FieldOffset(20)] public float M22;
      [FieldOffset(24)] public float M23;
      [FieldOffset(28)] public float M24;
      [FieldOffset(32)] public float M31;
      [FieldOffset(36)] public float M32;
      [FieldOffset(40)] public float M33;
      [FieldOffset(44)] public float M34;
      [FieldOffset(48)] public float M41;
      [FieldOffset(52)] public float M42;
      [FieldOffset(56)] public float M43;
      [FieldOffset(60)] public float M44;

      //notice the use of "fixed" keyword to make the array inline
      //and the use of the FieldOffset attribute to overlay that inline array on top of the other fields
      [FieldOffset(0)] public fixed float data[16];

      public float4x4(float m11, float m12, float m13, float m14,
        float m21, float m22, float m23, float m24,
        float m31, float m32, float m33, float m34,
        float m41, float m42, float m43, float m44) {
        M11=m11; M12=m12; M13=m13; M14=m14;
        M21=m21; M22=m22; M23=m23; M24=m24;
        M31=m31; M32=m32; M33=m33; M34=m34;
        M41=m41; M42=m42; M43=m43; M44=m44;
      }
    }
  }
}

如果你在比较正在调试的代码和未经调试的代码,性能差异是巨大的;这是否是你遇到的问题?如果运行时知道它正在被调试,那么即使JIT编译器生成得不够积极优化,CLR也会做很多额外的工作。 - Eric Lippert
是的,直到现在我才意识到这种差异有多么明显。 - Corey Kosak

-1
也许你可以使用浮点数数组来别名结构体数组,而且完全不需要复制。请查看这个SO答案作为起点。

我只是建议使用FieldOffset可能会节省复制。既然这是邪恶的想法,我一定会在下次忏悔时提到它并请求宽恕.. - renick

-1

这并不一定是一对一的复制。CLR可以自由地以任何它喜欢的方式布局结构中的字段。它可能会重新排序它们,重新对齐它们。

如果您添加了[StructLayout(LayoutKind.Sequential)],则可能会进行直接复制,但我仍然建议使用类似于原始代码的东西。


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