【问题标题】:How to store structs of different types without boxing如何在不装箱的情况下存储不同类型的结构
【发布时间】:2011-05-28 17:43:26
【问题描述】:

我正在创建一个用于 XNA 游戏的消息传递系统。我的消息类型是结构,因为我希望它们以值类型的方式运行。

struct MyMessageType1 : IMessage {}
struct MyMessageType2 : IMessage {}

List<IMessage> messageQueue = new List<IMessage>();

我希望能够在我的消息队列中存储不同类型的消息,但我希望这样做时不将它们装箱。

如果我让结构实现了 IMessage 之类的接口,并且我尝试将它们存储在列表中,它们就会被装箱。

我不提前知道所有可能的消息类型,所以我不能只为每种类型硬编码一个 List。

所以问题是如何存储不同类型的结构列表而不将它们装箱?

【问题讨论】:

  • 为什么你希望它们成为结构?你为什么不希望他们装箱?你担心性能吗?
  • 我希望它们成为结构,因为我认为值类型语义对于我的 Message 对象来说感觉更正确。我不希望它们被装箱,因为我在 XNA 游戏中使用它,并且对于我的消息传递内容,我不想创建任何垃圾收集器必须清理的垃圾。
  • “我不想制造任何垃圾” 为什么不呢?你知道它会给你带来性能问题吗?
  • 垃圾在 Xbox 上造成问题。这个消息系统每帧会使用很多次,如果它产生垃圾,GC 就会一直运行。

标签: c# generics struct xna boxing


【解决方案1】:

这是做不到的。

备选方案 1

但是,您可以通过使用两个列表(List&lt;MyMessageType1&gt;List&lt;MyMessageType2&gt;)来模拟事物。

然后,您编制了一个超级索引(可能只是另一个整数数组(长整数?)),以便可以(间接)将一个项目作为一个列表来寻址。

您可能想要优化索引(运行长度编码:仅存储支持数组切换的索引:这在迭代已知在支持数组之一中连续的子范围时也将非常有帮助)

列表在内部使用数组存储,所以 - 你没有拳击 - 快速随机访问 - 使用 list.ForEach 进行惊人的迭代

备选方案 2

查看 StructLayout 属性并通过执行所有操作以某种方式模拟 Union。如果您真的准备好弄脏自己的手,请放入 unsafe {} 块(并使用 /unsafe 编译)...但是,如果重要,请认真考虑 P/Invoke a C DLL 或使用 C++/CLI 那个很多

备选方案 3(添加)

因为我真的很喜欢 Marc Gravell 指出您可以使用我提到的 StructLayout 来精确定位 union .NET 结构的所有三个成员在相同偏移处的事实;我想我会采取额外的步骤,看看我是否可以让它变得更加leaky transparent。这几乎是透明的:

using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace LeakyAbstractions
{
    struct TypeA {}
    struct TypeB {}
    struct TypeC {}

    [StructLayout(LayoutKind.Explicit)] internal struct AnyMessage {
        [FieldOffset(0)] public TypeA A;
        [FieldOffset(0)] public TypeB B;
        [FieldOffset(0)] public TypeC C;

        AnyMessage(TypeA a) { A = a; }
        AnyMessage(TypeB b) { B = b; }
        AnyMessage(TypeC c) { C = c; }

        public static implicit operator TypeA(AnyMessage msg) { return msg.A; }
        public static implicit operator TypeB(AnyMessage msg) { return msg.B; }
        public static implicit operator TypeC(AnyMessage msg) { return msg.C; }

        public static implicit operator AnyMessage(TypeA a) { return a; }
        public static implicit operator AnyMessage(TypeB b) { return b; }
        public static implicit operator AnyMessage(TypeC c) { return c; }
    }

    public class X
    {
        public static void Main(string[] s) 
        {
            var anyMessages = new List<AnyMessage> { 
                new TypeA(),
                new TypeB(),
                new TypeC(),
            };

            TypeA a = anyMessages[0];
            TypeB b = anyMessages[1];
            TypeC c = anyMessages[2];

            anyMessages.Add(a);
            anyMessages.Add(b);
            anyMessages.Add(c);
        }
    }
}

