Страницы

Поиск по вопросам

четверг, 5 декабря 2019 г.

Упаковка типов в C#

#c_sharp #net


Насколько я правильно понимаю, упаковка — это процесс, при котором выделяется место
в управляемой куче для копии значимого типа. Ничего сложного для понимания, вроде как.
Но я прочитал, что существуют неявные упаковки, которые выполняются самим компилятором.
А, т.к. процесс упаковки — весьма нежелательное действие, то программисту нужно бы
знать, когда компилятор совершает в тайне все эти грязные штучки.

В книге Джеффри Рихтера есть интересный пример:

Int32 v = 5;
Object o = v;
Console.WriteLine(v + ", " + o);


Здесь процесс упаковки происходит дважды. Почему? Ведь я вижу только одну упаковку: 

Object o = v; // копия экземпляра значимого типа Int32 попадает в кучу


Но Джеффри поясняет, что вторая упаковка происходит неявно. Статический метод WriteLine
класса Console в качестве аргумента ждет объект типа String. А у нас есть типы Int32,
String и Object. Их нужно как-то объединить, чтобы получился объект String. Для этого,
по словам Рихтера, компилятор формирует код, в котором вызывается статический метод
Concat объекта String. Есть несколько перегруженных версий данного метода, одна из которых:

public static String Concat(Object arg0, Object arg1, Object arg2);


Здесь, собственно, и происходит вторая упаковка, когда мы передаем тип Int32, он
неявно приводится к Object. 

До сих пор для меня всё понятно. Это логично и красиво. Но дальше Джеффри советует:
если хотите избавиться от этой неявной упаковке, вызовите метод ToString, который вернет
строку. А String, как мы знаем, это ссылочный тип, и, при приведении типов, никакого
процесса упаковки не будет. Да, говорит Рихтер, возможно, где-то глубоко внутри может
и есть эта самая упаковка, но нас это не должно касаться. В коде мы от неё избавились. 

После этих слов у меня в голове появилась неопределенность: "Постой-ка, Джеффри,
а ведь ToString возвращает полную квалификацию имени типа, а не его строковое представление.
Хм, а может в классе String где-то этот метод переопределяется? Ну-ка пойду гляну в
вижлу". И, таки да, нашел я в классе String переопределение метода ToString, которые
возвращает ссылку на себя же, т.е. тип String. 

Но, к чему я это всё? Дело в том, что я не понимаю, зачем же метод Concat сначала
приводит все типы к Object, а потом вызывает на них ToString? Почему нельзя сразу вызвать
ToString без упаковки? Да, понимаю, что может существовать какой-нибудь пользовательский
тип, о котором разработчики, естественно, ничего не знают и в перегрузках метода Concat
этого типа не будет. Но неужели нельзя как-то по-другому вызывать этот самый ToString?
И если нет, то зачем тогда упрощать работу программисту ценой потери производительности.
Пусть бы в методе WriteLine вызывали метод ToString на всех экземплярах. Ведь многие
и не догадываются, что там происходит процесс упаковки. А если какой-нибудь программист
в цикле будет выводить на консоль значения экземляров значимых типов? Это же сколько
упаковок произойдет? Или я что-то не так понял? Вопрос не совсем конкретный. Мне просто
стало интересно. Спасибо.
    


Ответы

Ответ 1



Почему нельзя сразу вызвать ToString без упаковки? метод Concat, вызываемый в этом куске кода, принимает три аргумента типа object, соответственно, при передаче в него int в качестве параметра этот самый int должен быть упакован, это вполне логично. Чтобы ваш пример работал без упаковки, нужно чтобы существовала перегрузка метода Concat с такой сигнатурой: void Concat(int arg1, string arg2, object arg3) Думаю, вполне очевидно, что это довольно странная перегрузка. Исходя из такой логики также стоило бы создать соответствующие перегрузки для всех возможных вариантов простых типов: с int, long, char, float, double, decimal, bool в самых разных порядках их следования и количеством аргументов. Посчитайте сами сколько должно быть возможных комбинаций. Неоправданно много. Стоит отметить, что при большом желании всю эту кучу различных перегрузок можно написать самостоятельно, используя механизм методов расширения, благодаря чему они будут выглядеть как методы самого класса string (на этих самых методах работает практически весь LINQ) зачем тогда упрощать работу программисту ценой потери производительности затем, что это нормальная практика, во всяком случае для таких языков, как C#. Простота и ясность кода важнее микроскопической экономии на спичках. В противном случае все бы по-прежнем писали на ассемблере. А если какой-нибудь программист в цикле будет выводить на консоль значения экземляров значимых типов? Это же сколько упаковок произойдет? вы слишком преувеличиваете масштаб трагедии. Упаковка, конечно, не очень хорошая вещь, но говорить, что "процесс упаковки — весьма нежелательное действие" - это большое преувеличение. Если вы не пишете что-то очень требовательное к производительности аж на уровне упаковок-распаковок (а C# очень редко используют для подобных задач), то слишком беспокоиться на этот счет вряд ли стоит. Гораздо более значимыми источниками проблем с производительностью являются общение с базами данных, http-запросы, работа с файлами.

