Monday, 12 August 2013

Створення обєктів в памяті

Нещодавно провів досить цікаву презентацію на тему управління памятью в .NET аплікаціях, чим й хочу поділитись з вами в цьому пості. Через те, що тема сама по собі досить обємна, я розібю її на декілька і буду додавати їх сюди по мірі того як зявиться вільний час.
Отож сьогодні ми поговоримо про те, скільки памяті займають обєкти, де і ким вони створюються, та ще деякі цікаві особливості їх існування.
На початок хочу представити вам невеличку табличку з прикладами розмірів обєктів:

Type       

x86 size           

x64 size

object        12 24
object[]   16 + length * 4 32 + length * 8
int[]      12 + length * 4 28 + length * 4
byte[]       12 + length            24 + length
string        14 + length * 2 26 + length * 2

Отож давайте розглянемо чому object займає 12 байт і що в цих 12 байт входить.
Кожен обєкт в .NET має так званий оверхед, це службові дані, в даному випадку таких даних 8 байт. Перших чотири байти це SyncBlock наступны 4 байти MethodTable, ще 4 байти залишають пустими і в них ми можемо зберрегти дані(наприклад int, так як його розмір якраз 4 байти). Якщо ми захочемо записати ще якісь дані в обєкт, нам автоматично буде виділено ще 4 байти(х86), тобто як тільки ми перевищуємо поточну вмістимість обєкту нам автоматично виділяється ще 4 байти(х86) або більше в залежності від того обєкт якого розміру ми хочемо додати в обєкт.
Схематично пустий обєкт виглядає наступним чином:
EmptyObjectStructure
Про те що зберігаться в SyncBlock та MethodTable ми поговоримо трохи пішніше.
Сам обєкт розміщується в керованій купі, де за ним пильно слідкує збирач мусору(Garbage collector). Сама купа може містити довільну кількість обєктів, як тільки хтось з них стає не потрібним його видаляють а сама купа дефрагментується. Детальніше про купу видалення обєктів та інші речі буде в наступному пості,  поки що,  давайте зосередимось на розмірах обєктів, а саме перевіримо розміри пустого обєкті в коді:
   1: private static void Main(string[] args)

   2: {

   3:     long beforeObj = GC.GetTotalMemory(true);

   4:     object obj = new object();

   5:     long afterObj = GC.GetTotalMemory(true);

   6:     long diff = afterObj - beforeObj;

   7:  

   8:     Console.WriteLine("object: \t" + diff);

   9:     Console.ReadLine();

  10: }
Код сам по собі досить простий, винісши його в окремий метод отримаємо наступне:
   1: private static void ObjectSizes()

   2:         {

   3:             int size = 1000;

   4:             double diff;

   5:  

   6:             long beforeObj = GC.GetTotalMemory(true);

   7:             object obj = new object();

   8:             long afterObj = GC.GetTotalMemory(true);

   9:             diff = afterObj - beforeObj;

  10:  

  11:             Console.WriteLine("obj: \t\t" + diff);

  12:  

  13:             //---------------------------------------

  14:  

  15:             long beforeObjArray0 = GC.GetTotalMemory(true);

  16:             object[] array0 = new object[0];

  17:             long afterObjArray0 = GC.GetTotalMemory(true);

  18:             diff = afterObjArray0 - beforeObjArray0;

  19:  

  20:             Console.WriteLine("array0: \t" + diff);

  21:  

  22:             //---------------------------------------

  23:  

  24:             long beforeObjArray = GC.GetTotalMemory(true);

  25:             object[] arrayEmpty = new object[size];

  26:             long afterObjArray = GC.GetTotalMemory(true);

  27:             diff = afterObjArray - beforeObjArray;

  28:  

  29:             Console.WriteLine("arrayEmpty: \t" + (diff));

  30:             Console.WriteLine("Per object: \t" + ((diff - 16) / size));

  31:  

  32:             //---------------------------------------

  33:  

  34:             //PLEASE COMMENT CODE ABOVE AS GC DOESN'T ALLOW TO MEASURE NEXT array100 PROPERLY

  35:  

  36:             long before = GC.GetTotalMemory(true);

  37:             object[] array100 = new object[100]; //416 + 100 * 12

  38:  

  39:             for (int i = 0; i < 100; i++)

  40:             {

  41:                 array100[i] = new object();

  42:             }

  43:             long after = GC.GetTotalMemory(true);

  44:             diff = after - before;

  45:             

  46:             Console.WriteLine("array100: \t" + diff);

  47:             Console.WriteLine("Per object: \t" + (diff - 416) / 100);

  48:         }
