Valgrind, muistinhallintaanalysaattori¶
Ohjelmointi2: Valgrind materiaaliosiossa
Muistityypit ja niiden elinajat¶
Sidonta voi tarkoittaa muuttujan liittämistä tiettyyn muistipaikkaan. Sidonnan alussa muistitila varataan muuttujalle, ja kun sidonta on valmis, muistitilaa vapautetaan. Jaksoa, jonka aikana muuttuja on sidottu tiettyyn muistipaikkaan, kutsutaan muuttujan elinkaareksi. C++:ssa ohjelmoija voi määritellä muuttujia, joilla on eri elinikä, mukaan lukien:
Staattiset muuttujat
static int globalVariable = 10; // Staattinen globaali muuttuja
void foo() {
static int staticLocalVariable = 5; // Staattinen paikallinen muuttuja
}
Muuttujat, jotka esitellään “static” avainsanalla, ovat esimerkkejä staattisista muuttujista. C++:ssa staattinen muuttuja sidotaan muistipaikkaansa ennen ohjelman suorittamista, ja sidos säilyy koko suorituksen ajan. Aiemmissa C++:n versioissa kaikkia muuttujia pidettiin staattisina.
Pinodynaamiset muuttujat funktion sisällä
void bar() {
int pinoMuuttuja = 7; // Pinodynaaminen paikallinen muuttuja
}
bar() suorittamisen jälkeen pinoMuuttuja tuhotaan pinosta, eikä sitä enää ole.
Eksplisiittiset kekodynaamiset muuttujat
int* dynaaminenMuuttuja = new int(42); // Eksplisiittinen keon dynaaminen muuttuja
delete dynaaminenMuuttuja; // Eksplisiittinen purku ennen ohjelman päättymistä
Eksplisiittinen muistinvaraus komennolla new tai malloc.
void bar() {
int* dynaaminenMuuttuja = new int(42); // Eksplisiittinen keon dynaaminen muuttuja
}
Mikään eksplisiittinen poistaminen tai keon muistin poistaminen ei johda muistivuotoon. Mielellään poisto tehdään samassa skoopissa, missä muuttuja määriteltiin.
Epäsuorat keon dynaamiset muuttujat
C++:ssa implisiittisiä dynaamisia muuttujia voidaan luoda esimerkiksi seuraavalla tavalla:
std::vector<int>v = {0};
Koodinpätkä varaa muistia heapissa sijaitsevalle int-taulukolle. Vektori käsittelee muistin varaamisen ja vapauttamisen sisäisesti, mikä helpottaa käyttäjän työskentelyä taulukoiden kanssa ilman nimenomaista muistinhallintaa.
Kuinka voit itse tarkastella pinon ja heapin käyttöä:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
// memory usage examples
// stack
int i = 42;
int* stack_i = &i;
// heap
int* heap_i = new int(69);
cout << "stack_i: " << stack_i << " heap_i: " << heap_i << std::endl<< std::endl;
// heap or stack?: an array with one value
int arr[1] = {0};
int* arr_ptr = &arr[0];
std::vector<int>v = {0};
// stack
cout << "arr_ptr: " << arr_ptr << std::end;
// stack
cout << "v_ptr: " << &v << std::endl;
// heap!
cout << " The content of vector: " << &v.front() << endl;
delete heap_i;
return 0;
}
Ohjelma tulostaa:
Pino-muistiosoitteet alkavat 0x7ff*-tavuilla, kun taas heap-muistiosoitteet alkavat 0x56*-tavuilla. Taulukon arr sisältö on staattista, joten se tallentaan pinoon.
Itse vektoriobjekti (+ metatiedot) sijoitetaan pinoon, mutta vaikka vektori on pinossa, sen dynaaminen sisältö (kokonaislukutaulukko) on keossa.
Ulkoiset muuttujat
C++ tukee myös ulkoisia viittauksia (esim. “extern”-avainsana) ja tiedostoissa olevia pysyviä tietoelementtejä, jotka säilyvät ohjelman suorituksen päätyttyäkin. Näin ollen muuttujan elinikä voi olla pidempi kuin ohjelman ajonaika.
Muistinhallinnan parhaat käytännöt¶
- Vältä iteraattorin mitätöintiä:
- ole varovainen iteraattorin mitätöimisen suhteen, kun muokkaat säilöjä (esim. lisäät tai poistat)
- Käytä älykkäitä osoittimia:
- “std::unique_ptr” ja “std::shared_ptr” hallitsevat muistia automaattisesti
#sisällytä <muisti>
std::ainutlaatuinen_ptr<int> ainutlaatuinenPtr = std::make_unique<int>(42);
- suosi STL-säilöjä ja algoritmeja:
- kuten “std::vector”, “std::map” ja STL-algoritmit aina kun mahdollista.
- Vältä keon muistin varaamista kokonaan, jos mahdollista:
- minimoi “new”, “delete”, “malloc” ja “free” käyttö.
- Muista poistaa ja antaa selkeät menetelmät resurssien vapauttamiseksi
- Profiloi ja testaa:
- Käytä Valgrindia! (valgrind –quiet –leak-check=full ./a.out )
Muistinhallinnan analysaattori valgrind¶
Ohjelmointi2: Valgrind-materiaali
Muutaman rivin ohjelmasta muistinkäsittelyvirheet löytyvät silmäilemällä koodia ja miettimällä, mitä tuli tehtyä. Kun ohjelman koko kasvaa, ongelmien löytäminen muuttuu aina vaikeammaksi.
Muistinkäsittelyyn liittyviä ongelmia voi jäljittää ohjelmalla
nimeltä valgrind
, joka pitää suorittaa erikseen.
Se analysoi ohjelmakoodin ja tulostaa virheilmoituksen, jos esimerkiksi
- ohjelmasi varaa dynaamista muistia komennolla
new
, mutta ei vapauta sitä komennolladelete
- ohjelmassa käytetään muuttujaa (dynaamisesti tai automaattisesti varattua), jolle ei ole asetettu arvoa
- yrität käsitellä dynaamisesti varattua muistia, joka on jo vapautettu.
Huomautus
Valgrind toimii Linux-etätyöpöydällä, mutta todennäköisesti ei Mac- eikä Windows-koneilla (tai valgrindin asentaminen näille koneille ei onnistu).
Työkalu valgrind
on asennettuna linux-desktopilla
(Oikeudet haetaan palvelussa https://id.tuni.fi/idm/entitlement)
Jos haluat, voit myös asentaa sen omalle koneellesi.
Voit suorittaa valgrind
-komennon joko Qt Creatorissa tai komentorivillä.
Seuraavassa tehtävässä valgrind
suoritetaan harjoituksen vuoksi molemmilla
tavoilla, jotta opimme käyttämään sitä monipuolisemmin.
Valgrindin virheilmoitukset¶
Joissakin virheilmoituksissa esiintyy numeroita 1, 4, 8.
Luvut tarkoittavat tyypin tarvitseman muistialueen kokoa tavuina.
Merkkityypille (char
) riittää yksi tavu, kokonaisluku (int
)
tarvitsee neljä tavua ja osoittimet kahdeksan.
Valgrind kertoo sarakkeessa Location
tiedoston ja rivinumeron.
Kyseinen kohta on se, missä virhe ilmenee tai missä se havaitaan.
Korjattava koodikohta voi olla jossakin muualla
(jossakin kohdassa, joka suoritettiin ennen virheen havaitsemiskohtaa).
Seuraavaksi käydään läpi joitakin yleisimpiä virheilmoituksia.
Use of unitialized value of size 8¶
Jos muuttujalle ei varata muistia (komennolla new
):
int* new_int;
mutta alustamatonta muuttujaa yritetään lukea seuraamalla osoitinta, seuraa ongelmia:
std::cout << *new_int;
Valgrind valgrind --quiet --leak-check=full ./a.out
tulostaa seuraavat virheet:
- Use of uninitialised value of size 8
- Invalid read of size 4
Jos alustat osoittimen arvoksi nullptr
:
int* new_item = nullptr;
virhe “Use of uninitialised value of size 8” katoaa, mutta koodi on edelleen virheellinen, toinen virhe, Invalid read, säilyy, ja ohjelma kaatuu segmentointivirheeseen.
Conditional jump or move depends on uninitialised value(s)¶
Tietueen kenttä tai jokin muu muuttuja on jäänyt alustamatta.
Esimerkiksi linkitetyssä listassa jollekin tietueen kentistä ei ole
annettu arvoa.
Jos alustamaton kenttä on jokin muu kuin next
-kenttä, virhe voi
ilmetä, kun kyseisen kentän arvoa yritetään tulostaa.
Jos next
-kenttään ei sijoiteta mitään arvoa (ei edes nullptr
),
ohjelma voi näyttää toimivan normaalisti.
Toisaalta tästä voi aiheutua myös ikuinen silmukka, kun listaa
käydään läpi niin kauan, kun next
-kentän arvo ei ole nullptr
.
Invalid read of size N¶
Yritetään käyttää (lukea) jo vapautettua muistialuetta. Esimerkiksi:
delete item_to_be_removed;
...
cout << item_to_be_removed->task << endl;
Tässä task
-kenttä on merkkijono, jolloin virheilmoituksessa N
on 8.
Jos tulostettava kenttä olisi int
-tyyppinen, N
olisi 4.
Invalid free() / delete / delete[]¶
Yritetään vapauttaa jo vapautettua muistia. Esimerkiksi:
delete item_to_be_removed;
...
delete item_to_be_removed;
Ohjelma voi silti toimia oikein, eli virhettä on vaikea havaita ilman valgrindia.
Jos välillä sijoitetaan arvo nullptr
esimerkiksi:
delete item_to_be_removed;
item_to_be_removed = nullptr;
...
delete item_to_be_removed;
valgrind ei anna mitään ilmoitusta, ja ohjelma voi näyttää toimivan oikein.
Tämä johtuu siitä, että nullptr
ei osoita mitään muistipaikkaa,
joten mitään ei voida vapauttaakaan.
N (…) bytes in M blocks are definitely lost in loss record X of Y¶
Muistia jää vapauttamatta, eli delete
-komento unohtui.
Ohjelma voi silti toimia oikein, eli virhettä on vaikea havaita ilman valgrindia.
TIRAKA-fall2023 tavallisimmat virheet¶
Vuoden 2023 Valgrind-tulokset vahvistavat aiemmat tulokset siitä, että muistivuodot ovat merkittävin virhelähde, kuten alla olevasta kuvasta ilmenee:
Pääasialliset syyt muistivuodoille ovat new-operatorin (muisti) virheellinen käyttö.
Jossain määrin meillä oli myös uusi virheluokka, nimeltään irreflexive requirements
.
Irreflexive requirements¶
Note
Virhe näyttää tältä: Error: comparison doesn’t meet irreflexive requirements, assert(!(a < a)).
Irrefleksiiviset virheet johtuvat virheellisistä vertailijoista (comparator). C++ STL:ssä vertailijat ovat usein yhteydessä tietorakenteisiin kuten std::map, std::set tai std::priority_queue, ja algoritmeihin kuten std::sort. Kun käytetään omia rakenteita tai luokkia esimerkiksi mapissä, vertailija on olennainen määrittämään elementtien lajittelukriteerit.
Vertailijat voivat olla perinteisiä funktioita, lambdoja tai funktio-objekteija (functors). Tutkitaan esimerkkiä kustakin.
Meillä on Person-strukti-esimerkki, ja seuraavaksi luomme joukon Person-strukteja, jotka lajittelemme iän perusteella:
struct Person {
std::string name="";
int age=0;
};
Perinteinen funktio:
bool ageComparator(const Person& p1, const Person& p2) {
return p1.age < p2.age;
}
std::set<Person, decltype(&ageComparator)> peopleSet(&ageComparator);
Lambda:
std::set<Person, [](const Person& p1, const Person& p2) {
return p1.age < p2.age;
}> peopleSet;
Funktori:
/* Functor */
struct AgeComparator {
bool operator()(const Person& p1, const Person& p2) const {
return p1.age < p2.age;
}
};
std::set<Person, AgeComparator> peopleSet;
Lambdat ja funktorit lisäävät vertailijoiden monimutkaisuutta. Kuitenkin irrefleksiivisten vaatimusten virhe liittyy yleensä itse vertailuprosessiin eikä vertailijan muotoon.
Kuten yllä olevissa tapauksissa näytetään, vertailijan on suoritettava “on pienempi kuin” -vertailu.
- Jos ensimmäinen parametri on pienempi kuin toinen, palautetaan true
- Muussa tapauksessa palautetaan false
Minkä tahansa muun vertailun (kuten <=) käyttö vertailulogiikassa johtaa irrefleksiivisiin virheisiin. Lisäksi objekteja voidaan yrittää verrata väärällä tavalla, kuten osoittimien vertailu ilman asianmukaista purkamista. Virheen vianmäärityksessä tarkista ja vahvista logiikka comparator-funktiossa, erityisesti mukautettujen vertailijoiden tapauksessa.