Почему бокс в примитивном типе значения в .NET не кэшируется, в отличие от Java? - PullRequest
11 голосов
/ 23 ноября 2010

Рассмотрим:

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' (unboxed) тип значения в объект ссылка (тип O). Это завершено созданием нового объекта и копирование данных из значения введите во вновь выделенный объект.

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

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

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

РЕДАКТИРОВАТЬ : Я действительно предлагал статический кэш "часто встречающихся" примитивов , очень похожий на то, что делает Java . Пример реализации см. В ответе Джона Скита. Я понимаю, что делать это для произвольных, возможно изменяемых, типов значений или динамически «запоминания» экземпляров во время выполнения - это совершенно другой вопрос.

РЕДАКТИРОВАТЬ : изменен заголовок для ясности.

Ответы [ 6 ]

11 голосов
/ 23 ноября 2010

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

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

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

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

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

3 голосов
/ 23 ноября 2010

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

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

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

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

1 голос
/ 27 ноября 2010

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

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

0 голосов
/ 27 февраля 2012

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

0 голосов
/ 27 января 2011

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

...