我将把区分这种穷人变种的问题留给你作为练习。最简单的方法是在 AnyMessage 结构中添加一个字段,但根据有效负载,其他策略可能更(空间/时间)效率更高。


我的 0.02 美元

哦,我从来没有真正这样做过,因为它看起来过于复杂。我假设你有充分的理由来优化这个


PS。如果您在阅读我的回答后问这个问题(昨天:Should I use a struct or a class to represent a Lat/Lng coordinate?),我将快速判断这个过早的优化

【讨论】:

  • 我在考虑使用备选方案 1,即多个通用列表解决方案,但由于我在编译时不知道所有可能的消息类型,我需要动态创建这些通用列表,这我可以。但现在我必须弄清楚如何存储列表。如果我将它们放在 List 中,那么当我尝试从 IList 中获取值时,我会招致装箱。如果我将它们存储为 ILists,我想不出一种方法在运行时将其转换回其 List 自身,以免受到任何拳击的影响。有没有办法做到这一点?
  • @BowserKingKoopa:如果你的元素是有限的类型,我会选择List&lt;Ilist&gt;提供您尽可能早地转换List本身,而不是项目,这绝不会在访问底层项目时引起装箱。除此之外的任何事情都是要求 valuetype Variant 实现。如果允许的话,就不会发明拳击了。
  • 我在 Marc Gravells PoC 上实施了更多胶水;这几乎是透明的(参见 Main 中的用法)
【解决方案2】:

基本上,你不能很好地

  • 当作object或者一个接口处理:装箱
  • 用抽象基类包装一个泛型类型:重新发明一个盒子
  • 反射:使用object,加框
  • dynamic:本质上是object,盒装

选项,但是,封装对象在更大的结构中,即

struct AnyMessage {
    public TypeA A;
    public TypeB B;
    public TypeC C;
}
struct TypeA {...}
struct TypeB {...}
struct TypeC {...}

现在,这应该可行,但显然,它的缺点是要大得多。您可能能够使用显式布局将它们全部定位在字节 0(制作 union)来解决此问题,但我怀疑在 Xbox 上是不允许的。但在常规 .NET 上:

[StructLayout(LayoutKind.Explicit)] struct AnyMessage {
    [FieldOffset(0)] public TypeA A;
    [FieldOffset(0)] public TypeB B;
    [FieldOffset(0)] public TypeC C;
}

【讨论】:

  • 其实你可以在Xbox上使用StructLayoutFieldOffset
【解决方案3】:

您可以创建一个队列来存储您的结构而无需装箱,然后使用具有如下通用方法的接口对其进行处理:

interface IMessageProcessor
{
    void Process<T>(T message) where T : struct, IMessage;
}

class MessageQueue
{
    abstract class TypedMessageQueue
    {
        public abstract void ProcessNext(IMessageProcessor messageProcessor);
    }

    class TypedMessageQueue<T> : TypedMessageQueue where T : struct, IMessage
    {
        Queue<T> m_queue = new Queue<T>();

        public void Enqueue(T message)
        {
            m_queue.Enqueue(message);
        }

        public override void ProcessNext(IMessageProcessor messageProcessor)
        {
            messageProcessor.Process(m_queue.Dequeue());
        }
    }

    Queue<Type> m_queueSelectorQueue = new Queue<Type>();
    Dictionary<Type, TypedMessageQueue> m_queues =
        new Dictionary<Type, TypedMessageQueue>();

    public void Enqueue<T>(T message) where T : struct, IMessage
    {
        TypedMessageQueue<T> queue;
        if (!m_queues.ContainsKey(typeof(T)))
        {
            queue = new TypedMessageQueue<T>();
            m_queues[typeof(T)] = queue;
        }
        else
            queue = (TypedMessageQueue<T>)m_queues[typeof(T)];

        queue.Enqueue(message);
        m_queueSelectorQueue.Enqueue(typeof(T));
    }

