List<T> 到 T[] 不复制

Posted

技术标签:

【中文标题】List<T> 到 T[] 不复制【英文标题】:List<T> to T[] without copying 【发布时间】:2011-06-25 18:27:38 【问题描述】:

我有大量需要提供给 OpenGL 的值类型。如果这能尽快发生,那就太好了。 我现在正在做的事情是这样的:

List<Vertex> VList = new List<Vertex>();
... //Add vertices
Vertex[] VArray;
VList.CopyTo(VArray, VList.Length);
GL.SetData(..., VArray);

这个列表很容易有 10MB 大,所以复制很慢。我可以在不复制的情况下做到这一点,比如以某种方式获取指向 List 内部使用的数组的指针吗?

还是我必须实现自己的 List 类..

编辑:我忘了提到我不知道将添加到列表中的元素数量。

【问题讨论】:

@Iain,大概他不知道他将使用多少个Vertex 对象。 因为我不知道开头的长度。我必须制作一个大得离谱的数组,然后调整它的大小。 @Hannesh,我的猜测是,因为我还没有调查过,所以列表的后备数组将有许多空槽,这些空槽将在随后的Add 操作中被填充。这是为了便于快速添加操作,而不必不断调整数组的大小(当然,它会在必要时这样做)。因此,1) 对后备数组的直接引用可能仍需要一些清理。 2) 如果您真的关心性能,请注意此调整大小并做出相应计划。 @Anthony:List :D @Hannesh 也没有,如果您自己推出,请确保您对其进行分析。您的 IList 实现可能比 List 慢得多,以至于开销大于收益。 有谁知道为什么不暴露内部数组?我看不出这可能有害的任何原因。 【参考方案1】:

如果您需要重复访问内部数组,最好将访问器存储为委托。

在这个例子中,它是动态方法的委托。第一次调用可能不会很快,但后续调用(在相同类型的 List 上)会快得多。

public static class ListExtensions

    static class ArrayAccessor<T>
    
        public static Func<List<T>, T[]> Getter;

        static ArrayAccessor()
        
            var dm = new DynamicMethod("get", MethodAttributes.Static | MethodAttributes.Public, CallingConventions.Standard, typeof(T[]), new Type[]  typeof(List<T>) , typeof(ArrayAccessor<T>), true);
            var il = dm.GetILGenerator();
            il.Emit(OpCodes.Ldarg_0); // Load List<T> argument
            il.Emit(OpCodes.Ldfld, typeof(List<T>).GetField("_items", BindingFlags.NonPublic | BindingFlags.Instance)); // Replace argument by field
            il.Emit(OpCodes.Ret); // Return field
            Getter = (Func<List<T>, T[]>)dm.CreateDelegate(typeof(Func<List<T>, T[]>));
        
    

    public static T[] GetInternalArray<T>(this List<T> list)
    
        return ArrayAccessor<T>.Getter(list);
    

确保包括:

using System.Reflection;
using System.Reflection.Emit;

【讨论】:

【参考方案2】:

我不会推荐你想做的事。你为什么首先使用List&lt;T&gt;?如果您能准确地告诉我们您想要创建的数据结构应该具有哪些特征,以及它应该如何与消费 API 交互,我们或许能够为您的问题提供适当的解决方案。

但我会尽力回答问题。

我可以在不复制的情况下做到这一点,例如 以某种方式获得指向数组的指针 List 内部使用?

是的,尽管您将依赖未记录的实现细节。从 NET 4.0 开始,支持数组字段称为 _items

Vertex[] vertices = (Vertex[]) typeof(List<Vertex>)
                   .GetField("_items", BindingFlags.NonPublic | BindingFlags.Instance)
                   .GetValue(VList);

请注意,这个数组的末尾几乎肯定会有 slack(这就是List&lt;T&gt; 的全部意义),所以这个数组上的array.Length 不会那么有用。需要通过其他方式通知使用数组的 API 数组的“真实”长度(通过告诉它列表的真实 Count 是什么)。

【讨论】:

+1,但实际上,OP 应该只创建自己的数据结构。 请放心,MS 不会在下一个版本中更改内部实现... 你会推荐什么而不是列表?我对想法持开放态度。 @Hannesh 推出你自己的IList&lt;T&gt; 实现。 @Hannesh,其实我只是建议你重新实现List&lt;T&gt;,公开暴露内部数组。【参考方案3】:

IList<T> 接口并不难做到(好吧,只要 Reflector 是免费的并且可以正常工作,提示提示)。

您可以创建自己的实现并将内部数组公开为公共属性。

【讨论】:

我什至不会打扰IList&lt;T&gt;,只需包装一个T[] 并引入一个Add 方法。 IndexOfInsert 等——不需要。 @Dan 依赖于他的其余代码。如果他正在听我们的主人和指挥官 Jeffrey Richter 的话,他将能够将他的新集合用于任何采用 IEnumerable、IList、IEnumerable 等的方法。 @Will:是的,假设。对我来说,这听起来像是 YAGNI 的情况。尤其是非通用的IList 部分(那是另一只野兽!)。但是,由于我不知道他可能会将其用于什么else,所以您是对的;我可能完全错了。让我这样说吧:如果实际上我需要的只是一个可以添加并仍然作为数组访问的数组,我绝对不会费心实现IList&lt;T&gt;。也许IEnumerable&lt;T&gt;,只是因为它太琐碎了。 @Dan sokay,你的打击是正义的。他必须从他的整体设计中确定实现该接口是否有任何好处。 @DanTao 我有类似的问题,但由于使用了一些闭源 API,我无法使用 IList。我对你说的包装很感兴趣。你能看看我的问题吗? ***.com/questions/34448350/…【参考方案4】:

