Управление на паметта и ресурсите



страница6/7
Дата25.02.2017
Размер0.85 Mb.
#15739
1   2   3   4   5   6   7

Слаби референции


Слабите референции (weak reference) представляват референция към обект, която подлежи на почистване от системата за почистване на боклук след като всички силни референции към обекта отпаднат. Имайте предвид, че при недостиг на памет първо се почистват обектите сочени от слаби референции.

Създаване на слаба референция


Референция към достижим обект (обект с корен) се нарича силна референция (strong reference). Силна референция може да се превърне в слаба, като се създаде инстанция на класа System.WeakReference и се подаде силната референция като параметър на конструктора. Обаче само конструирането на WeakReference обект не прави силната референция слаба. За целта, на всички корени, сочещи обекта, трябва да се присвои null. Ето един пример:

// Създаваме нов обект и го присвояваме на променлива.

// Това създава силна референция.

object obj = new object();
// Създаваме слаба референция към обекта

WeakReference wr = new WeakReference(obj);


// Тук все още имаме силна референция към обекта. Премахваме я.

obj = null;


Получаване на силна референция от слаба


Слабата референция също сочи към достижим обект, наричан цел (target) и може да се превърне отново в силна референция като се присвои стойността от свойството Target на съответната променлива. Свойството IsAlive показва дали обектът вече не е почистен.

if (wr.IsAlive)

{

// Обектът още не е почистен, създаваме силна референция



object obj = wr.Target;

}

else



{

// Обекта вече е почистен от GC, wr.Target е null

}

Сценарии за употреба на слаби референции


Слабите референции се използват за съхранение на данни, които не са критични за приложението. Под некритични имаме предвид, че приложе­нието не се нуждае от тези данни за константно време и може да изчака малко време за тяхното пресъздаване.

Например едно приложение може да обхожда твърдия диск и да прави списък с всички файлове, открити във всички директории и техните поддиректории. Този списък би могъл да заема доста памет. Създаването на този списък е бавно, но ако той липсва, винаги може да бъде създаден отново (с цената на повторно обхождане на всички директории по диска). За съхранението на списъка може да се използва слаба референция. Така когато списъкът потрябва и слабата референция не го е освободила поради недостиг на памет, списъкът ще може да се използва директно. В противен случай ще трябва да се изгради отново.

Имайте предвид, че една слаба референция има управлявана и native част. Управляваната част е самият клас WeakReference. В конструктора си той създава GC манипулатор (което е native частта) и вмъква запис в таблицата за манипулаторите на AppDomain-a си. Обектът, към който сочи слабата референция ще умре, когато няма силни референции към него, а също и самата слаба референция, когато няма силни референции към нея (все пак тя също е управляван обект).

Слабата референция съдържа манипулатор с големината на 1 указател (32 бита на 32-битови архитектури), едно булево поле и GC манипу­латора, който също е с големината на 1 указател, така че ако имате много малък обект, да кажем съдържащ 1 int поле, вашият обект ще изразходва 12 байта памет (размера на минималния обект). Същият обект, вмъкнат в WeakReference, ще харчи поне още 9 байта. Следователно не си създавайте сами ситуации в които създавате много слаби референции, сочещи малки обекти.


Ефективно използване на паметта





Люк: Ще опитам.




Йода: Не. Не опитвай. Направи го. Или не. Няма опитване.

Едно от основните разлики между управлявания и неуправлявания код е автоматичното управление на паметта. Вие заделяте нови обекти, но garbage collector автоматично ги освобождава за вас когато вече не се използват. GC се изпълнява от време на време, често незабележимо, като най-често спира вашето приложение само за милисекунда или по-рядко две или повече.

Ако следвате съветите в тази тема, общата цена на използването на garbage collector ще бъде незабележима, конкурентна или дори по-добра от традиционните в C++ new и delete. Амортизираната цена на създаване и на по-късното освобождаване на обект е достатъчно ниска, че да е възможно да създавате десетки милиони малки обекти в секунда.

Системата на .NET за почистване на паметта предоставя изключително бързо заделяне на памет без дългосрочни проблеми с фрагментацията, но е възможно да пишете код, който да доведе до по-малка от оптималната й производителност.

