Почему бокс-это примитивный тип значения in.NET некэшированные, в отличие от Java?


Рассмотрим:

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, который является, почему ссылочное равенство между ними не удается. Эта страница , по-видимому, указывает на то, что это заданное поведение:

Инструкция box преобразует 'raw' (распакованный) тип значения в объект ссылка (тип O). Это выполняется путем создания нового объекта и копирование данных из значения введите в только что выделенный объект.

Но почему это должно быть именно так? Есть ли какая-либо убедительная причина, по которой среда CLR не выбирает хранение "кэша" коробочных Int32s или даже более сильных общих значений для всех примитивных типов значений (которые все неизменяемы)? Я знаю, что на Яве есть нечто подобное.

Во времена не-дженериков, разве это не помогло бы много с уменьшением требований к памяти, а также рабочей нагрузки GC для большого ArrayList, состоящего в основном из малых целых чисел? Я также уверен, что существует несколько современных .NET-приложений, которыедействительно используют дженерики, но по какой-либо причине (отражение, назначение интерфейса и т. д.), запускают большие бокс-распределения, которые могут быть массово сокращены с помощью (что представляется) простой оптимизации.

Так в чем же причина? Некоторые последствия производительности я не рассматривал (я сомневаюсь, что проверка того, что элемент находится в кэше и т. д. приведет к потере чистой производительности, но что я знаю)? Реализация трудности? Проблемы с небезопасным кодом? Нарушение обратной совместимости (я не могу придумать ни одной хорошей причины, почему хорошо написанная программа должна полагаться на существующее поведение)? Или что-то еще?

EDIT: то, что я действительно предполагал, было статическим кэшем" часто встречающихся " примитивов, очень похоже на то, что делает Java . Пример реализации см. В ответе Джона Скита. Я понимаю, что делаю это для произвольного, возможно изменчивого, типы значений или динамически " запоминающие " экземпляры во время выполнения-это совершенно другое дело.

EDIT : изменено название для ясности.

6 11

6 ответов:

Одна из причин, которую я нахожу убедительной, - это последовательность. Как вы говорите, 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
}

Я не могу утверждать, что умею читать мысли, но вот пара факторов:

1) кэширование типы значений могут сделать для непредсказуемость - сравнение двух коробочных значений, которые равны могут быть истинными или ложными в зависимости от попадания в кэш и реализации. Ай!

2) срок жизни коробочного типа значений, скорее всего, короткий - так как долго вы держите значение в кэше? Теперь у вас либо есть много кэшированных значений, которые больше не будут использоваться, либо вам нужно сделать GC реализация более сложная для отслеживания времени жизни кэшированных типов значений.

С этими недостатками, какова потенциальная победа? Меньший объем памяти в приложении, которое выполняет много долгоживущих операций с одинаковыми типами значений. Поскольку эта победа-нечто, что повлияет на небольшое число приложений и может быть обойдено путем изменения кода, я собираюсь согласиться с решениями c# spec writer здесь.

Упакованные объекты значений не обязательно неизменны. Можно изменить значение в поле типа значения, например, через интерфейс.

Таким образом, если бокс типа значения всегда возвращает один и тот же экземпляр, основанный на одном и том же исходном значении, это создаст ссылки, которые могут быть неуместными (например, два разных экземпляра типа значения, которые имеют одно и то же значение, заканчиваются одной и той же ссылкой, даже если они не должны).

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

Boxed1.X = 3

Boxed1.Y = 4

Boxed2.X = 1

Boxed2.Y = 2

Если бы boxing не создавал новый экземпляр, то boxed1 и boxed2 имели бы одинаковые значения,что было бы неуместно, если бы они были созданы из другого экземпляра исходного типа значения.

Этому есть простое объяснение: un / boxing-это быстрый. Он должен был вернуться в .NET 1.x дней. После того, как JIT-компилятор генерирует машинный код для него, для него генерируется всего несколько инструкций процессора, все встроенные без вызовов методов. Не считая угловых случаев, таких как nullable типы и большие структуры.

Поиск кэшированного значения значительно снизит скорость работы этого кода.

Я не думаю, что заполненный во время выполнения кэш будет хорошей идеей, но я думаю, что было бы разумно в 64-битных системах определить ~8 миллиардов из 64 квинтиллионов возможных объектов-ссылочных значений как целочисленные или плавающие литералы, а в любой системе предварительно поместить все примитивные литералы. Проверка того, содержат ли верхние 31 бит ссылочного типа некоторое значение, вероятно, должна быть дешевле, чем ссылка на память.

Добавление к уже перечисленным ответам заключается в том, что в .net, по крайней мере с обычным сборщиком мусора, ссылки на объекты хранятся внутри как прямые указатели. Это означает, что при сборке мусора система должна обновлять каждую отдельную ссылку на каждый перемещаемый объект, но это также означает, что операция "магистрали" может быть очень быстрой. Если бы ссылки на объекты были иногда прямыми указателями, а иногда чем-то еще, это потребовало бы дополнительного кода каждый раз объект разыменован. Поскольку разыменование объектов является одной из наиболее распространенных операций во время выполнения .net-программы, даже 5% - ное замедление здесь было бы разрушительным, если бы оно не сопровождалось потрясающим ускорением. Например, "64-разрядная компактная" модель, в которой каждая ссылка на объект является 32-разрядным индексом в таблице объектов, может обеспечить лучшую производительность, чем существующая модель, в которой каждая ссылка является 64-разрядным прямым указателем. Операции отсрочки потребовали бы дополнительный поиск таблицы, который был бы плохим, но ссылки на объекты были бы меньше, таким образом позволяя больше из них храниться в кэше сразу. В некоторых обстоятельствах это может быть крупным выигрышем в производительности (возможно, достаточно часто, чтобы быть стоящим-возможно, нет). Неясно, однако, что разрешение объектной ссылке иногда быть прямым указателем памяти, а иногда быть чем-то другим действительно даст много преимуществ.