Подтвердить что ты не робот

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

Ответ 3

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

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

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);
}

Производит этот вывод:

original.X = 1

original.Y = 2

boxed1.X = 3

boxed1.Y = 4

boxed2.X = 1

boxed2.Y = 2

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

Ответ 4

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

Усилия по поиску кешированного значения значительно уменьшат скорость этого кода.

Ответ 5

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

Ответ 6

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