За да постигнете най-доброто, използвайте следните утвърдени практики:


Внимавайте с абстракциите


.NET Framework скрива толкова много детайли, че болшинството от програмистите без предишен опит с езици от по-ниско ниво нямат почти никаква представа за цената на техния код.

Може да заредите 1 мегабайт XML от Web site с няколко реда код, нали? Толкова е лесно! Наистина. Толкова е лесно да похарчите мегабайти памет, докато зареждате XML-а само за да използвате няколко елемента от него. В C или C++ е толкова “болезнено”, че щеше да се позамислите и да проектирате или използвате API с push (SAX) или pull (XmlReader) модел. В .NET Framework просто можете да заредите целия XML на един раз. Може би го правите отново и отново. После може би вашето приложение не изглежда вече толкова бързо. Може би трябваше да помислите за цената на тези лесни за използване методи...


Изберете най-доброто за целта API или алгоритъм


Нека си представим, че трябва да напишем проста конзолна програма, която отпечатва последните N реда от даден текстов файл, посочен на командния ред14. Може да напишете програмата по много начини:

  • Заделяте масив с N елемента и докато четете файла ред по ред, попълвате масива като цикличен буфер (това е добро решение по отношение заделянето на памет, но не толкова добро по отношение на скоростта, ако файлът е голям, а освен това за всеки ред ще създадете нова инстанция на класа System.String).

  • Прочитате файла на един дъх (с помощта на метода ReadToEnd() на класа StreamReader) и го сканирате отзад напред, отброявайки броя на достигнатите знаци за нов ред, докато достигнете до N (това решение е лошо по отношение на използване на паметта, но дава идеята за следващото решение).

  • Отваряте файла в двоичен режим и четете отзад напред докато преброите N реда или не достигнете началото на файла, като или

    • поставяте редовете отзад напред в предварително заделен масив от N елемента (жертвайки памет за сметка на скоростта) или

    • препрочитате файла от достигнатата позиция до края му, отпе­чатвайки всеки ред (жертвайки скорост за сметка на паметта).

Ако знаете, че един ред не надминава например 80 символа, може да прочетете наведнъж N * 80 символа в един буфер, да затворите файла и да преминете през буфера, отпечатвайки всеки ред.

Поуката от примера е, че може да решите конкретна задача по много различни начини и обикновено при всеки от тях се налага да вземете важното решение дали да пожертвате повече памет за сметка на скоростта или обратно. Универсална рецепта няма и решението зависи от нуждите на вашето приложение.


Няма безплатен обяд


Почистването на обектите от GC, особено на тези от поколение 0 е много бързо, но не е “безплатно”, дори ако голяма част от обектите са “мъртви”. За да се открият (и маркират) живите обекти първо трябва да се приспят нишките и да се обходят техните стекове и други структури, за да се съберат коренните референции към обекти в хийпа.

Заделянето на памет за обект също не е безплатно. Обектите заемат място. Неумереното създаване на обекти води до по-често задействане на GC. Дори по-лошо, ненужното задържане на референции към безполезни графи от обекти ги поддържа "живи".

Може да срещнете скромни програмки с печални working sets от по над 100 MB, чиито автори отричат тяхната вина и вместо това присвояват лошата производителност на някакъв мистериозен, неразгадаем (и следо­вателно нерешим) проблем, свързан със самия управляван код. Трагично! Обикновено в такива случаи след час изучаване на проблема с CLR Profiler и промяна на няколко реда код, програмите намаляват изискването си за динамична памет с повече от 10 пъти. Ако имате проблем с големината на вашия working set, първата ви стъпка трябва да бъде да погледнете работата в паметта във вашето приложение.

Не създавайте обекти без да е необходимо


Програмистите често пъти неволно създават повече и повече обекти просто защото автоматичното управление на паметта решава много запле­тени проблеми и грешки при заделяне и освобождаване на обекти и просто защото е бързо и удобно.

