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

本文关键字:方法 简洁 结构体 复制 数组 是否 一种 | 更新日期: 2023-09-27 18:08:46

我有一个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;
}

我想把这些结构体的数组复制到一个大的浮点数数组中。据我所知,这是一个内存块的1:1副本

我所知道的是相当丑陋的,而且不是那么快:

        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;
        }

如果我使用较低级别的语言,我会简单地使用内存,我可以在c#中使用什么作为等效的?

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

不能使用内存副本,因为不能盲目地假设成员是如何存储在结构体中的。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;
}

这可能同样丑陋,但非常快。

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;
      }
    }
  }
}

好了,这是我的测试工具。我的项目属性是发布构建,"优化代码",也"允许不安全的代码"检查。

令人惊讶的是(至少对我来说)在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;
      }
    }
  }
}

也许您可以用浮点数数组别名结构数组,并且绝对不进行复制。检查这个SO答案的起点

不一定是1:1的复制。CLR可以自由地按照自己喜欢的方式布局结构中的字段。它可能会重新排序,重新排列。

如果你添加一个[StructLayout(LayoutKind.Sequential)]直接复制可能是可能的,但我仍然会去类似于你的原始代码。