与其使用反射来访问List&lt;T&gt; 中的内部数组,如果您只需要添加 的能力,那么我实际上建议您实现自己的可调整大小的数组(喘气!)。 没那么难。

类似:

class ResizableArray<T>

    T[] m_array;
    int m_count;

    public ResizableArray(int? initialCapacity = null)
    
        m_array = new T[initialCapacity ?? 4]; // or whatever
    

    internal T[] InternalArray  get  return m_array;  

    public int Count  get  return m_count;  

    public void Add(T element)
    
        if (m_count == m_array.Length)
        
            Array.Resize(ref m_array, m_array.Length * 2);
        

        m_array[m_count++] = element;
    

然后您可以使用InternalArray 获取内部数组,并使用Count 知道数组中有多少项。

【讨论】:

我不想假设消费 API 将如何处理数组末尾的“slack”。 @Iain:我也不会,但如果它可以接受length 参数,那将是完美的。否则我不知道 OP 真正希望的是什么;我怀疑他是否期望 List&lt;T&gt; 在每次调用 Add 时将其内部数组的大小调整为 1 (想象一下,保证 O(N) 追加!)。 @Lain 显然他必须考虑到这一点。 OP 使用它来对抗 OpenGL 堆栈,所以我敢打赌 OP 调用的许多采用数组的方法也采用数组长度。 @Iain:我是这样看的。 API 肯定需要一个 array,否则 OP 的问题将没有意义List&lt;T&gt;IEnumerable,所以他可以通过其他方式)。另一方面,我假设它可以选择接受length 参数;否则 OP 的希望将是徒劳的,因为毕竟没有办法避免将元素复制到适当大小的数组中。如果不是这样,我认为 OP 有点搞砸了 ;) @Dan:对不起,我只是说如果它确实期望一个 IEnumerable 会更好。 @Will:如果是这样,没问题:)【参考方案5】:

您可以通过反射来做到这一点:

public static T[] GetUnderlyingArray<T>(this List<T> list)

    var field = list.GetType().GetField("_items",
        System.Reflection.BindingFlags.Instance |
        System.Reflection.BindingFlags.NonPublic);
    return (T[])field.GetValue(list);

编辑:啊,在我测试这个时有人已经说过了..

【讨论】:

【参考方案6】:

您可能需要考虑您的处理方法是否错误。如果你发现自己使用反射来做到这一点 - 你已经迷路了。

我可以想出几种方法来解决这个问题,但哪种方法最理想在很大程度上取决于这是否是多线程代码。

让我们假设它不是......

想想数组的特性。每次调用此方法时,都会创建一个 N 长度数组。您的目标是提高性能(这意味着您希望最小化分配和数据副本)。

您能否在编译或运行时提示数组的理想起始大小是多少?我的意思是 - 如果 95% 的时间 N 长度是 100k 或更少......从 100k 项目数组开始。继续使用它,直到遇到数组太小的情况。

当您遇到这种情况时,您可以根据您对程序的理解来决定您要做什么。阵列是否应该增长 10%?它应该增长到字面所需的长度吗?您可以使用现有的数据并继续处理其余数据吗?

随着时间的推移会找到理想的尺寸。您甚至可以让您的程序在每次运行时监控最终大小,并将其用作下次启动时分配的提示(也许这个数组长度取决于环境因素,例如分辨率等)。

换句话说,我的建议是不要使用 List-to-Array 方法,而是预先分配一个数组,将其永久保留,并根据需要进行扩展。

如果您的程序存在线程问题,您显然需要解决这些问题。

【讨论】:

【参考方案7】:

您可能能够从通用列表中获取指针,但我不推荐它,它可能不会按照您期望的方式工作(如果有的话)。基本上它意味着获取一个指向对象的指针,而不是像数组这样的内存结构。

我认为你应该反过来做,如果你需要速度,然后在不安全的上下文中使用结构数组指针直接处理字节数组。

背景信息: “即使与 unsafe 关键字一起使用,也不允许获取托管对象的地址、获取托管对象的大小或声明指向托管类型的指针。” - 来自C#: convert generic pointer to array

MSDN unsafe

【讨论】:

哇——他想要的只是数组。例如,OP 可以轻松地使用反射来解决它。不确定所有这些带有不安全代码的业务是关于什么的。【参考方案8】:

由于您使用的是 GL,因此我假设您知道自己在做什么,并跳过所有警告。试试这个,或查看https://***.com/a/35588774/194921

  [StructLayout(LayoutKind.Explicit)]
  public struct ConvertHelper<TFrom, TTo>
      where TFrom : class
      where TTo : class 
    [FieldOffset( 0)] public long before;
    [FieldOffset( 8)] public TFrom input;
    [FieldOffset(16)] public TTo output;

    static public TTo Convert(TFrom thing) 
      var helper = new ConvertHelper<TFrom, TTo>  input = thing ;
      unsafe 
        long* dangerous = &helper.before;
        dangerous[2] = dangerous[1];  // ie, output = input
      
      var ret = helper.output;
      helper.input = null;
      helper.output = null;
      return ret;
    
  

  class PublicList<T> 
    public T[] _items;
  

  public static T[] GetBackingArray<T>(this List<T> list) 
    return ConvertHelper<List<T>, PublicList<T>>.Convert(list)._items;
  

【讨论】:

以上是关于List<T> 到 T[] 不复制的主要内容,如果未能解决你的问题,请参考以下文章

如何在不克隆的情况下复制 List<T>

List<T> 任何或计数? [复制]

IEnumeration<T> 和 List<T> 之间的区别? [复制]

C# 中 List<T> 的只读冲突? [复制]

如何轻松将 DataReader 转换为 List<T>? [复制]

如何从 C# 中的通用 List<T> 中获取元素? [复制]