Ответ 2



Я почти уверен, что jit от этой упаковки сам сможет избавиться, т. е. формально она как бы есть, но реально её не будет. Хотя было бы хорошо произвести замеры. Другой пример, где формально упаковка есть, а реально jit её не делает (проверено замерами времени): var a = new List {1,2,3,4,5,6,7}; var b = (from x in a select x).ToList(); var c = (from int x in a select x).ToList(); При формировании последовательности для получения c генерируется код a.Cast().Select(...). В случае с b вызова Cast нет. Вызов Cast означает приведение a к IEnumerable (не generic!), из чего должна следовать упаковка и распаковка каждого элемента последовательности. Но реально такое не делается, замеры времени показывают абсолютно одинаковый результат для обоих вариантов. Вообще, общая позиция компилятора .net - я компилирую так как написано, а всё, что можно соптимизировать должен сделать jit-компилятор.

Ответ 3



Сам по себе факт потери производительности надо еще доказать - измерить. Вы исходите из предположения что boxing - это "плохая, медленная" операция. Что почему-то воспринимается как "она настолько плохая и медленная что лучше умереть быстро, чем наблюдать как .NET медленно-медленно боксирует int!". И почему-то автоматически предполагаете что ToString() - это операция быстрая (ну уж точно быстрее боксинга!). Это совсем не так. Цена упаковки в .NET состоит из: цены создания объекта. цены копирования значения в кучу. цены сборки мусора. С точки зрения .NET куча - это длинный бесконечный массив. Для этого массива есть адрес границы "занятой памяти" (*). Как-то так: [a] [b] [c] [*] [ ] [ ] [ ] Так вот, выделение нового объекта - это просто увеличение этого адреса на размер объекта и .. все! В случае int - это практически увеличение границы на 4 (на самом деле на чуть больше чем 4, но не суть важно). [a] [b] [c] [ ] [*] [ ] [ ] После этого по адресу нового объекта копирутеся значение: [a] [b] [c] [5] [*] [ ] [ ] Как это выглядит в коде: Int32 v = 5; 00340471 mov ecx,737027C0h 00340476 call 002E30C8 // вызов new, вернет адрес нового объекта в eax 0034047B mov dword ptr [eax+4],5 Т.е. первые две состовляющие боксинга - копеечные операции - это, практически инкремент и mov. С третьей чуть сложнее: Как в .NET работает сборщик мусора: находятся все достижимые объекты. предположим, это a и c [a] [b] [c] [5] [*] [ ] [ ] они перекладываются поплотнее к началу кучи [a] [с] [c] [5] [*] [ ] [ ] адрес границы меняется на последний переложенный объект: [a] [c] [*] [ ] [ ] [ ] [ ] как при этом наличие мусора b и 5 повлияло на время сборки? никак! Стоит ли заменять инкремент и mov на вызов ToString()? Измеримой разницы не будет. А если и будет - то совсем не факт, что вызов ToString() будет работать быстрее - он ведь тоже создает объект в куче. Т.е. ускорение должно наступить за счет замены mov на преобразование числа в строку? Очень маловероятно. А вот читабильность кода точно пострадает. UP: На моей машине разница 0.0000094 миллисекунд в пользу ToString(). Это примерно секунда на 100 000 000 операций. Сомневаюсь, что разницу кто-нибудь заметит в реальной жизни.

Комментариев нет:

Отправить комментарий