Типичен пример за безсмислен разход на памет е проектирането на т. нар. one-shot класове. Такъв клас има един или повече конструктора, приемащи всички необходими аргументи за да се извърши някаква операция и един метод, изпълняващ операция нуждаеща се от тези аргументи. Такива класове заемат излишна памет вместо да реализират функционалността си като статичен метод. Повечето от вас ще разпознаят шаблона за дизайн Command. Той има големи предимства, когато се използва разумно и при необходимост, но за съжаление, по-неопитните програмисти го използват по-често от необходимото.

Пример за one-shot клас, проектиран безсмислено да бъде инстанциран за да бъде използван:

class FileDownloader

{

private readonly string mUrl;



private readonly string mPath;
public FileDownloader(string aFileUrl, string aLocalPath)

{

mUrl = aFileUrl;



mUath = aLocalPath;

}
public void Download()

{

// сваляме файла от mUrl и го записваме в директория mPath



// …

}

}



Същата функционалност може да бъде проектирана като статичен метод, който не изисква създаването на излишен обект.

Пример:


class FileDownloadHelper

{

public static void Download(string aUrl, string aPath)



{

// сваляме файла от aUrl и го записваме в директория aPath

// …

}

}



Ако по-късно решите, че се нуждаете от клас, който капсулира парамет­рите на заявката, с цел ползването му в шаблона за дизайн Command, винаги може да напишете клас FileDownloadCommand, който делегира към FileDownloadHelper.

Ако искате да пишете наистина бърз управляван код, създавайте обектите с мисъл и само когато е необходимо. Това се отнася още повече за онези от вас, които проектират програмни интерфейси (APIs). Възможно е да проектирате тип и неговите методи така, че да изисква от клиентите да създават и освобождават обекти непрекъснато. Не го правете!


Създавайте обекти където е необходимо


Въпреки, че компилаторът на вашия език и JIT компилаторът по време на изпълнение правят оптимизации, те не са перфектни15. Долният пример показва как може да напишете код, който се изпълнява 5 и повече пъти по-бавно просто от нехайство:

for (int i = 0; i < 5000; ++i)

{

int buffer[] = new int[65536];



// Правим някакво изчисление с buffer

}


Същият код може да се оптимизира значително, като просто се извади заделянето на памет преди цикъла:

int buffer[] = new int[65536];

for (int i = 0; i < 5000; ++i)

{

// Правим някакво изчисление с buffer



}

Не създавайте обекти с излишни полета


Това, че на пръв поглед ви изглежда нормално да направите общ клас за възлите и листата на едно дърво, не означава, че трябва да имплемен­тирате структурата от данни дърво по този начин. Ето типична имплемен­тация на дърво, срещана в огромен брой проекти:

public sealed class LameTreeNode : IEnumerable

{

private string mName;



private ArrayList mChildren;
public LameTreeNode(string aName)

{

mName = aName;



mChildren = new ArrayList();

}
public string Name

{

get


{

return mName;

}

}
public int Count



{

get


{

return mChildren.Count;

}

}
public IEnumerator GetEnumerator()



{

return mChildren.GetEnumerator();

}
public void Add(TreeNode aChild)

{

mChildren.Add(aChild);



}

}


Големите проблеми тук са поне два:

  • Конструкторът без параметри на ArrayList създава по подразбиране масив от 16 елемента – похабена памет за създаването на масива. Ако елементът е стойностен тип, всички стойности ще бъде опако­вани, което допълнително увеличава изискването за памет.

  • Не се знае дали на възела ще бъдат добавени листа, т.е. дали той самият няма да остане листо, но паметта за масива се харчи във всички случаи. Това, разбира се, опростява имплементацията на Count, GetEnumerator() и Add(…), но цената не е малка.

Същият клас може да се преправи така, че да изразходва по-малко допълнителна памет за елементите, които са листа:

public sealed class DecentTreeNode : IEnumerable

{

private string mName;



private ArrayList mChildren;

private static IEnumerator mNullEnumerator =

(new TreeNode[0]).GetEnumerator();

private const int DEFAULT_CAPACITY = 4;


public DecentTreeNode(string aName)

{

mName = aName;



}
public string Name

{

get



{

return mName;

}

}
public int Count



{

get


{

return mChildren == null ? 0 : mChildren.Count;

}

}
public IEnumerator GetEnumerator()



{

return mChildren == null ?

mNullEnumerator :

mChildren.GetEnumerator();

}
public void Add(TreeNode aChild)

{

if (mChildren == null)



{

mChildren = new ArrayList(DEFAULT_CAPACITY);

}

mChildren.Add(aChild);



}

}


