代码之家  ›  专栏  ›  技术社区  ›  Ani

为什么拳击是一个原始的值类型,在.NET中被取消,不像Java?

  •  11
  • Ani  · 技术社区  · 14 年前

    考虑:

    int a = 42;
    
    // Reference equality on two boxed ints with the same value
    Console.WriteLine( (object)a == (object)a ); // False
    
    // Same thing - listed only for clarity
    Console.WriteLine(ReferenceEquals(a, a));  // False
    

    显然,每个装箱指令分配一个单独的装箱实例。 Int32 ,这就是它们之间引用相等失败的原因。 This page 似乎表明这是指定的行为:

    box指令转换“raw” (未绑定)值类型到对象中 参考(O型)。这是 完成者 创建新对象 从值中复制数据 键入新分配的对象。

    但为什么一定要这样? CLR不选择保存装箱的“缓存”有什么令人信服的原因吗 国际32 所有原语值类型(都是不可变的)的s,甚至更强的公共值?我知道Java有这样的东西。

    在没有泛型的日子里,对于一个大的 ArrayList 主要由小整数组成?我也肯定有几个 现代的 .NET应用程序 使用泛型,但是出于任何原因(反射、接口分配等),运行大的拳击分配,可以用(什么)大幅度减少? 出现 一个简单的优化。

    那是什么原因?一些我没有考虑的性能暗示(我怀疑测试项目是否在缓存中等是否会导致净性能损失,但我知道什么)?执行困难?不安全代码的问题?打破向后兼容(我想不出任何 好的 为什么一个好的程序应该依赖于现有的行为?或者别的什么?

    编辑 :我真正的建议是 静止的 “常见”的缓存 原语 , much like what Java does . 有关示例实现,请参见Jon Skeet的答案。我知道这样做是为了任意的,可能是可变的,值类型或者 动态 在运行时“记住”实例是完全不同的事情。

    编辑 :为清楚起见,更改了标题。

    6 回复  |  直到 7 年前
        1
  •  11
  •   Jon Skeet    14 年前

    其中一个原因 找到令人信服的是一致性。如你所说,Java 缓存一定范围内的装箱值。。。这意味着写代码太容易了 一段时间 :

    // Passes in all my tests. Shame it fails if they're > 127...
    if (value1 == value2) {
        // Do something
    }
    

    我已经被这个咬了一口——当然,幸运的是,这是在一个测试中而不是在生产代码中,但是有一些东西在给定的范围之外显著地改变了行为,这仍然是令人讨厌的。

    别忘了,任何有条件的行为也要付出代价 全部的 拳击操作——所以在不使用缓存的情况下,你会发现它是比较慢的(因为它首先必须检查是否使用缓存)。

    当然,如果您真的想编写自己的缓存框操作,可以这样做:

    public static class Int32Extensions
    {
        private static readonly object[] BoxedIntegers = CreateCache();
    
        private static object[] CreateCache()
        {
            object[] ret = new object[256];
            for (int i = -128; i < 128; i++)
            {
                ret[i + 128] = i;
            }
        }
    
        public object Box(this int i)
        {
            return (i >= -128 && i < 128) ? BoxedIntegers[i + 128] : (object) i;
        }
    }
    

    然后像这样使用:

    object y = 100.Box();
    object z = 100.Box();
    
    if (y == z)
    {
        // Cache is working
    }
    
        2
  •  3
  •   Philip Rieck    14 年前

    我不能说我能读心术,但有几个因素:

    1)缓存值类型可以使 不可预测性 -根据缓存命中率和实现,比较两个相等的装箱值可能为真或假。哎哟!

    2)装箱值类型的生存期很可能很短—那么您在缓存中保存该值多长时间?现在,您要么拥有大量不再使用的缓存值,要么需要使GC实现更加复杂,以跟踪缓存值类型的生存期。

    有了这些不利因素,潜在的胜利是什么?应用程序中有很多相同类型的长寿命的小的内存占用。既然这场胜利会影响到一小部分应用程序,而且可以通过修改代码来解决,我同意c规范编写者的决定。

        3
  •  3
  •   Samuel Neff    14 年前

    装箱值对象不一定是不可变的。可以更改盒装值类型中的值,例如通过接口。

    因此,如果装箱一个值类型总是基于相同的原始值返回相同的实例,它将创建可能不合适的引用(例如,两个不同的值类型实例,它们碰巧具有相同的值,即使它们不应该以相同的引用结束)。

    public interface IBoxed
    {
        int X { get; set; }
        int Y { get; set; }
    }
    
    public struct BoxMe : IBoxed
    {
        public int X { get; set; }
    
        public int Y { get; set; }
    }
    
    public static void Test()
    {
        BoxMe original = new BoxMe()
                            {
                                X = 1,
                                Y = 2
                            };
    
        object boxed1 = (object) original;
        object boxed2 = (object) original;
    
        ((IBoxed) boxed1).X = 3;
        ((IBoxed) boxed1).Y = 4;
    
        Console.WriteLine("original.X = " + original.X);
        Console.WriteLine("original.Y = " + original.Y);
        Console.WriteLine("boxed1.X = " + ((IBoxed)boxed1).X);
        Console.WriteLine("boxed1.Y = " + ((IBoxed)boxed1).Y);
        Console.WriteLine("boxed2.X = " + ((IBoxed)boxed2).X);
        Console.WriteLine("boxed2.Y = " + ((IBoxed)boxed2).Y);
    }
    

    生成此输出:

    原始.X=1

    原Y=2

    框1.X=3

    框1.Y=4

    框2.X=1

    框2.Y=2

    如果装箱没有创建新的实例,那么BOXDE1和BOXED2将具有相同的值,如果它们是由不同的原始值类型实例创建的,那么这将是不合适的。

        4
  •  1
  •   Hans Passant    14 年前

    有一个简单的解释:un/拳击是 快速的 . 它需要在.NET 1.x天内返回。在JIT编译器为它生成机器代码之后,只为它生成了少量的CPU指令,这些指令都是内联的,没有方法调用。不包括可空类型和大型结构之类的角案例。

    查找缓存值的工作将大大降低此代码的速度。

        5
  •  0
  •   supercat    14 年前

    我不认为运行时填充缓存是一个好主意,但我认为在64位系统上,将64个可能的对象引用值中的80亿定义为整数或浮点数,以及在任何系统预选框上,将所有原始文本都定义为整数或浮点数可能是合理的。测试引用类型的上31位是否包含某些值可能比内存引用便宜。

        6
  •  0
  •   supercat    13 年前

    除了已经列出的答案之外,事实上在.net中,至少在普通垃圾收集器中,对象引用在内部存储为直接指针。这意味着在执行垃圾收集时,系统必须更新对每个移动对象的每个引用,但这也意味着“主线”操作可能非常快。如果对象引用有时是直接指针,有时是其他指针,则每次取消引用对象时都需要额外的代码。由于对象解引用是.net程序执行过程中最常见的操作之一,因此,如果没有惊人的加速,即使是5%的速度减慢也将是毁灭性的。例如,可能是一个“64位紧凑”模型,其中每个对象引用是一个32位索引到对象表中,它可以提供比现有模型中更好的性能,其中每个引用是64位直接指针。延迟操作将需要额外的表查找,这将是不好的,但对象引用将更小,从而允许将更多的表同时存储在缓存中。在某些情况下,这可能是一个重大的业绩胜利(也许经常是值得的——也许不是)。不过,现在还不清楚,允许对象引用有时是直接的内存指针,有时是其他东西,是否真的能提供很多好处。