Regula 50: Când este logic să înlocuiți și să ștergeți
Să revenim la elementele de bază. În primul rând, de ce ar trebui cineva să înlocuiască versiunea compilatorului cu operatorul nou și operatorul șterge? Există cel puțin trei cauze comune.
• Îmbunătățirea eficienței. Versiunile operatorilor noi și ștergători, furnizați împreună cu compilatorul, sunt universali. Acestea ar trebui să fie acceptabile atât pentru programele de lungă durată (de exemplu, serverele Web), cât și pentru programele care rulează mai puțin de o secundă. Acestea ar trebui să poată gestiona o serie de cereri de alocare a unor blocuri mari de memorie, blocuri mici și amestecuri de ambele. Ei trebuie să se adapteze la o gamă largă de cazuri de utilizare - de la alocarea dinamică a câteva blocuri mari care există pe întreaga durată a programului, să aloce și dealocă de memorie pentru un număr mare de obiecte mici, cu o durată de viață scurtă. Acestea ar trebui să prevină fragmentarea „haldă“, pentru că dacă nu, atunci în cele din urmă va fi imposibil de a satisface cererea de a aloca un bloc mare de memorie, chiar și în cazul în care valoarea totală a acestei disponibile, dar distanțate printr-o multitudine de secțiuni mici.
Având în vedere toate cerințele pentru managerii de memorie, nu este surprinzător faptul că operatorii noi și ștergeți furnizați cu compilatorii respectă strategia medie. Ele funcționează destul de bine pentru toată lumea, dar în mod optim - pentru nimeni. Dacă aveți o idee bună despre modul în care este utilizată memoria dinamică în programul dvs., veți putea scrie propriile versiuni ale operatorilor noi și ștergeți, care sunt mai eficienți decât cei standard. Prin "superioritate" vreau să spun că acestea funcționează mai repede (uneori multe ordini de mărime) și necesită mai puțină memorie (până la 50%). Pentru unii, dar în nici un caz toate, aplicațiile, înlocuirea de noi și șterse cu versiunile proprii este o modalitate simplă de îmbunătățire a performanțelor tangibile.
• Să colecteze statistici de utilizare. Înainte de a începe să scrieți propriul dvs. nou și să ștergeți, este prudent să colectați informații despre modul în care programul dvs. folosește o memorie dinamică. Cum sunt dimensionate blocurile alocate? Cum se distribuie timpul lor de viață? Ordinea de alocare și eliberare respectă în principiu principiul FIFO ("first-in-out-out") sau LIFO ("last-in-first-out")? Sau nu există nici un model? Are natura utilizării memoriei în timp, adică, există diferența în ordinea alocării-eliberării memoriei între diferitele etape de execuție? Care este cantitatea maximă de memorie alocată dinamic utilizată la un moment dat?
În esență, scrierea versiunilor personalizate ale noilor și ștergerea este o sarcină destul de simplă. De exemplu, luați în considerare pe scurt modul în care este posibil să implementați un nou operator global cu control de scriere dincolo de limitele blocului selectat. Adevărat, are multe defecte, dar pentru moment nu le vom acorda atenție.
statică const int signature = 0xDEADBEEF;
typedef nesigned char Byte;
// în acest cod există mai multe defecte - vezi mai jos
void * operator nou (std: size_t size) arunca (std :: bad_alloc)
folosind namespace std;
size_t realSize = dimensiunea + 2 * sizeof (int); // măriți dimensiunea solicitată
// Blocați astfel încât să puteți plasa
void * pMem = malloc (realSize); // sunați la malloc pentru a obține memorie
// scrie semnătura primului și ultimului cuvânt al blocului selectat
* (static_cast pMem)) = semnătură;
* (reinterpret_cast (static_cast (pMem) + realSize-sizeof (int))) =
// returnați pointerul în memorie imediat după semnarea inițială
retur static_cast (pMem) + dimensiunea (int);
Majoritatea deficiențelor acestei versiuni a operatorului nou se datorează faptului că nu respectă pe deplin convențiile C ++ referitoare la funcțiile cu acest nume. De exemplu, regula 51 explică faptul că toți operatorii noi trebuie să includă o buclă pentru a apela funcția de handler nou, ceea ce nu face acest lucru. Această regulă este dedicată regulii 51, deci acum vreau să mă concentrez asupra unui punct mai subtil: alinierea.
Alinierea este importantă deoarece C ++ necesită alinierea tuturor pointerilor returnați de noul operator pentru orice tip de date. Funcția malloc se supune acelorași cerințe, astfel că folosirea indicatorului returnat de malloc este sigur. Dar mai presus de noul operator, nu returnează pointerul obținut din malloc, și returnează un pointer returnat de malloc mutat la int dimensiune. Nu există garanții că este în siguranță! În cazul în care clientul va apela operatorul nou, pentru a obține memorie, suficientă pentru a se potrivi dublu (sau dacă vom scrie operatorul nou [] pentru a aloca memorie pentru o matrice de valori duble), și apoi executați programul pe o mașină în cazul în care int este de 4 octeți și valori duble ar trebui să fie aliniate la limitele blocurilor de opt octeți, atunci, cel mai probabil, vom returna pointerul aliniat incorect. Acest lucru poate provoca crasharea programului. Sau chiar încetini-o. În orice caz, acest lucru nu este deloc ceea ce dorim.
Atenția la astfel de detalii se distinge prin managerii de memorie de calitate profesională față de cei care sunt încurcați în grabă de către programatori care sunt forțați să fie distrasi de alte sarcini. Scrie propriul manager de memorie, care aproape că funcționează, destul de simplu. A scrie un lucru care funcționează bine este mult mai complicat. În general, nu vă recomand să faceți acest lucru dacă nu este nevoie urgentă.
În multe cazuri, nu este. Unii compilatori au întrerupătoare care vă permit să debugați și să înregistrați funcțiile de gestionare a memoriei. Comunicarea de nivel cu documentația de pe compilatorul dvs. poate elimina necesitatea de a scrie propriile versiuni de noi și de a șterge. Pe multe platforme, sunt disponibile produse comerciale care vă permit să înlocuiți funcțiile de gestionare a memoriei care vin împreună cu compilatorii. Pentru a profita de funcționalitatea lor îmbunătățită și de performanța (probabil) a crescut, va trebui doar să reasamblați programul (bineînțeles, plătiți).
Subiectul acestei reguli este problema când este logic să înlocuiți versiunile noi și să ștergeți în mod implicit - la nivel global sau la nivel de clasă. Acum putem răspunde la această întrebare mai detaliat.
• Pentru a detecta erorile de utilizare (așa cum sa menționat mai sus).
• Să colecteze statistici despre utilizarea memoriei alocate dinamic (menționată mai sus).
• Pentru a accelera procesul de alocare și a elibera memoria. Distribuitorii de uz general deseori (deși nu întotdeauna) lucrează mult mai încet decât versiunile optimizate, mai ales dacă acestea sunt special concepute pentru obiecte de un anumit tip. Alocatorii specifici de clasă reprezintă exemple de alocare a blocurilor cu dimensiune fixă, cum ar fi cele reprezentate de biblioteca bazinelor din proiectul Boost. Dacă aplicația dvs. are un singur fir, dar managerul de memorie furnizat împreună cu compilatorul este în siguranță în condiții de siguranță, atunci puteți obține un impuls de performanță notabil scriind un manager de memorie pentru aplicații cu un singur filet. Desigur, înainte de a decide că trebuie să rescrieți operatorii noi și să ștergeți operatorii pentru a mări viteza, asigurați-vă că profilați că aceste funcții reprezintă într-adevăr un obstacol.
• Pentru a reduce cheltuielile generale ale unui manager de memorie standard. Managerii de memorie de uz general deseori (deși nu întotdeauna) nu numai mai lent decât versiunile optimizate, ci consumă mai multă memorie. Acest lucru se datorează faptului că unele costuri generale sunt asociate cu fiecare bloc alocat. Distribuitorii optimizați pentru obiecte mici (cum ar fi Pool-ul) pot elimina aproape aceste costuri.
• Pentru a compensa alinierea suboptimală în alocatorii impliciți. Așa cum am menționat deja, accesul rapid la valori duble pe arhitectura x86 se obține atunci când sunt aliniate la limite de opt octeți. Din nefericire, noii operatori furnizați cu niște compilatoare nu garantează o aliniere de opt octeți atunci când dublul este alocat dinamic. În aceste cazuri, înlocuirea în mod prestabilit a noului operator cu unul special care garantează această aliniere poate da o creștere semnificativă în performanța programului.
• Pentru a grupa obiecte conexe între ele. Dacă știți că anumite structuri de date sunt utilizate în mod obișnuit împreună, și doriți să reducă la minimum rata de eroare din cauza lipsei de pagini în memorie fizică atunci când se lucrează cu astfel de date, ar putea avea sens pentru a crea un teanc separat pentru astfel de structuri, astfel încât acestea s-au adunat, sau pe cât mai puține pagini posibil. Versiunile operatorilor noi și ștergători cu alocare (a se vedea regula 52) pot oferi o astfel de grupare.
• Pentru a obține un comportament nestandard. Uneori este posibil ca operatorii noi și ștergeți să facă ceva pe care versiunile furnizate împreună cu compilatorul nu le pot face. De exemplu, trebuie să aloce și blocuri dealocă de memorie în memoria partajată, dar aveți doar o interfață de programare pentru operațiuni cu memoria C. Scrierea o versiune specială a noului și șterge (eventual cu cazare -. A se vedea articolul 52) vă va permite să-și încheie un API C în clase C ++. Puteți scrie, de asemenea, o instrucțiune specială de ștergere care umple memoria care urmează să fie eliberată de zerouri pentru a crește gradul de protecție a datelor în aplicație.