Pokazywanie postów oznaczonych etykietą valgrind. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą valgrind. Pokaż wszystkie posty

24 września 2013

[C++] Usuwanie elementów z mapy o zadanej wartości

Problem nad, którym od dawna się zastanawiałem, a mianowicie jak najlepiej usunąć elementy o zadanej wartości z mapy. Miałem nadzieje na wykorzystanie, jakiegoś wbudowanego w standard mechanizmu/algorytmu, albo chociaż czegoś z biblioteki boost - niestety. Wyniki poszukiwań poniżej.

Na pierwszy ogień poszedł std::remove_if(), niestety problemem jest tu w jaki sposób mapy przechowują swoje elementy. std::map<K,V>::value_type jest parą std::pair<const K, V>. std::remove_if() przesuwa elementy do usunięcia w taki sposób, że zostają one nadpisane. Ponieważ nie można przypisywać do const, std::remove_if() nie nadaje się do zastosowania na mapach. Wykorzystałem go jednak w zwykłej metodzie nib::erase_if() [linia 21], gdyż świetnie nadaje się dla zwykłych kontenerów. Wątek na stackoveflow, który traktuje o tym problemie:
Z tego co udało mi się wyszukać innym, częstym rozwiązaniem jest skorzystanie z std::remove_copy_if(). Tym razem wszystkie elementy, które nie spełniają naszego kryterium są kopiowane do nowej mapy. Jeżeli mapa jest sporych rozmiarów, a elementów do usunięcia jest stosunkowo dużo, to minusem rozwiązaniem będzie niepotrzebne chwilowe zwiększenie zapotrzebowanie na pamięć. Działanie tego algorytmu zostało zawarte w metodzie nib_test::erase_if() [linia 47].

Ostatnie i najbardziej preferowane rozwiązanie - nib::erase_if [linia 27], to napisanie własnego algorytmu, który podejmie się tego zadania. Szkoda, że nie udało się niczego takiego zaadoptować (albo jeszcze nie znalazłem) w bibliotece standardowej albo w innych popularnych bibliotekach (np. boost).
Po odnalezieniu elementów o zadanej wartości zostaną one usunięty za pomocą metody std::map::erase(). Dobra wiadomość jest taka, że std::map::erase(), unieważnia jedynie referencje i iteratory do usuwanego elementu, pozostałe referencje i iteratory w mapie są nadal ważne: Dwa wątki traktujące o tym rozwiązaniu:
Jak informuje autor, jednego z rozwiązań, w przypadku skorzystania z zapisu "erase(it++)", standard gwarantuje, że wszystkie wyrażenia argumentów, będą wykonane przed wywołaniem funkcji. Prosta inkrementacja zostanie wykonany przed wywołaniem funkcji std::map::erase(), a do niej zostanie przekazana wartość jeszcze niezainkrementowana.
#include <map>
#include <vector>
#include <algorithm>
#include <iostream>

template <typename Key, typename Value>
std::ostream& operator<<(std::ostream& out, const std::pair<Key, Value>& p) {
    return out << "[" << p.first << "]" << "=" << p.second;
}

template <typename Container>
void print_container(const Container& container) {
    for(const auto& v : container)
        std::cout << v << std::endl;
}

namespace nib
{

template <typename Container, typename Predicate>
void erase_if(Container& container, const Predicate& predicate) {
    container.erase(std::remove_if(begin(container), end(container), predicate),
                    end(container));
}

template <typename Key, typename Value, typename Predicate>
void erase_if(std::map<Key, Value>& dict, const Predicate& predicate) {
    auto it = begin(dict);
    while(it != end(dict)) {
        if (predicate(*it)) {
            // Standard zapewnia, ze it++ zostanie obliczone, zanim samo "it"
            // zostanie przekazane do funkcji
            dict.erase(it++);
        }
        else {
            ++it;
        }
    }
}

} // namespace nib

namespace nib_test
{

template <typename Key, typename Value, typename Predicate>
void erase_if(std::map<Key, Value>& dict, const Predicate& predicate) {
    typedef typename std::remove_reference<decltype(dict)>::type Map;
    Map tmp;
    std::remove_copy_if(begin(dict), end(dict),
                        std::inserter(tmp, begin(tmp)),
                        predicate);
    dict = tmp;
}

} // namespace nib_test