Както винаги, класът може да се подобри поне по още два начина:

  • Ако знаете минимумът и/или максимумът на броя на листата на възела, можете да инициализирате ArrayList член-променливата с капацитет, подаден в конструктора на DecentTreeNode.

  • Можете да създадете абстрактен клас Node и конкретни класове за възли и листа. В този случай се подразбира, че знаете кога ще създавате обект от единия или другия тип.

Не инициализирайте полетата в конструкторите


След като паметта за новосъздаден обект се задели, CLR го инициализира (конструира). CLR гарантира, че всички референтни полета са предвари­телно инициализирани с null и всички примитивни скаларни полета са инициализирани с 0, 0.0, false или съответната нулева стойност. Следователно е ненужно повторно да ги инициализирате в дефинираните от вас конструктори. Длъжни сме да ви предупредим, че в текущата си имплементация компилаторът не оптимизира и не премахва повторни инициализации от конструкторите ви.

Не проектирайте излишно дълбоки йерархии


Вторият принцип на обектно-ориентираният дизайн гласи16:

Предпочитайте композицията на обекти пред наследяването на клас.

Голяма част от програмистите обаче (особено тези с предишен опит с езици, които не поддържат наследяване), злоупотребяват с наследяване­то, веднъж след като придобият някакъв опит с ООП.

След като CLR задели памет за даден обект и инициализира полетата му, се извиква конструкторът на съответния тип. Конструкторът на всеки тип, дефиниран от програмиста или компилатора, първо извиква конструктора на базовия си тип, после изпълнява дефинираната от програмиста инициализация.

На теория, това може да бъде скъпо като време на изпълнение, тъй като ако имаме клас Е наследяващ D, наследяващ C, наследяващ B, наследя­ващ A (наследяващ System.Object), тогава конструирането на Е ще предизвика пет извиквания на конструктори. На практика нещата не са толкова зле, тъй като компилаторът слива в едно (inline) извикванията към празни конструктори на базовите класове.


Кеширани и некеширани ресурси


На места в .NET Framework стратегията за управляване на ресурси не е добре замислена. Да вземем например следните два реда код:

Brush brush = Brushes.White;

Font font = SystemInformation.MenuFont;



За първия ред не се изисква да извикате brush.Dispose(), тъй като колекцията Brushes ви връща кеширано копие. На втория ред, обаче има проблем, тъй като font обектът, върнат от свойството MenuFont е новосъз­даден и като такъв трябва да му извикате метода Dispose() след като приключите работата си с него.

Очевидно ли е това от кода? Не. Споменато ли е някъде в документа­цията? Не. Внимавайте! Ако не сте сигурни, проверявайте с Reflector. Използвайте следното лесно за запомняне правило, когато проектирате вашите типове:





Ако инстанцията на обекта, който връщате, е кеширана от вас, изпол­звайте свойство за извличането й. Ако създава­те и връщате нова инстанция, използвайте метод. Имену­вайте методите и свойствата по начин, който не оставя съмнение за това дали върнатият обект е новосъздаден и се нуждае от освобождаване.

Ето един пример:

class Brushes

{

public static Brush CreateSolidBlackBrush()



{

// Отговорността за освобождаване на ресурса е на

// извикващия този метод

return new SolidBrush(Color.Black);

}
public static Brush CachedBlack

{

// Отговорността за освобождаване на ресурса е на



// програмиста, проектирал класа Brushes

return mCachedBlackBrush;

}

}

Заделете цялата памет, нужна за създаването на структура от данни, наведнъж


Напишете програма, която създава масив от 1 милион int елемента и прост свързан списък от 1 милион възли, като всеки възел обвива един int елемент. После измерете времето, нужно да съберете първите хиляда, 10 хиляди, 100 хиляди и 1 милион елемента. Повторете всеки цикъл много пъти (вкарайте го във външен цикъл) за да измерите скоростта.

