c# -是否有可能合并盒子?

本文关键字:盒子 合并 有可能 是否 | 更新日期: 2023-09-27 18:08:15

装箱将值类型转换为对象类型。或者正如MSDN所说,装箱是一种"将结构包装在托管堆上的引用类型对象内的操作"。

但是如果你试图通过查看IL代码来深入研究,你只会看到神奇的单词"box"。"

推测,我猜运行时有某种基于泛型的秘密类在袖子里,像Box<T>public T Value属性,装箱一个int看起来像:

int i = 5;
Box<int> box = new Box<int>;
box.Value = 5;

将整型拆箱要便宜得多:return box.Value;

不幸的是,我的性能要求很高的服务器应用程序做了相当多的装箱,特别是小数。更糟糕的是,这些盒子寿命很短,这让我怀疑我付了两次钱,一次是实例化盒子,另一次是在我用完盒子后回收它。

如果我自己分配内存,我会考虑在这里使用对象池。但是,由于实际的对象创建隐藏在IL中的一个神奇的单词后面,我的选择是什么?

我的具体问题:

  • 是否有一个现有的机制来诱导运行时从池中取出盒子,而不是实例化它们?
  • 装箱过程中创建的实例类型是什么?是否有可能手动控制装箱过程,但仍然与拆箱兼容?

如果最后一个问题看起来很奇怪,我的意思是我可以创建我自己的Box<T>DecimalBox类,池它,并手动装箱/拆箱。但是我不想去修改代码中使用盒装值的各个地方(也就是打开它)。

c# -是否有可能合并盒子?

推测,我猜运行时有某种基于泛型的秘密类在它的袖子

