none
Принципы хранения значимых типов в обобщенных коллекциях и занимаемая память. RRS feed

  • Вопрос

  • Здравствуйте коллеги.

    Вопрос, скорее говоря, имеет под собой больше теоретическое понимание сути того, как работает CLR. Решил подтянуть знания, разобраться более подробно в упаковке/распаковке, управлением памяти и далее. Далее уже по сути:

    Как известно, значимые типы, если они не входят в состав ссылочного типа всегда располагаются при создании в стеке. Теперь, у нас будет набор таких значимых типов, которые мы захотим сохранить для начала в обычной необобщенной коллекции ArrayList. При добавлении в коллекцию автоматически будет происходить упаковка нашего значимого типа (а как я понял, по простому - это обертка нашего значимого типа объектом object, создание области памяти равного размеру значимого типа в куче, и копирование туда значений полей) и соханение этой ссылки внутри структуры ArrayList'а. В принципе теперь понятно, что после того, как приложение будет выгружено из памяти, GC пробежится по объектам в куче, и уничтожит сначала все обертки значимых типов и затем сам объект списка.

    Тогда как обобщенная коллекция видимо работает несколько по другому принципу. Если добавлять в коллекцию значимые типы, то не происходит их упаковка, следовательно, как я понимаю, не аллоцируется доп. память в куче (может здесь я фундаментально не прав?). Хотя с другой стороны, время жизни объектов в коллекции определнно должно быть дольше чем время жизни своих собратьев объявленных локально в методах. Где же тогда хранятся объекты обобщенной коллекции? Все же их копии хранятся в куче, так как изменяя их поля мы не меняем поля исходных объектов. Но в таком случае, почему сборщик мусора выполняя сборку не указывает на то что эти объекты были удалены из памяти?

    Может быть получилось сумбурно, но надеюсь смысл вопроса более менее понятен.



    13 сентября 2012 г. 6:32

Ответы

  • Тогда как обобщенная коллекция видимо работает несколько по другому принципу.

    Да, при введении дженериков были внесены изменения в саму среду исполнения - CLR. Со значимыми типами она работает сильно по-другому, в сравнении со ссылочными. Кстати, чтобы на всю катушку использовать эти возможности, у многих методов (например, Console.Write и WriteLine) есть множество перегрузок со всеми простыми значимыми типами.

    Если добавлять в коллекцию значимые типы, то не происходит их упаковка, следовательно, как я понимаю, не аллоцируется доп. память в куче (может здесь я фундаментально не прав?).

    При хранении значимых типов в обобщённых коллекциях, упаковка действительно не происходит. Но память конечно же выделяется, и именно в куче.

    Хотя с другой стороны, время жизни объектов в коллекции определнно должно быть дольше чем время жизни своих собратьев объявленных локально в методах. Где же тогда хранятся объекты обобщенной коллекции?

    В куче. Но не ссылки на значимые типы, а сами значения.

    Все же их копии хранятся в куче, так как изменяя их поля мы не меняем поля исходных объектов. Но в таком случае, почему сборщик мусора выполняя сборку не указывает на то что эти объекты были удалены из памяти?

    Да, хранятся именно копии.

    А сам последний вопрос я не понял.

    Если в приложении больше нет ссылок на обобщённую коллекцию, то при очередном пробуждении GC удаляет эту коллекцию со всем содержимым.


    • Изменено Petalvik 13 сентября 2012 г. 13:47
    • Помечено в качестве ответа Eugene_Olisevich 14 сентября 2012 г. 4:48
    13 сентября 2012 г. 13:44
  • Да, видимо не прочитали или прочитали невнимательно ссылки данные мной, особенно последнюю. "Как известно, значимые типы, если они не входят в состав ссылочного типа всегда располагаются при создании в стеке." - это не так. "Т.е. исходя из этого теста, использование в качестве ключа пользовательского значимого типа, а он как известно "легче" в плане нагрузки на память и операции с ним" - это тоже не всегда так. Завтра рабочий, послезавтра нет, постараюсь более развёрнуто ответить. А пока времени нет.
    • Помечено в качестве ответа Eugene_Olisevich 14 сентября 2012 г. 4:37
    • Снята пометка об ответе Eugene_Olisevich 14 сентября 2012 г. 4:37
    • Помечено в качестве ответа Eugene_Olisevich 14 сентября 2012 г. 4:47
    13 сентября 2012 г. 20:11
    Модератор