Указва се, че колкото повече данни обхождате, толкова по-бавно се държи свързаният списък. Версията с масива е винаги по-бърза, въпреки, че изпълнява два пъти повече инструкции. За 100 хиляди елемента, версията с масива е до 7 пъти по-бърза.

Защо? Първо, много по-малко възли се поместват в който и да е кеш на процесора. Всички заглавни части на обектите (8 байта) и връзките към следващия елемент (4 байта на 32-битова машина) заемат ненужно място. Вярно, че с версията със свързания списък заемате памет, само когато ви е нужна, но:


  • Версията с масива заема 4 пъти по-малко памет (само 4 байта за 1 елемент, вместо 8 (заглавна част) + 4 (int числото) + 4 (връзката към следващия елемент) = 16 байта) и съответно по-голям брой елементи се поместват в кеша на процесора, а той е многократно по-бърз от коя да друга е памет.

  • Днешните модерни процесори могат както да изпълняват инструк­циите не в реда както са им подадени, така и да се опитват да прочитат данни преди да сме им ги поискали. В нашия случай, процесорът може да прочете още данни, преди да сме ги поискали за масива си.

Версията със свързания списък е винаги по-бавна, тъй като преди възела да достигне кеша на процесора, той не може да прочете следващия възел, т.е. имаме четене само на 1 възел в даден момент.

Каква е поуката тук?


Имайте предвид кеша на процесора в дизайна си


  • Експериментирайте и премервайте. Трудно е, както да се предвидят страничните ефекти във всяка една програма, така и да се дадат някакви специфични указания как да проектирате най-ефективно структурите от данни, които ползвате.

  • Близките данни се достъпват по-бързо, предпочитайте масиви пред свързани списъци.

  • Когато масивите не ви вършат работа, използвайте хибридни струк­тури, например списъци от по-малки масиви, масиви от масиви и т.н.

  • Тъй като garbage collector запазва относителния ред на обектите, тези обекти, които за създадени заедно по едно и също време (и на същата нишка) обикновено остават заедно (и близо един до друг) в паметта. Използвайки това знание можете внимателно да създадете обекти, които ще се използват заедно, така, че да споделят общо място в кеша на процесора.

  • Можете да разделите обектите си на “топли” и “студени” части, като топлите съдържат често използваните данни, а студените – рядко използваните – и могат да влизат и излизат от кеша на процесора без това да е осезаемо за приложението ви.

Използвайте следния модел за цената на пространството


  • Размерът на стойностните типове обикновено е общият размер на всичките му полета, като полетата, които са по-малки от 4 байта се подравняват до 4 байта.

  • Можете да имплементирате обединения на последователни полета (unions), като използвате атрибутите [StructLayout(LayoutKind. Explicit)] и [FieldOffset(n)].

  • Размерът на референтните типове е 8 байта (размерът на заглавната част на всеки референтен обект) + размера на всичките им полета, подравнен до 4-байтова стойност, като по-малките от 4 байта полета се подравняват.

  • В C# при декларация на изброим тип може да укажете произволен целочислен тип, така че е възможно да дефинирате 8, 16, 32 и 64-битови изброими типове.

  • Както и в C/C++ винаги може да изцедите малко размера на по-голям обект, като внимателно прецените и промените типовете на целочислените му полета.

  • Може да използвате CLR Profiler за да определите размера на референтен тип.

Отражение на типовете


Избягвайте използването на отражение на типовете (reflection), когато е възможно. Ако се питате каква е цената на reflection, тя е такава, че не можете да си я позволите. Ето защо и класът ObjectPool, даден като пример в една от следващите точки, не използва reflection за да създава обекти. Отражението на типовете е полезно и мощно средство, но сравнено код, преминал през JIT компилатора е много пъти по-бавно.

Премахнете създаването на временни обекти, които могат да бъдат избегнати с цената на малко повече код


Например, ако трябва да сортирате CSV файл (файл, в който данните в редовете са разделени със запетаи) и първата колона съдържа ключа за сортиране, може да напишете следния клас за сравнение на редовете:

sealed class SlowComparer : IComparer