В результаті отримаємо:
obj:              12       
array0:         16       
arrayEmpty: 4016   
Per object:   4        
array100:      1392  
Per object:    9.76
  

Останні два рядки не відображають реальні дані, для того щоб побачити обєм даних закоментуйте визначення розміру для попередніх обєктів.
Як бачимо дійсно пустий обєкт займає 12 байт. Масив обєктів на 4 байти більше, в ці 4 байти записано Element Type Pointer, 4 байти які для обєкту по замовчуванню вільні, містять розмір масиву. Та на даний час це не суттєво.
Я б хотів зосередитись більше на визначенні розмірів елементарних обєктів тому що, по перше це можуть спитати на інтервю а по друге це дуже просто і робити таких елементарних помилок не варто.
Отож маємо наступний класс:
   1: class EmptyWithMethods

   2: {

   3:     public void DO(int x)

   4:     { }

   5:  

   6:     public virtual void DOVirtual(int x)

   7:     { }

   8: }
Запитання досить просте, скільки займатиме цей обєкт в памяті?
З першого погляду можна подумати що дві змінні які приймають методи будуть давати нам додаткове навантаження відповідно 8 байт оверхед плюс 8 байт для двох інтів і в результаті матимемо 16 байт. Та й в принципі стає цікаво стільки займають і чи займають щось взагалі методи? Відповідь проста, пустий клас по замовчуванню займає 12 байт (4 з яких пусті і в них можна записати 1 int або 4 byte), методи відносяться до класу а не до обєтку, і як ми знаємо з книжок клас це як заготовка на основі якої створюється обєкт, отож опис класу і методів до розміру обєкту значення не мають, вони оголошені в одному місці і є спільними для всіх нащадків класу. В результаті скільки б методів не було в нашому класі, і скільки б вони параметрів не приймали наш обєт займає тих самих 12 байт! при чому 4 з них пусті.

   1: class OneInt32 { int x; }
Тут все також дуже просто як я вже писав обєкт має окрім 8 байт оверхеду ще 4 байти вільних в ці 4 байти якраз чудово вписується наш int отож наш обэкт буде мати розмір 12 байт з тою лише різницею що останні 4 байти будуть зайняті інтом навідміну від попереднього прикладу.
   1: class TwoInt32 { int x, y; }

Цей обєкт займе 16 байт так як окрім заповнених 4 пустих байт нам потрібно ще 4 байти на ще один int y.

   1: class Mixed1

   2: {

   3:     int x;

   4:     byte b1;

   5: }
Цей приклад спершу може здатись простим клас (8 байт) + x (4 байти) + b1 (1 байт) = 13 байт, але слід памятати що для х86 память виділяється кусками по 4 байти. Беручи це до уваги давайте порахуємо:
клас (8 байт) + x (4 байти) + b1 (1 байт) = 16 байт  тому що для зберігання обєкту виділиться мінімальний обсяг памяті в 4 байти і лише один з цих 4 байтів буде зайнятий а ще три лишаться вільними.
   1: class Mixed1

   2: {

   3:     int x;

   4:     byte b1, b2, b3, b4;

   5: }
Цей обєкт також займе лише 16 байт тому місце під змінні b1, b2, b3, b4 буде відповідно в байтах від 13-го до 16-го включно.
Знаючи ці основи можна легко обчислити розміри обєктів, а якщо знати в що перетворюються стандартні проперті, то можна вирахувати розмір і більш важких типів.

String

Уважний читач мав би звернути увагу на те що розмір стрічки зазначено 14 байт +… і реально в памяті він займає 14 байт, діло в тому що стрінги це особливий тип даних в .NET і він працює трохи по іншоиму а ніж звичайні обєкти. перших 8 байт ті самі що й в звичайному обєкті, в наступні 4 записано розмір стрічки, а останні 2 байти це unicode NULL terminator. Він використовується для того щоб передавати ці стрічки в Win32 API. Отакий то нюанс)

Overhead

Ну і на останок трохи про такзваний оверхед. Перші 4 байти це посилання на синкблок, який використовується для синхронізації в мультипотокових аплікаціях та містить деякі інші службові дані. Наступних 4 байти містять інформацію про тип, це фактично його опис. Детальніше про це планую написати в наступних постах.
Код на гітхабі: https://github.com/BatsIhor/ObjectSizeDemo/