获取对数组内部结构的引用

我想修改一个结构的字段,该结构在一个数组内,而不必设置整个结构。 在下面的示例中,我想在数组中设置元素543的一个字段。 我不想复制整个元素(因为复制MassiveStruct会损害性能)。

class P { struct S { public int a; public MassiveStruct b; } void f(ref S s) { sa = 3; } public static void Main() { S[] s = new S[1000]; f(ref s[543]); // Error: An object reference is required for the non-static field, method, or property } } 

有没有办法在C#中做到这一点? 或者我总是要从数组中复制整个结构,修改副本,然后将修改后的副本放回到数组中。

唯一的问题是你试图从静态方法调用实例方法,而没有P的实例。

使f成为一个静态方法(或创建一个P的实例来调用它),它会没问题。 这都是关于阅读编译器错误:)

话虽如此,我强烈建议你:

  • 尽可能避免创建大型结构
  • 尽可能避免创建可变结构
  • 避免公共领域

[ 编辑2017: 在本帖末尾看到关于C#7的重要评论 ]

经过多年与这个确切问题的摔跤,我将总结我发现的一些技术和解决方案。 除了文体品味外, 结构数组实际上是C#中唯一可用的批量存储方法。 如果您的应用程序在高吞吐量条件下真正处理数百万个中型对象,那么几乎没有其他选择。

我同意@kaalus对象标题和GC压力可以快速安装; 在解析或生成冗长的自然语言句子时,我的语法处理系统可以在不到一分钟的时间内操纵8-10千兆字节(或更多)的结构分析。 提示“C#不适用于这些问题,切换到汇编语言,换行封装FPGA等”。 相反,让我们进行一些测试。

首先,完全理解价值类型 (结构)管理问题的全部范围以及类与结构权衡交易的最佳点是至关重要的。 当然还有装箱,固定/不安全代码,固定缓冲区, GCHandle, IntPtr,等等,但在我看来最重要的是,明智地使用托管指针。

您对此主题的掌握还将包括以下事实的知识:如果您在结构中包含一个或多个对托管类型的引用(而不仅仅是blittable原语),那么使用unsafe指针访问结构的选项非常大降低。 对于我将在下面提到的托管指针方法,这不是问题。 因此,一般来说,包括对象引用很好,并且在本讨论中没有太大变化。

哦,如果确实需要保留unsafe访问权限,可以在“正常”模式下使用GCHandle无限期地在结构中存储对象引用。 幸运的是,将GCHandle放入您的结构中不会触发不安全访问禁止。 (请注意, GCHandle本身就是一个值类型,您甚至可以定义并前往城镇

 var gch = GCHandle.Alloc("spookee",GCHandleType.Normal); GCHandle* p = &gch; String s = (String)p->Target; 

……等等。 作为一种值类型,GCHandle直接映射到您的结构中,但显然它存储的任何引用类型都不是。 它们在堆中,不包含在数组的物理布局中。 最后在GCHandle上,请注意它的复制语义,因为如果你最终没有释放你分配的每个GCHandle,你将会有内存泄漏。

@Ani提醒我们,有些人认为可变结构是“邪恶的”,但事实上他们很容易发生意外事故。 的确,参考OP的例子,

 s[543].a = 3; 

正是我们想要实现的目标: 现场访问我们的数据记录。 (请注意,锯齿状数组的语法是相同的,但我在这里只讨论用户定义的值类型的非锯齿状数组。)对于我自己的程序,如果遇到超大的blittable,我通常认为它是一个严重的错误已经(意外地)从其数组存储行中完全成像的结构:

  rec no_no = s [543];  //不要这样做
 no_no.a = 3 //就像这样 

至于你的结构可以或应该有多大(宽),它都无关紧要,因为你要小心,不要让它们做我刚刚展示的东西,即迁移出它们的数组。 定义一个结构来覆盖我们的数组很容易(实际上,将结构视为一个空的“内存模板” – 而不是数据容器,封装器 – 鼓励正确思考。)

 public struct rec { public int a, b, c, d, e, f; } 

这个有6个int ,总共24个字节。 您需要考虑并注意包装选项以获得对齐友好的尺寸。 但过多的填充可能会削减内存预算:因为更重要的考虑因素是非LOH对象的85,000字节限制。 确保您的记录大小乘以预期的行数不超过此限制。

因此,对于此示例,建议您最多将rec数组保留为每行不超过3,000行。 希望您的应用程序可以围绕这个甜点设计。 当您记住 – 或者 – 每行将是一个单独的垃圾收集对象而不仅仅是一个数组时,这不是那么限制。 你已经将对象扩散减少了三个数量级,这对于一天的工作是有益的。 因此,这里的.NET环境强烈地指导我们一个非常具体的约束:似乎如果你的应用程序的内存设计针对30-70 KB范围内的单片分配,那么你真的可以逃脱它们的很多很多,事实上,你会受到一系列棘手的性能瓶颈(即硬件总线上的带宽)的限制。

所以现在你有一个.NET引用类型(数组),在物理上连续的表格存储中有3,000个6元组。 首先,我们必须非常小心, 不要 “捡起”其中一个结构。 正如Jon Skeet在上面指出的那样,“大规模的结构通常会比类更糟糕”,这绝对是正确的。 没有更好的方法来瘫痪你的记忆总线,而不是开始投掷丰富的价值类型在willy-nilly周围。

因此,让我们利用结构数组中不常提到的一个方面:整个数组的所有行的所有对象(以及那些对象或结构的字段)始终初始化为其默认值。 您可以开始在数组中的任何位置或列(字段)中一次一个地插入值。 您可以将某些字段保留为默认值,或者替换相邻字段而不会打扰中间的字段。 在使用之前,堆栈驻留(局部变量)结构需要烦人的手动初始化。

有时很难保持逐场的方法,因为.NET总是试图让我们在一个new ‘d-up结构中爆炸 – 但对我来说,这种所谓的“初始化”只是违反了我们的禁忌(反对从arrays中取出整个结构),以不同的forms。

现在我们谈到问题的症结所在。 显然,原位访问表格数据可以最大限度地减少数据重组繁忙工作。 但这往往是一个不方便的麻烦。 由于边界检查,数组访问在.NET中可能很慢。 那么如何将“工作”指针保持在数组内部,以避免系统不断重新计算索引偏移量。

评估

让我们评估五种不同方法在值类型数组存储行中操作各个字段的性能。 下面的测试旨在测量集中访问位于某个数组索引处的结构的数据字段的效率,即原位 – 即“它们所在的位置”,而不提取或重写整个结构(数组元素)。 比较了五种不同的访问方法,所有其他因素保持不变。

五种方法如下:

  1. 通过方括号和字段说明符点进行正常的直接数组访问。 请注意,在.NET中,数组是Common Type System的特殊且唯一的原语。 正如@Ani上面提到的,即使使用值类型参数化,此语法也不能用于更改引用实例的单个字段,例如列表。
  2. 使用未记录的__makeref C#语言关键字。
  3. 通过使用ref关键字的委托 管理指针
  4. “不安全”的指针
  5. 与#3相同,但使用C# 函数而不是委托。

在我给出C#测试结果之前,这是测试工具的实现。 这些测试是在.NET 4.5上运行的,这是一个在x64,Workstation gc上运行的AnyCPU发布版本。 (注意,因为测试对分配和解除分配数组本身的效率不感兴趣,所以上述LOH考虑不适用。)

 const int num_test = 100000; static rec[] s1, s2, s3, s4, s5; static long t_n, t_r, t_m, t_u, t_f; static Stopwatch sw = Stopwatch.StartNew(); static Random rnd = new Random(); static void test2() { s1 = new rec[num_test]; s2 = new rec[num_test]; s3 = new rec[num_test]; s4 = new rec[num_test]; s5 = new rec[num_test]; for (int x, i = 0; i < 5000000; i++) { x = rnd.Next(num_test); test_m(x); test_n(x); test_r(x); test_u(x); test_f(x); x = rnd.Next(num_test); test_n(x); test_r(x); test_u(x); test_f(x); test_m(x); x = rnd.Next(num_test); test_r(x); test_u(x); test_f(x); test_m(x); test_n(x); x = rnd.Next(num_test); test_u(x); test_f(x); test_m(x); test_n(x); test_r(x); x = rnd.Next(num_test); test_f(x); test_m(x); test_n(x); test_r(x); test_u(x); x = rnd.Next(num_test); } Debug.Print("Normal (subscript+field): {0,18}", t_n); Debug.Print("Typed-reference: {0,18}", t_r); Debug.Print("C# Managed pointer: (ref delegate) {0,18}", t_m); Debug.Print("C# Unsafe pointer: {0,18}", t_u); Debug.Print("C# Managed pointer: (ref func): {0,18}", t_f); } 

因为为每种特定方法实现测试的代码片段很长,所以我先给出结果。 时间是'滴答声'; 更低意味着更好。

 Normal (subscript+field): 20,804,691 Typed-reference: 30,920,655 Managed pointer: (ref delegate) 18,777,666 // <- a close 2nd Unsafe pointer: 22,395,806 Managed pointer: (ref func): 18,767,179 // <- winner 

我很惊讶这些结果是如此明确。 TypedReferences是最慢的,可能是因为它们随着指针一起晃动类型信息。 考虑到IL-code对于标准版“正常”版本的重要性,它的表现令人惊讶。 模式转换似乎会伤害不安全的代码,以至于您必须certificate,计划和衡量您要部署它的每个地方。

但是,通过在函数的参数传递中利用ref关键字来指向数组的内部部分,从而消除了“按字段访问”数组索引计算,从而实现了最快的时间。

也许我的测试设计有利于这个,但测试场景代表了我的应用程序中的经验使用模式。 让我对这些数字感到惊讶的是,保持托管模式的优势 - 同时也有你的指针 - 没有通过调用函数或通过委托调用来取消。

获胜者,冠军

最快的一个:(也许最简单?)

 static void f(ref rec e) { ea = 4; ee = ea; eb = ed; ef = ed; eb = ee; ea = ec; eb = 5; ed = ef; ec = eb; ee = ea; eb = ed; ef = ed; ec = 6; eb = ee; ea = ec; ed = ef; ec = eb; ee = ea; ed = 7; eb = ed; ef = ed; eb = ee; ea = ec; ed = ef; ee = 8; ec = eb; ee = ea; eb = ed; ef = ed; eb = ee; ef = 9; ea = ec; ed = ef; ec = eb; ee = ea; eb = ed; ea = 10; ef = ed; eb = ee; ea = ec; ed = ef; ec = eb; } static void test_f(int ix) { long q = sw.ElapsedTicks; f(ref s5[ix]); t_f += sw.ElapsedTicks - q; } 

但它的缺点是你不能在你的程序中保持相关的逻辑:函数的实现分为两个C#函数ftest_f

我们可以通过性能上的微小牺牲来解决这个特殊问题。 下一个与上述内容基本相同,但将其中一个函数作为lambda函数嵌入到另一个中...

紧接着

使用内联委托替换前面示例中的静态函数需要使用ref参数,这反过来会排除使用Func lambda语法; 相反,你必须使用旧式.NET的显式委托。

通过添加此全局声明一次:

 delegate void b(ref rec ee); 

...我们可以在整个程序中使用它直接ref数组rec []的元素,内联访问它们:

 static void test_m(int ix) { long q = sw.ElapsedTicks; /// the element to manipulate "e", is selected at the bottom of this lambda block ((b)((ref rec e) => { ea = 4; ee = ea; eb = ed; ef = ed; eb = ee; ea = ec; eb = 5; ed = ef; ec = eb; ee = ea; eb = ed; ef = ed; ec = 6; eb = ee; ea = ec; ed = ef; ec = eb; ee = ea; ed = 7; eb = ed; ef = ed; eb = ee; ea = ec; ed = ef; ee = 8; ec = eb; ee = ea; eb = ed; ef = ed; eb = ee; ef = 9; ea = ec; ed = ef; ec = eb; ee = ea; eb = ed; ea = 10; ef = ed; eb = ee; ea = ec; ed = ef; ec = eb; }))(ref s3[ix]); t_m += sw.ElapsedTicks - q; } 

此外,虽然看起来像是在每次调用时实例化一个新的lambda函数,但是如果你小心的话就不会发生这种情况:当使用这个方法时,请确保你没有“关闭”任何局部变量(也就是说,引用lambda函数之外的变量,从其体内引用,或者做任何其他会阻止委托实例静态的变量。 如果一个局部变量恰好落入你的lambda并且lambda因此被提升为一个实例/类,那么当你试图创建五百万个委托时,你“可能”会注意到差异。

只要你保持lambda函数不受这些副作用的影响,就不会有多个实例; 这里发生的事情是,每当C#确定lambda没有非显式依赖关系时,它就会懒惰地创建(并缓存)一个静态单例。 有点不幸的是,这种激烈的表演交替隐藏在我们作为无声优化的视野之外。 总的来说,我喜欢这种方法。 它快速而且没有杂乱 - 除了奇怪的括号外,这里没有一个可以省略。

其余的

为了完整性,以下是其余测试:普通包围加点; TypedReference; 和不安全的指针。

 static void test_n(int ix) { long q = sw.ElapsedTicks; s1[ix].a = 4; s1[ix].e = s1[ix].a; s1[ix].b = s1[ix].d; s1[ix].f = s1[ix].d; s1[ix].b = s1[ix].e; s1[ix].a = s1[ix].c; s1[ix].b = 5; s1[ix].d = s1[ix].f; s1[ix].c = s1[ix].b; s1[ix].e = s1[ix].a; s1[ix].b = s1[ix].d; s1[ix].f = s1[ix].d; s1[ix].c = 6; s1[ix].b = s1[ix].e; s1[ix].a = s1[ix].c; s1[ix].d = s1[ix].f; s1[ix].c = s1[ix].b; s1[ix].e = s1[ix].a; s1[ix].d = 7; s1[ix].b = s1[ix].d; s1[ix].f = s1[ix].d; s1[ix].b = s1[ix].e; s1[ix].a = s1[ix].c; s1[ix].d = s1[ix].f; s1[ix].e = 8; s1[ix].c = s1[ix].b; s1[ix].e = s1[ix].a; s1[ix].b = s1[ix].d; s1[ix].f = s1[ix].d; s1[ix].b = s1[ix].e; s1[ix].f = 9; s1[ix].a = s1[ix].c; s1[ix].d = s1[ix].f; s1[ix].c = s1[ix].b; s1[ix].e = s1[ix].a; s1[ix].b = s1[ix].d; s1[ix].a = 10; s1[ix].f = s1[ix].d; s1[ix].b = s1[ix].e; s1[ix].a = s1[ix].c; s1[ix].d = s1[ix].f; s1[ix].c = s1[ix].b; t_n += sw.ElapsedTicks - q; } static void test_r(int ix) { long q = sw.ElapsedTicks; var tr = __makeref(s2[ix]); __refvalue(tr, rec).a = 4; __refvalue(tr, rec).e = __refvalue( tr, rec).a; __refvalue(tr, rec).b = __refvalue( tr, rec).d; __refvalue(tr, rec).f = __refvalue( tr, rec).d; __refvalue(tr, rec).b = __refvalue( tr, rec).e; __refvalue(tr, rec).a = __refvalue( tr, rec).c; __refvalue(tr, rec).b = 5; __refvalue(tr, rec).d = __refvalue( tr, rec).f; __refvalue(tr, rec).c = __refvalue( tr, rec).b; __refvalue(tr, rec).e = __refvalue( tr, rec).a; __refvalue(tr, rec).b = __refvalue( tr, rec).d; __refvalue(tr, rec).f = __refvalue( tr, rec).d; __refvalue(tr, rec).c = 6; __refvalue(tr, rec).b = __refvalue( tr, rec).e; __refvalue(tr, rec).a = __refvalue( tr, rec).c; __refvalue(tr, rec).d = __refvalue( tr, rec).f; __refvalue(tr, rec).c = __refvalue( tr, rec).b; __refvalue(tr, rec).e = __refvalue( tr, rec).a; __refvalue(tr, rec).d = 7; __refvalue(tr, rec).b = __refvalue( tr, rec).d; __refvalue(tr, rec).f = __refvalue( tr, rec).d; __refvalue(tr, rec).b = __refvalue( tr, rec).e; __refvalue(tr, rec).a = __refvalue( tr, rec).c; __refvalue(tr, rec).d = __refvalue( tr, rec).f; __refvalue(tr, rec).e = 8; __refvalue(tr, rec).c = __refvalue( tr, rec).b; __refvalue(tr, rec).e = __refvalue( tr, rec).a; __refvalue(tr, rec).b = __refvalue( tr, rec).d; __refvalue(tr, rec).f = __refvalue( tr, rec).d; __refvalue(tr, rec).b = __refvalue( tr, rec).e; __refvalue(tr, rec).f = 9; __refvalue(tr, rec).a = __refvalue( tr, rec).c; __refvalue(tr, rec).d = __refvalue( tr, rec).f; __refvalue(tr, rec).c = __refvalue( tr, rec).b; __refvalue(tr, rec).e = __refvalue( tr, rec).a; __refvalue(tr, rec).b = __refvalue( tr, rec).d; __refvalue(tr, rec).a = 10; __refvalue(tr, rec).f = __refvalue( tr, rec).d; __refvalue(tr, rec).b = __refvalue( tr, rec).e; __refvalue(tr, rec).a = __refvalue( tr, rec).c; __refvalue(tr, rec).d = __refvalue( tr, rec).f; __refvalue(tr, rec).c = __refvalue( tr, rec).b; t_r += sw.ElapsedTicks - q; } static void test_u(int ix) { long q = sw.ElapsedTicks; fixed (rec* p = &s4[ix]) { p->a = 4; p->e = p->a; p->b = p->d; p->f = p->d; p->b = p->e; p->a = p->c; p->b = 5; p->d = p->f; p->c = p->b; p->e = p->a; p->b = p->d; p->f = p->d; p->c = 6; p->b = p->e; p->a = p->c; p->d = p->f; p->c = p->b; p->e = p->a; p->d = 7; p->b = p->d; p->f = p->d; p->b = p->e; p->a = p->c; p->d = p->f; p->e = 8; p->c = p->b; p->e = p->a; p->b = p->d; p->f = p->d; p->b = p->e; p->f = 9; p->a = p->c; p->d = p->f; p->c = p->b; p->e = p->a; p->b = p->d; p->a = 10; p->f = p->d; p->b = p->e; p->a = p->c; p->d = p->f; p->c = p->b; } t_u += sw.ElapsedTicks - q; } 

摘要

对于大型C#应用程序中的内存密集型工作,使用托管指针直接访问原位 值类型数组元素的字段是可行的方法。

如果你真的非常认真考虑性能,那么这可能是使用C++/CLI (或CIL ,就此而言)而不是C#用于应用程序相关部分的充分理由,因为这些语言允许您直接在应用程序中声明托管指针。function体。

C# ,创建托管指针的唯一方法是使用refout参数声明一个函数,然后被调用者将观察托管指针。 因此,为了在C#中获得性能优势,您必须使用上面显示的(前两个)方法之一。 [见下面的C#7]

遗憾的是,为了访问数组元素,这些部署了将函数拆分为多个部分的方法。 虽然比同等的C++/CLI代码要优雅得多,但测试表明,即使在C#中,对于高吞吐量应用程序,我们仍然可以获得与天真的值类型arrays访问相比的巨大性能优势。


[ 编辑2017:虽然可能会给本文的劝告带来一点点预见,但在Visual Studio 2017发布C#7会导致上述特定方法完全过时。 简而言之,语言中的新ref localsfunction允许您将自己的托管指针声明为局部变量,并使用它来合并单个arrays解除引用操作。 所以举例来自上面的测试结构......

 public struct rec { public int a, b, c, d, e, f; } static rec[] s7 = new rec[100000]; 

...这里是如何编写上面相同的测试函数:

 static void test_7(int ix) { ref rec e = ref s7[ix]; // <--- C#7 ref local ea = 4; ee = ea; eb = ed; ef = ed; eb = ee; ea = ec; eb = 5; ed = ef; ec = eb; ee = ea; eb = ed; ef = ed; ec = 6; eb = ee; ea = ec; ed = ef; ec = eb; ee = ea; ed = 7; eb = ed; ef = ed; eb = ee; ea = ec; ed = ef; ee = 8; ec = eb; ee = ea; eb = ed; ef = ed; eb = ee; ef = 9; ea = ec; ed = ef; ec = eb; ee = ea; eb = ed; ea = 10; ef = ed; eb = ee; ea = ec; ed = ef; ec = eb; } 

请注意这是如何完全消除对我所讨论的kludges的需求。 更流畅地使用托管指针可以避免在“获胜者”中使用的不必要的函数调用,这是我所评论的最佳表现方法 。 因此,新function的性能只能比上述方法的获胜者更好

具有讽刺意味的是,C#7还增加了本地function ,这一function可直接解决我为上述两种黑客攻击而导致封装不良的抱怨。 令人高兴的是,为了获得对托管指针的访问而扩散专用function的整个企业现在完全没有实际意义。

虽然Jon Skeet对于您的程序无法编译的原因是正确的,但您可以这样做:

 s[543].a = 3; 

…它将直接在数组中的结构上运行,而不是在副本上运行。

请注意,这个想法仅适用于数组 ,其他集合(如列表)将从indexer-getter返回一个副本(如果在结果值上尝试类似的操作,则会给出编译器错误)。

另一方面, 可变结构被认为是邪恶的 。 你有什么理由不想让S上课吗?

您可以尝试使用转发空结构 ,它不保存实际数据,但只保留数据提供者对象的索引。 这样,您可以存储大量数据,而不会使对象图复杂化。 我非常肯定,只要您不尝试将其编组为非托管代码,在您的情况下使用转发emtpy结构替换您的巨型结构应该非常容易。

看看这个结构。 它可以包含尽可能多的数据。 诀窍是您将实际数据存储在另一个对象中。 通过这种方式,您可以获得引用语义和结构体的优势,这些结构体比类对象消耗更少的内存,并且由于更简单的对象图(如果您有许多实例(数百万)),GC周期更快。

  [StructLayout(LayoutKind.Sequential, Pack=1)] public struct ForwardingEmptyValueStruct { int _Row; byte _ProviderIdx; public ForwardingEmptyValueStruct(byte providerIdx, int row) { _ProviderIdx = providerIdx; _Row = row; } public double V1 { get { return DataProvider._DataProviders[_ProviderIdx].Value1[_Row]; } } public int V2 { get { return DataProvider._DataProviders[_ProviderIdx].Value2[_Row]; } } }