{

private readonly char mDelimiter;


public SlowComparer(char aKeyDelimiter)

{

mDelimiter = aKeyDelimiter;



}
public int Compare(object aObj1, object aObj2)

{

string key1 = (aObj1 as string).Split(mDelimiter)[0];



string key2 = (aObj2 as string).Split(mDelimiter)[0];

int len = Math.Min(key1.Length, key2.Length);

return String.Compare(key1, 0, key2, 0, len);

}

}



Методът за сравнение Compare(), показан в горната фигура първо разделя реда на колони, използвайки метода Split() на класа System.String, а после ползва първата колона като ключ. Извикването на Split() създава масив състоящ се от низове, като както масива, така и низовете са заделени в динамичната памет.

С малко повече усилия, при положение, че знаете, че редовете са разделени със запетаи, може да извлечете ключовете за сортиране, без да създавате ненужен разход на памет:



sealed class FastComparer : IComparer

{

private readonly char mDelimiter;


public FastComparer(char aKeyDelimiter)

{

mDelimiter = aKeyDelimiter;



}
public int Compare(object aObj1, object aObj2)

{

string str1 = aObj1 as string;



string str2 = aObj2 as string;

int pos1 = str1.IndexOf(mDelimiter, 0);

int pos2 = str2.IndexOf(mDelimiter, 0);

int len = Math.Min(pos1, pos2) + 1;

return String.Compare(str1, 0, str2, 0, len);

}

}



Сортирането на един и същ неподреден масив, състоящ се от 100 000 низа от по 100 символа, с ключ между 5 и 10 символа с FastComparer е повече от 20 пъти по-бързо от това с помощта на SlowComparer.

Минимизирайте броя на записите на указатели към вашите обекти, особено онези, които се правят в по-стари обекти


Малко по-горе обяснихме как кешът на процесора не трябва да се пренебрегва. Когато в поле, намиращо се в по-стар обект (такъв, който се е преместил в по-горно поколение) запишете референция вие първо предизвиквате GC да обнови таблицата с референции, неподлежащи на GC и второ, но не по-малко важно, “докосвате” стар обект, който е много вероятно да е излязъл от кеша на процесора. Внимателно прегледайте дизайна си, тъй като би трябвало по-често да ви се налага да записвате референция към стар обект в поле на нов, отколкото обратно.

Използвайте възможно най-малко финализатори


Ако е нужно разбийте обектите си на подобекти, за да го постигнете – това също важи за разделянето на топли и студени обекти.

Запознайте се с инструмента CLR Profiler


Преглеждайте кода, имплементиращ често използвани структури от данни и го оптимизирайте с CLR Profiler за да сте сигурни, че употребявате паметта ефективно и за да работи GC най-добре за вас.

CLR Profiler (бившият Allocation Profiler) е полезна програма, написана от екипа на Microsoft, която използва програмните интерфейси за профили­ране на CLR код (CLR profiling APIs), събирайки и визуализирайки по подходящ начин информация за събития като:



  • извикване на метод

  • връщане от извикан метод

  • заделяне на памет за обект

  • почистване на паметта и др.

След като необходимата информация от събитията е събрана, можете да използвате CLR Profiler за да разгледате заделянето на памет и пове­дението на GC за вашето приложение, включително взаимодействи­ето между йерархичното извикване на методите ви и шаблоните, по които заделяте памет.

Изучаването на CLR Profiler си струва, защото за много приложения, имащи проблеми с производителността, разбирането на шаблона на заделянето на памет за вашите данни помага за намаляването на working set паметта и за създаването на бързи компоненти и приложения.

CLR Profiler (с включена документация) може да се свали свободно от: http://www.microsoft.com/downloads/details.aspx?FamilyId=86CE6052-D7F4-4AEB-9B7A-94635BEEBDDA&displaylang=en

Проектирайте, мислейки за ефективността


За някои проекти производителността е с малко или без значение, а за други тя е най-важната характеристика на продукта. Преждевременната оптимизация е корена на всяко зло, но и неханието по отношение на ефективността води до много проблеми. Вие сте професионалисти, ето защо трябва да знаете цената на нещата. Ако не я знаете, пък и дори да си мислите, че я знаете – премервайте често.




Сподели с приятели:
1   2   3   4   5   6   7




©obuch.info 2024
отнасят до администрацията

    Начална страница