Все ответы

  • Вам надо прямо сюда, сюда и сюда, а потом и весь блог если время будет. Там все ответы на ваши вопросы.
    13 сентября 2012 г. 6:55
    Модератор
  • Ну, сейчас еще раз перечитаю, хотя уже читал, и попутно досконально перечитал CLR via С#... но всеже четкого понимания насчет вышеупомянутого вопроса не получил.

    Пойдем по второму кругу :)

    13 сентября 2012 г. 7:05
  • Тогда как обобщенная коллекция видимо работает несколько по другому принципу.

    Да, при введении дженериков были внесены изменения в саму среду исполнения - CLR. Со значимыми типами она работает сильно по-другому, в сравнении со ссылочными. Кстати, чтобы на всю катушку использовать эти возможности, у многих методов (например, Console.Write и WriteLine) есть множество перегрузок со всеми простыми значимыми типами.

    Если добавлять в коллекцию значимые типы, то не происходит их упаковка, следовательно, как я понимаю, не аллоцируется доп. память в куче (может здесь я фундаментально не прав?).

    При хранении значимых типов в обобщённых коллекциях, упаковка действительно не происходит. Но память конечно же выделяется, и именно в куче.

    Хотя с другой стороны, время жизни объектов в коллекции определнно должно быть дольше чем время жизни своих собратьев объявленных локально в методах. Где же тогда хранятся объекты обобщенной коллекции?

    В куче. Но не ссылки на значимые типы, а сами значения.

    Все же их копии хранятся в куче, так как изменяя их поля мы не меняем поля исходных объектов. Но в таком случае, почему сборщик мусора выполняя сборку не указывает на то что эти объекты были удалены из памяти?

    Да, хранятся именно копии.

    А сам последний вопрос я не понял.

    Если в приложении больше нет ссылок на обобщённую коллекцию, то при очередном пробуждении GC удаляет эту коллекцию со всем содержимым.


    • Изменено Petalvik 13 сентября 2012 г. 13:47
    • Помечено в качестве ответа Eugene_Olisevich 14 сентября 2012 г. 4:48
    13 сентября 2012 г. 13:44
  • ОК. Теперь более менее понятно, как ведет себя среда со значимыми и ссылочными объектами.

    Однако, возник очень интересный момент во время исследования. Предлагаю высказывать идеи, отчего происходят результаты следющей небольшой тестовой программы.

    using System;
    using System.Collections.Generic;
    using System.Text;
    
    namespace ConsoleApplication2
    {
    	public class TestClass
    	{
    		public int TestField1;
    		public int TestField2;
    
    		public override int GetHashCode()
    		{
    			return TestField1 ^ TestField2;
    		}
    	}
    
    	public struct TestStruct
    	{
    		public int TestField1;
    		public int TestField2;
    
    		public override int GetHashCode()
    		{
    			return TestField1 ^ TestField2;
    		}
    	}
    
    	class Program
    	{
    		static void GCTextDump()
    		{
    			GC.Collect();
    			Console.WriteLine(String.Format("{0}-{1}-{2}",
    				GC.CollectionCount(0),
    				GC.CollectionCount(1),
    				GC.CollectionCount(2)));
    		}
    
    		static void Main(string[] args)
    		{
    			TestClass TestObject1 = new TestClass();
    			Dictionary<TestClass, int> TestClassDict = new Dictionary<TestClass, int>();
    			TestClassDict.Add(TestObject1, 1);
    
    			TestStruct TestObject2 = new TestStruct();
    			Dictionary<TestStruct, int> TestStructDict = new Dictionary<TestStruct, int>();
    			TestStructDict.Add(TestObject2, 1);
    
    			int a;
    			int time = Environment.TickCount;
    			for(int i=0;i<1000000;i++)
    				a = TestClassDict[TestObject1];
    			time = Environment.TickCount - time;
    			Console.WriteLine("ElapseTime" + time.ToString());
    			GCTextDump();
    
    			time = Environment.TickCount;
    			for (int i = 0; i < 1000000; i++)
    				a = TestStructDict[TestObject2];
    			time = Environment.TickCount - time;
    			Console.WriteLine("ElapseTime" + time.ToString());
    			GCTextDump();
    
    			Console.ReadKey();
    		}
    	}
    -}
    

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

    Собственно, на моей машине скомпилированная в релизе программа выдала следующий результат:

    ElapsedTime:62

    1-1-1

    ElapsedTime:157

    9-3-2

    Хотя если во втором случае объявить словарь не с ключом TestStruct, а например с обычным int (тоже значимый тип), и немного переопределить остальные методы, результат будет совершенно другим:

    ElapsedTime:62

    1-1-1

    ElapsedTime:10

    2-2-2

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

    Допустим, что память действительно потребляется от того, что при каждой итерации создается копия структуры, передающейся в качестве индексатора списку. Это действительно отъедает лишнюю память. С другой стороны, отчего тогда такая серьезная задержка в обработке. Как видно из примера, в структуре перегружена основная функция отвечающая за работу с объектами где требуется хэш, что по идее, не должно вызывать обращение к родительскому классу, не должно происходить боксингов и т.п. А тип int в подобной же ситуации работает предсказуемо быстро, не потребляя при этом лишней памяти.

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

    13 сентября 2012 г. 17:04
  • Да, видимо не прочитали или прочитали невнимательно ссылки данные мной, особенно последнюю. "Как известно, значимые типы, если они не входят в состав ссылочного типа всегда располагаются при создании в стеке." - это не так. "Т.е. исходя из этого теста, использование в качестве ключа пользовательского значимого типа, а он как известно "легче" в плане нагрузки на память и операции с ним" - это тоже не всегда так. Завтра рабочий, послезавтра нет, постараюсь более развёрнуто ответить. А пока времени нет.
    • Помечено в качестве ответа Eugene_Olisevich 14 сентября 2012 г. 4:37
    • Снята пометка об ответе Eugene_Olisevich 14 сентября 2012 г. 4:37
    • Помечено в качестве ответа Eugene_Olisevich 14 сентября 2012 г. 4:47
    13 сентября 2012 г. 20:11
    Модератор
  • "Да, видимо не прочитали или прочитали невнимательно ссылки данные мной, особенно последнюю. "Как известно, значимые типы, если они не входят в состав ссылочного типа всегда располагаются при создании в стеке." - это не так."

    Прочитал, в моем случае даже оригинал, чтобы уберечься от превратностей перевода :). Действительно, в моей формулеровке есть неточность. Полностью согласен, что расположение памяти под значимую переменную зависит от многих факторов (в случае с вашей ссылкой например, память аллоцируется в куче, из-за обертки лямбда выражением).

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

    С удовльствием выслушаю ваши варианты, почему так происходит. Почему значимый пользовательский тип явно торомзит супротив реализации на встроенном значимом типе.

    Спасибо за проявленный интерес возможно к заранее простому и обыденному вопросу!




    14 сентября 2012 г. 4:53
  • Да, интересный код получился, нужно копнуть глубже. В алгоритм извлечения ключей, посмотреть как он реализован.
    16 сентября 2012 г. 19:39
    Модератор
  • Отдыхал пару дней. Решил вернуться к вопросу.

    В который раз убеждаюсь, что использование значимых типов в дотнете не так просто, как кажется. Может правы разработчики Java, не сделавшие такие типы?

    Надо бы мне снова Рихтера полистать. Увы, не помню некоторые нюансы. Однако, поправлю собственный предыдущий ответ. Я писал, что CLR особым образом работает со значимыми типами в генерик-коллекциях. Это действительно так, но только с простыми типами, такими как int, long и т. д.

    В приведённом примере кода, если я не ошибаюсь в очередной раз, боксинг значимых типов будет (потому что TestStruct не простой тип). Отсюда и просадка в производительности. (Надо бы рефлектором код Dictionary посмотреть, а также ILDasm'ом код этого примера.)

    Любопытно было почитать вот эту свежую тему на RSDN: Простой вопрос. В значимых типах путаются даже очень опытные дотнетчики.

    17 сентября 2012 г. 9:01
  • "а также ILDasm'ом код этого примера" - уже смотрел, он одинаковый для обеих типов. Копнул до места извлечения ключей, там тоже ничего, а вот уже дальше нке успел, как сказал высше - "нужно алгоритм извлечения ключей, посмотреть как он реализован".
    17 сентября 2012 г. 9:10
    Модератор
  • Отдыхал пару дней. Решил вернуться к вопросу.

    В который раз убеждаюсь, что использование значимых типов в дотнете не так просто, как кажется. Может правы разработчики Java, не сделавшие такие типы?

    Надо бы мне снова Рихтера полистать. Увы, не помню некоторые нюансы. Однако, поправлю собственный предыдущий ответ. Я писал, что CLR особым образом работает со значимыми типами в генерик-коллекциях. Это действительно так, но только с простыми типами, такими как int, long и т. д.

    В приведённом примере кода, если я не ошибаюсь в очередной раз, боксинг значимых типов будет (потому что TestStruct не простой тип). Отсюда и просадка в производительности. (Надо бы рефлектором код Dictionary посмотреть, а также ILDasm'ом код этого примера.)

    Любопытно было почитать вот эту свежую тему на RSDN: Простой вопрос. В значимых типах путаются даже очень опытные дотнетчики.

    Тему почитаем обязательно!

    По ILdasm'у код примера я тоже глядел. сразу же. Было интересно насчет боксинга/анбоксинга, так как сразу же стал думать именно про эту дорогостоящую операцию. Не поверите :), но инструкций боксинга в асме именно этого примера не встречается нигде. А вот за кулисами, в dictionary, посмтрел ILSpy'ем... ой честно, лучше бы не смотрел. Но вот внутри там действительно допускаю что где то идет боксинг, т.к. оказывается ключи там хранятся в обычном массиве, а значит там действительно скорее всего идет неявная упаковка.

    17 сентября 2012 г. 10:00
  • Всё времени не было написать, но всё же написал. Вот и полный ответ на Ваш вопрос.
    26 января 2013 г. 9:46
    Модератор