你的猜测几乎是正确的。逻辑上你可以把一个盒子想象成一个神奇的Box<T>类型,它的行为就像你描述的那样(再加上一些魔法;例如,可空值类型框的方式有点不寻常。作为实际的实现细节,运行时不会对泛型类型执行此操作。在泛型类型被添加到类型系统之前,CLR v1就存在装箱。

我的性能要求很高的服务器应用程序做了相当多的装箱,特别是小数。

如果当你这样做的时候很疼,那么停止这样做。与其试图让拳击变得更便宜,不如从一开始就停止。你为什么要打小数点?

更糟糕的是,这些盒子寿命很短,这让我怀疑我花了两次钱,一次是为了实例化盒子,另一次是为了在我用完盒子后收集它。

寿命短比寿命长好;对于寿命较短的堆对象,您需要付费收集它们一次,然后它们就死亡了。对于长寿命堆对象,当对象继续存活时,您将一次又一次地支付该成本。

当然,您可能担心的关于短期对象的成本并不是收集本身的成本。而是收集压力;分配的寿命较短的对象越多,垃圾收集就越频繁。

分配成本非常小。移动GC堆上的指针,将小数点复制到该位置,完成。

如果我自己分配这些内存,我会考虑在这里使用对象池。

;您为收集长寿命对象付出了更多的成本,但由于产生的收集压力更小,因此总共执行的收集次数更少。

是否有一个现有的机制来诱导运行时从池中取出盒子而不是实例化它们?

不。

装箱过程中创建的实例类型是什么?是否有可能手动控制装箱过程,但仍然与拆箱兼容?

盒子的类型是被盒子的东西的类型。只需调用GetType;它会告诉你的。盒子很神奇;它们是它们所包含的事物的类型。

就像我之前说的,与其试图让拳击变得更便宜,不如从一开始就不要这么做。

运行时完成了你所描述的大部分工作,但是不涉及泛型,因为泛型不是原始框架的一部分。

如果您正在使用一些需要盒装值的代码,那么对于盒装,您可以做的事情并不多。您可以通过覆盖隐式转换来创建一个使用相同语法返回值的对象,但这仍然需要是一个对象,并且您基本上要做相同的工作。

尝试池化盒装值很可能会降低而不是提高性能。垃圾收集器是专门为有效地处理短期对象而设计的,如果将对象放入池中,它们将成为长期对象。当对象在垃圾收集中幸存下来时,它们将被移动到下一个堆,这涉及到将对象从内存中的一个位置复制到另一个位置。因此,通过池化对象,实际上可能会给垃圾收集器带来更多的工作,而不是更少。

默认没有像Box<T>这样的类。类型仍然是原始类型,但是是一个引用。因为Decimal是不可变的,你不能在创建后改变它的值。因此,你不能真正使用池与普通的盒装小数。

可以通过实现缓存来避免对重复出现的值进行装箱。或者你需要实现你自己的框类型。

您自己的box类型不能通过标准强制转换从object中打开。因此,您需要调整消费代码。


编写自己的方法返回一个盒装值:

[ThreadStatic]
private static Dictionary<T,object> cache;
public object BoxIt<T>(T value)
  where T:struct
{
  if(cache==null)
    cache=new Dictionary<T,object>();
  object result;
  if(!cache.TryGetValue(T,result))
  {
    result=(object)T;
    cache[value]=result;
  }
  return result;
}

这个简单实现的一个问题是缓存永远不会删除项。也就是说,这是一个内存泄漏。

int i = 5;
object boxedInt = i;

System.Object指定一个值类型或多或少就是您的代码所关心的所有装箱(我不会进入装箱操作的技术细节)。

将您的十进制值保存在System.Object变量中可以节省一些装箱和创建System.Object实例的时间,但是您总是必须解箱。如果您必须频繁地更改这些值,因为每次更改都是赋值,因此至少是装箱,那么这将变得更加困难甚至不可能。

这里有一个这样的例子——.Net框架在一个类中使用预先装箱的布尔值:
class BoolBox
{
    // boxing here
    private static object _true = true;
    // boxing here
    private static object _false = false;
    public static object True { get { return _true; } }
    public static object False { get { return _false; } }
}

WPF系统经常使用System.Object变量作为依赖属性,只是为了说明即使在这些"现代"也无法避免装箱/拆箱的情况。

不幸的是,您不能钩住装箱过程,但是,您可以使用隐式转换使其"看起来"像装箱。

我也会避免在Dictionary中存储每个值-您的内存问题会变得更糟。这是一个可能有用的装箱框架。

public class Box
{
    internal Box()
    { }
    public static Box<T> ItUp<T>(T value)
        where T : struct
    {
        return value;
    }
    public static T ItOut<T>(object value)
        where T : struct
    {
        var tbox = value as Box<T>;
        if (!object.ReferenceEquals(tbox, null))
            return tbox.Value;
        else
            return (T)value;
    }
}
public sealed class Box<T> : Box
    where T : struct
{
    public static IEqualityComparer<T> EqualityComparer { get; set; }
    private static readonly ConcurrentStack<Box<T>> _cache = new ConcurrentStack<Box<T>>();
    public T Value
    {
        get;
        private set;
    }
    static Box()
    {
        EqualityComparer = EqualityComparer<T>.Default;
    }
    private Box()
    {
    }
    ~Box()
    {
        if (_cache.Count < 4096) // Note this will be approximate.
        {
            GC.ReRegisterForFinalize(this);
            _cache.Push(this);
        }
    }
    public static implicit operator Box<T>(T value)
    {
        Box<T> box;
        if (!_cache.TryPop(out box))
            box = new Box<T>();
        box.Value = value;
        return box;
    }
    public static implicit operator T(Box<T> value)
    {
        return ((Box<T>)value).Value;
    }
    public override bool Equals(object obj)
    {
        var box = obj as Box<T>;
        if (!object.ReferenceEquals(box, null))
            return EqualityComparer.Equals(box.Value, Value);
        else if (obj is T)
            return EqualityComparer.Equals((T)obj, Value);
        else
            return false;
    }
    public override int GetHashCode()
    {
        return Value.GetHashCode();
    }
    public override string ToString()
    {
        return Value.ToString();
    }
}
// Sample usage:
var boxed = Box.ItUp(100);
LegacyCode(boxingIsFun);
void LegacyCode(object boxingIsFun)
{
  var value = Box.ItOut<int>(boxingIsFun);
}

老实说,你应该提出另一个问题——并就如何摆脱这种装箱问题征求意见。

我刚刚在博客中介绍了我们在对象数据库引擎中使用的盒缓存实现。小数的取值范围很广,缓存框可能不是很有效,但是如果您发现自己必须使用对象字段或对象[]数组来存储大量的通用值,那么这可能会有所帮助:

http://www.singulink.com/CodeIndex/post/value-type-box-cache

使用这个之后,我们的内存使用量急剧下降。基本上,数据库中的所有内容都存储在object[]数组中,可以是许多GB的数据,因此这是非常有益的。

有三种主要的方法可以使用:

  • object BoxCache<T>.GetBox(T value) -获取值的框(如果有)被缓存,否则它会为你装箱。
  • object BoxCache<T>.GetOrAddBox(T value) -如果有,则获取值的框缓存,否则它将一个添加到缓存中并返回它。
  • void AddValues<T>(IEnumerable<T> values) -添加指定的框