int main() {
    std::vector<int> vec { 1, 2, 1, 4, 1, 5 };
    auto val_to_erase = [] (int& v) { return v == 1; };
    nib::erase_if(vec, val_to_erase);
    print_container(vec);

    typedef std::map<int, std::string> Dict;
    Dict dict { {1, "a"}, {2, "b"}, {3, "c"}, {7, "b"} };

    auto val_to_erase_in_map = [] (typename Dict::value_type& v) { return v.second == "b"; };
    nib::erase_if(dict, val_to_erase_in_map);
//  nib_test::erase_if(dict, val_to_erase_in_map);
    print_container(dict);

    return 0;
}
Wyniki:
2
4
5
[1]=a
[3]=c
Aby się upewnić co do kosumpcji pamięci przez oba rozwiązania, przeprowadziłem prostą weryfikacją za pomocą valgrinda.
int main() {
    typedef std::map<int, std::string> Dict;
    Dict dict { {1, "a"}, {2, "b"}, {3, "c"}, {7, "b"} };

    for(int i = 0; i < 10000; ++i)
        dict.insert(std::make_pair(5000 + i, "x"));
//      dict.emplace(5000 + i, "x"); - Nie dziala w GCC 4.7!!!

    auto val_to_erase_in_map = [] (typename Dict::value_type& v) { return v.second == "b"; };
//  nib::erase_if(dict, val_to_erase_in_map);
    nib_test::erase_if(dict, val_to_erase_in_map);
    print_container(dict);

    return 0;
}
Wersja nib::erase_if(), odnotowała peak na poziomie 558272 bajtów, natomiast dla nib_test::erase_if() (korzystająca z std::remove_copy_if()) peak ten wynosił 874376 bajty.

25 listopada 2012

Benchmark framework - zużycie pamięci - valgrind Massif

Moje poszukiwania, mechanizmu pozwalającego zdobyć informację na temat maksymalnego zużycia pamięci działającego programu zaowocowały spotkaniem z tym oto narzędziem: http://valgrind.org/docs/manual/ms-manual.html. Z pewnością są lepsze rozwiązania, ale dla moich potrzeb to jest satysfakcjonujące, przynajmniej w tej chwili.
Zaczniemy do przykładu, którym ma zając się valgrind.
#include <iostream>
#include <vector>

using namespace std;

template <typename T>
T * alloc()
{
    cout << "size " << sizeof(T) << endl;
    return new T[1000000];
}

void alloc_dealloc()
{
    int * a = alloc<int>();
    delete [] a;
}

int main()
{
    alloc_dealloc();
    vector<char> v(100000);
    alloc<long long>();
    alloc_dealloc();
    for (int i = 0; i < 300; ++i)
        alloc_dealloc();

    return 0;
}
Taki projekt trzeba skompilować, generując informacja dla debuggera.
$ g++ -g main.cpp
Teraz nasz program można poddać analizie
$ valgrind --tool=massif --time-unit=B --stacks=yes --massif-out-file=mem.out ./a.out
$ ms_print mem.out
* pierwsza linijka włącza massif do analizy (--tool=massif),
* "jednostka czasu" używana przez profiler - nie jestem pewny jak to rozumieć.
The time unit used for the profiling. There are three possibilities: instructions executed (i), which is good for most cases; real (wallclock) time (ms, i.e. milliseconds), which is sometimes useful; and bytes allocated/deallocated on the heap and/or stack (B), which is useful for very short-run programs, and for testing purposes, because it is the most reproducible across different machines.
W każdym razie, jest zalecana do małych programów i w celach testowych, więc to czego szukam.
* informuje, że interesuje nas też pamięć, która zostanie zaalokowana na stosie (--stack=yes),
* jak będzie nazywał się surowy plik z analizą zużycia pamięci (--massif-out-file=mem.out),

Massif rozdziela proces zbierania danych od ich prezentacji, w ten sposób w przyszłości mogą pojawić się nowe metody na ich prezentowanie. Plik, gdzie zostały zgromadzone dane poddajemy działaniu ms_print.
Number of snapshots: 72
 Detailed snapshots: [1 (peak), 8, 27, 32, 37, 42, 47, 55, 65]

--------------------------------------------------------------------------------
  n        time(B)         total(B)   useful-heap(B) extra-heap(B)    stacks(B)
--------------------------------------------------------------------------------
  0              0                0                0             0            0
  1     21,983,976       12,103,920       12,100,000         3,576          344
Mnie interesują najbardziej dwie linijki z całego raportu. Pierwsza to numer snapshota, który zanotował największy "peak" (1). A druga do dokładne informacje z tego snapshota, czyli całkowite zużycie pamięci, w tym przypadku 12103920 bajtów.