    public void ProcessNext(IMessageProcessor messageProcessor)
    {
        var type = m_queueSelectorQueue.Dequeue();
        m_queues[type].ProcessNext(messageProcessor);
    }
}

您为每种类型的消息保留一个单独的队列,使用它可以完全避免消息装箱,无需任何 StructLayout 诡计,也无需事先了解所有可能的消息类型。

【讨论】:

    【解决方案4】:

    我不认为你可以。普遍性是有代价的。如果您担心的是性能,我的建议是不要过早优化。如果不是,并且您确实需要按值复制行为,请考虑使用不可变类型(例如 System.String)

    【讨论】:

      【解决方案5】:

      完全可以在托管代码中创建一个实现接口的非泛型类型的结构(我称之为 MagicInvoker),并保存对实现同一接口的任意数量的其他结构的引用,所有这些都没有使用反射、装箱或其他任何会导致 GC 压力的东西。实际上,一旦数组达到最大大小,就可以创建和删除数十亿个值类型对象,而无需再分配任何堆。

      这种方法的最大警告是,这种结构在许多方面变得像“旧 C”中的指针。虽然 MagicInvokers 本身就是值类型,并且它们引用值类型,但它们的语义更像是旧式指针。如果复制一个 MagicInvoker,它将引用与原始结构相同的结构。创建一个 MagicInvoker 然后在没有 Dispose 的情况下放弃它会导致内存泄漏,并且使用或尝试 Dispose 一个已经被释放的 MagicInvoker 的副本会导致未定义的行为。

      公共接口 IDoSomething 子 Dosomething() 端接口 结构 MagicInvoker 实现 IDoSomething、IDisposable 私人持有人作为 InvokerBase 私有索引为整数 Sub DoSomething() 实现 IDoSomething.Dosomething holder.DoDoSomething(索引) 结束子 共享函数 Create(Of T As IDoSomething)(ByVal thing As T) As MagicInvoker 将 newInvoker 调暗为 MagicInvoker newInvoker.holder = Invoker(Of T).HolderInstance newInvoker.index = Invoker(Of T).DoAdd(thing) 返回 newInvoker 结束功能 函数 Clone() 作为 MagicInvoker 将 newHolder 调暗为新的 MagicInvoker newHolder.holder = Me.holder newHolder.index = Me.holder.DoClone(Me.index) 返回新持有人 结束功能 私有 MustInherit 类 InvokerBase MustOverride Sub DoDoSomething(ByVal Index As Integer) MustOverride 函数 DoClone(ByVal srcIndex As Integer) As Integer MustOverride Sub DoDelete(ByVal srcIndex As Integer) 结束类 Private Class Invoker(Of T As IDoSomething) 继承 InvokerBase 共享 myInstances(15) 作为 T,numUsedInstances 作为整数 共享 myDeleted(15) 作为整数,numDeleted 作为整数 公共共享 HolderInstance 作为新的 Invoker(Of T) 覆盖 Sub DoDoSomething(ByVal index As Integer) myInstances(index).Dosomething() 结束子 私有共享函数 GetNewIndex() As Integer 如果 numDeleted > 0 则 numDeleted -= 1 返回 myDeleted(numDeleted) 别的 如果 numUsedInstances >= myInstances.Length 则 ReDim 保留 myInstances(myInstances.Length * 2 - 1) 万一 numUsedInstances += 1 返回 numUsedInstances - 1 万一 结束功能 公共共享函数 DoAdd(ByVal value As T) As Integer 将 newIndex 调暗为 Integer = GetNewIndex() myInstances(newIndex) = 值 返回新索引 结束功能 公共覆盖 Sub DoDelete(ByVal srcIndex As Integer) 如果 numDeleted >= myDeleted.Length 则 ReDim 保留 myDeleted(myDeleted.Length * 2 - 1) 万一 myDeleted(numDeleted) = srcIndex numDeleted += 1 结束子 公共覆盖函数 DoClone(ByVal srcIndex As Integer) As Integer 将 newIndex 调暗为 Integer = GetNewIndex() myInstances(newIndex) = myInstances(srcIndex) 返回新索引 结束功能 结束类 ' 注意:在 MagicInvoker 上调用 Dispose 会导致它的所有副本无效;尝试 ' 使用或处置一个将导致未定义的行为。相反,放弃最后一个副本 ' MagicInvoker 会导致内存泄漏。 Public Sub Dispose() 实现 System.IDisposable.Dispose If holder IsNot Nothing Then holder.DoDelete(索引) 持有人=没有 万一 结束子 末端结构

      MagicInvoker 拥有从 InvokerBase 派生的某个类的实例(对于实现 IDoSomething 的某个 T,它恰好是 Invoker)和数组索引。对于与 MagicInvoker.Create 一起使用的每种类型 T,都会创建一个类 Invoker 的实例;同一实例将用于从该类型创建的所有 MagicInvoker。

      【讨论】:

      • 感谢您的回答。我没有在我的问题中提到我的目标是表现,所以这个答案很遗憾对我没有帮助,我应该在一开始就提到这一点。
      • @MHGameWork:MagicInvoker 技术可以实现出色的性能;我没有说清楚吗?
      • 我在一个非常热的代码路径中使用它,所以我不想使用虚函数调用并冒着缓存未命中的风险。
      • @MHGameWork:.NET 框架即时编译器将为使用的每个不同的MagicInvoker&lt;T&gt; 类生成单独的机器代码,从而避免在运行时调用虚函数。这是这种方法的优点之一——它避免了虚函数调用。
      • 我会更深入地了解一下,自从我上次使用 vb.net 已经 15 年了 :) 肯定也必须运行基准测试
      【解决方案6】:

      这是我解决这个问题的方法。 入队时不分配,仅在并发队列扩大大小时分配。

      public class JobQueue { 
          abstract class Invoker {
              public abstract void Do();
          }
          class Invoker<F>:Invoker where F : IJob {
              static volatile Invoker<F> _ins = null;
              static object syncRoot = new Object();
      
              public static Invoker<F> ins {
                  get {
                      if (_ins == null) {
                          lock (syncRoot) {
                              if (_ins == null)
                                  _ins = new Invoker<F>();
                          }
                      }
                      return _ins;
                  }
              }
              public int registerIndex { get; private set; } = -1;
              ConcurrentQueue<F> queue;
      
              Invoker() {
                  queue = new ConcurrentQueue<F>();
              }
              public void Register(int index) {
                  registerIndex = index;
              }
              public void Enqueue(F t) {
                  queue.Enqueue(t);
              }
              public override void Do() {
                  if (queue.TryDequeue(out var msg)) {
                      msg.DoJob();
                  }
              }
          }
      
          public bool isWork { get; set; } = false;
          ManualResetEvent manualResetEvent = new ManualResetEvent(false);
          Invoker[] invokers = new Invoker[1000];
          int index = 0;
          ConcurrentQueue<int> jobIndexs = new ConcurrentQueue<int>();
      
          public void Enqueue<T>(T t)where T : IJob {
              var invoker = Invoker<T>.ins;
              lock (invoker) {
                  if (invoker.registerIndex == -1) {
                      lock (invokers) {
                          invokers[index] = invoker;
                          invoker.Register(index);
                          jobIndexs.Enqueue(index);
                          index++;
                      }               
                  } else {
                      jobIndexs.Enqueue(invoker.registerIndex);
                  }
                  invoker.Enqueue(t);
              }
              manualResetEvent.Set();
          }
          public void Run() {
              while (true) {
                  if(jobIndexs.TryDequeue(out var index)) {
                      invokers[index].Do();
                  } else {
                      manualResetEvent.Reset();
                      return;
                  }
                  //if (!isWork)
                  //    return;
                  manualResetEvent.WaitOne();
              }
          }
      }
      

      【讨论】:

        猜你喜欢
        • 2022-11-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-08-17
        • 2019-07-13
        • 2016-06-12
        • 1970-01-01
        • 2023-04-05
        相关资源
        最近更新 更多