Pokazywanie postów oznaczonych etykietą Benchmark. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą Benchmark. 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.

5 września 2013

[C++11] std::shared_ptr vs std::make_shared

Od wielu osób, słyszałem o wyższości stosowania dedykowanych metod w celu tworzenia inteligentnych wskaźników. Nadszedł czas przyjrzeć się tym dwóm mechanizmom bliżej, jak zawsze na bazie własnych testów, by ze świadomością móc korzystać z ich dobrodziejstw.

Swoje przemyślenia oparłem na "C++ Primer" oraz na artykule Herba Shuttera:

Kilka punktów, które mogą okazać się pomocne w przyszłości.
  • std::unique_ptr powinien być zawsze preferowany przed std::shared_ptr
  • std::shared_ptr i std::unique_ptr powinno być wybierane, tylko gdy chcemy skorzystać z własnego deletera (std::make_shared tego nie umożliwia), albo gdy adaptujemy stary kod i chcemy zarządzać surowym wskaźnikiem.
  • W innych przypadkach powinno się korzystać z std::make_unique i std::make_shared.
Zalety:
  • Upraszcza to kod. W jednej instrukcji zawarte jest tworzenie inteligentnego wskaźnika i chowany jest new (std::shared_ptr + new), który może rzucać trudne do wykrycia wyjątki. std::make_shared nas przed tym chroni (w C++17 został zmieniony sposób ewaluacji argumentów funkcji i nie jest to już problemem).
  • std::make_shared daje istotne optymalizacyjne usprawnienia. std::shared_ptr jedynie tworzy wskaźnik na zasób którym ma zarządzać (który zostanie stworzony przez new) i nie wie, czy to co mu podlega zostało stworzone specjalnie dla niego, czy ma do czynienia z surowym wskaźnikiem. Zasób taki będzie najprawdopodobniej w innym bloku pamięci + kompilator jak zawsze doda kilka ekstra bajtów, przy alokowaniu.
Wady:
  • Nie testowałem, aczkolwiek ma to sens. Ponieważ std::make_shared stworzy obiekt, a także reference counters w jednym bloku pamięci, pamięć taka będzie zwolniona dopiero, gdy nie będzie już żadnych std::weak_ptr wskazujących na ten obiekt. Tworzenie obiektu za pomocą std::shared_ptr ma tu taką przewagę, że będzie mógł on zwolnić zasób, którym zarządza i pozostawić przy życiu jedynie reference counters dla std::weak_ptr. Jeżeli zarządzany obiekt będzie duży, może się to odbić na wydajności.
std::make_shared zatroszczy się o to by wszystko zostało stworzone po kolei (ładnie to ilustrują obrazki zamieszczone przez Herba Shutter na jego stronie).
Dla dużej ilości małych obiektów, może to istotne przyśpieszyć działanie programu, ponieważ zmniejsza się czas dostępu do cache procesora. Właśnie to chciałem przetestować w swoim teście.
#include <iostream>
#include <memory>
#include <boost/date_time/posix_time/posix_time.hpp>

using namespace boost::posix_time;

struct MyObject {
    MyObject(std::string n): name(n) { }
    std::string name;
};

void fun(const std::shared_ptr<MyObject> &sp) {
    if (sp->name == "xxx")
        std::cout << "xxx" << std::endl;
}

int main() {
    const ptime time_start = microsec_clock::local_time();

    for (int i = 0; i < 100000000; ++i) {
#ifdef TEST_SHARED_PTR
        fun(std::shared_ptr<MyObject>(new MyObject("shared")));
#else
        fun(std::make_shared<MyObject>("make_shared"));
#endif
    }

    const ptime time_stop = microsec_clock::local_time();
    std::cout << "Time: " << time_start - time_stop << std::endl;

    return 0;
}
Kompilacja.
$ g++ main.cpp -std=c++11 -O2 -DTEST_SHARED_PTR
Poniżej zestawienie wyników dla gcc i clang-a. Zrobiłem też testy bez użycia flag optymalizacyjnych (-O2) i co ciekawe wyniki były zupełnie odwrotne! Nie wynikałem już w przyczynę tego stanu rzeczy. Może kiedyś rozwikłam tą zagadkę.

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.

31 października 2012

Vector vs. List

Na tegorocznej konferencji GovingNative 2012 Bjarne Stroustrup, przedstawił kilka ciekawostek dotyczących nowości w C++:
Jednym z punków była wyższość wektora na listą. Lista góruje nad wektorem, w takich operacjach jak wstawianie oraz usuwanie elementów, jednak proces wyszukania odpowiedniego elementu w kontenerze okazuje się być naprawdę wąskim gardłem. Wektor z kolei lepiej radzi sobie z wyszukiwaniem, gorzej natomiast z usuwaniem i wstawianiem. Dzięki współczesnym procesorem korzystającym z mechanizmu dedukcji skoku, wektor okazuje się zyskiwać ogromną przewagę. Mimo iż wykonuje znacznie więcej operacji w pamięci to będą one odbywały się w cache procesora, gdzie są naprawdę szybkie.

Opis testu.
Wygeneruj N losowych liczb i wstaw je tak by zachowana została posortowana kolejność np. 5 1 4 2:
  • 5
  • 1 5
  • 1 4 5
  • 1 2 4 5

Usuń elementy z losowej pozycji np. dla 1 2 0 0, będzie to:
  • 1 2 4 5
  • 1 4 5
  • 1 4
  • 4
Na początek kilka funkcji pomocniczych. Pierwsza łączy nasz generator z rozkładem jednorodnym
TRnd get_rnd(const int max)
{
    const int seed = 858446;
    TBaseGeneratorType generator(seed);
    boost::uniform_int<> uni_dist(0, max);
    TRnd rnd(generator, uni_dist);
    return rnd;
}
Pierwsza część testu - generyczna funkcja do wstawiania, zachowując posortowaną kolejność.
template <typename T>
void insert_sort(T& con, const int size, TRnd& rnd)
{
    for (int i = 0; i < size; ++i) {
        const int value = rnd();
        typename T::iterator it = std::find_if(con.begin(), con.end(),
                                               value < _1);
        con.insert(it, value);
    }
}
Druga część testu - usuwanie wartości z losowej pozycji.
template <typename T>
void erase_by_rand_pos(T& con, TRnd& rnd)
{
    while (not con.empty()) {
        const int pos = rnd() % con.size();
        typename T::iterator it = con.begin();
        std::advance(it, pos);
        con.erase(it);
    }
}
Ciało testu:
#include <vector>
#include <list>
#include <algorithm>
#include <iostream>

#include <boost/lambda/lambda.hpp>
#include <boost/lexical_cast.hpp>

#include <boost/random/mersenne_twister.hpp>
#include <boost/random/uniform_int.hpp>
#include <boost/random/variate_generator.hpp>

#include <boost/date_time/posix_time/posix_time.hpp>

using namespace std;
using namespace boost::lambda;
using namespace boost::posix_time;

typedef boost::mt19937 TBaseGeneratorType;
typedef boost::variate_generator<TBaseGeneratorType, 
                                boost::uniform_int<> > TRnd;

int main(int argc, char **argv)
{
    if (argc < 2) {
        cout << "Missing size argument" << endl;
        return -1;
    }

    TRnd rnd = get_rnd(std::numeric_limits<int>::max());
    const int size = boost::lexical_cast<int>(argv[1]);

    const ptime time_start = microsec_clock::local_time();
    cout << "Start at: " << time_start << ", size = "<< size << endl;

//    vector<int> container;
    list<int> container;
    insert_sort(container, size, rnd);

    const ptime time_insert = microsec_clock::local_time();
    cout << "Inserted sort:       " << time_insert - time_start << endl;

    erase_by_rand_pos(container, rnd);

    const ptime time_remove = microsec_clock::local_time();
    cout << "Erase at random pos: " << time_remove - time_insert << endl;
    cout << "Whole process:       " << time_remove - time_start << endl;

    return 0;
}
Wyniki. Wszystkie testy były kompilowane za pomocą g++ (4.6.3) z flagą -O2. Bjarne musiał korzystać z bardziej wyrafinowanego algorytmu niż mój, bo w jego wersji cały proces dla 200000 elementów zajął ok. 500 sekund, w moim przypadku było to już 1116 sekund (sic!), dlatego też nie testowałem powyżej tej wartości. Nie testowałem też "preallocated list" bo nie jestem pewien co to dokładnie jest.



I jeszcze jeden wykres, przedstawiający jak czasowo wyglądały poszczególne operacje, czyli wstawianie z wyszukiwaniem i usuwanie z wyszukiwaniem. Porównując dane, widać, że w przypadku listy, najwolniejszą operacją jest usuwanie z wyszukiwaniem, w przypadku wektora wstawianie z wyszukiwaniem. Usuwanie i wstawianie na wektorze powinny być jego słabym punktem, ale okazują się, że w połączeniu z wyszukiwaniem, są znacznie szybsze od tego co oferuje lista.



15 października 2012

Benchmark framework (Boost.Chrono) - część II

Druga biblioteka, z której pomocą chciałem mierzyć czas to Boost.Chrono. Nie do końca wyczuwam różnice między nią a Boost.Data_Time. Z pewnością obie mają rzeczy, których nie posiada "konkurent". Np. Boost.Chrono pozwala na zdefiniowanie z jakiego zegara systemowego będziemy wykorzystywać informacje. Ciekawe, czy zegar, który pokazuje czas wykonania danego wątku, uwzględnia, to że proces mógł być zawieszony, albo wywłaszczony na długi czas? Hmmm, jeszcze się w to nie wgłębiałem.

Pierwsza sprawa, to paczka w Ubuntu (libboost-all-dev), która ciągle wskazuje na wersję 1.46, a tam Boost.Chrono brakuje. Musiałem, wymienić to na:
apt-get install libboost1.48-all-dev
Druga spraw, to linkowanie. Chrono, korzysta z Boost.System i obie biblioteki trzeba zlinkować z programem (tu mam wątpliwości czy istnieje wersja, której nie trzeba linkować - nie doczytałem, korzystam z tego co mam w paczkach Ubuntu).

Zmiany w CMakeList.txt dla mojego projektu
project(benchmark_container_iterator)
cmake_minimum_required(VERSION 2.8)
aux_source_directory(. SRC_LIST)
add_executable(${PROJECT_NAME} ${SRC_LIST})

find_package(Boost COMPONENTS system chrono REQUIRED)
target_link_libraries(${PROJECT_NAME}
  ${Boost_SYSTEM_LIBRARY}
  ${Boost_CHRONO_LIBRARY}
)
Dla benchmarka chciałem użyć process_cpu_clock, ale jego precyzja była do kitu. Jednak według opisu, wydaje się tym czego szukam:
Process and thread clocks are used usually to measure the time spent by code blocks, as a basic time-spent profiling of different blocks of code (Boost.Stopwatch is a clear example of this use).
Ostatecznie padło na "steady_clock"

Zasada działania, jest podobna jak w wersji poprzedniej. To bardzo proste wykorzystanie możliwości tej biblioteki.
#include <list>
#include <iostream>
#include <boost/foreach.hpp>
#include <boost/assign/list_of.hpp>

#include <boost/chrono.hpp>

using namespace std;
using namespace boost::assign;

int main()
{
    using namespace boost::chrono;
    steady_clock::time_point time1 = steady_clock::now();
    cout << "Start at:              " << time1 << endl;

    const int size = 400500;
    list<int> l = list_of(1337).repeat(size, 1337);

    steady_clock::time_point time2 = steady_clock::now();
    nanoseconds td_init = duration_cast<nanoseconds>(time2 - time1);
    cout << "Memory Initialization: " << td_init << endl;

    int result = 0;
    BOOST_FOREACH(int value, l) {
        if (value > result)
            result = value;
    }

    steady_clock::time_point time3 = steady_clock::now();
    nanoseconds td_find = duration_cast<nanoseconds>(time3 - time2);
    cout << "Searching:             " << td_find << endl;

    return result;
}
Wyniki:
Start at:              14714172071593 nanoseconds since boot
Memory Initialization: 128273372 nanoseconds
Searching:             29473710 nanoseconds

14 października 2012

Benchmark framework (Boost.Date_Time)

Pisząc test porównujący czas wykonania dwóch algorytmów, natchnęło mnie by zrezygnować z wołania za pomocą metody system() polecenia wyświetlającego czas i skorzystać jakiejś biblioteki.
#include <iostream>
#include <stdlib.h>

int main()
{
    system("date +%M.%S.%N");
    return 0;
}
W boost znalazłem dwie, które nadawały by się do moich potrzeb. Niestety w żadną nie zagłębiałem się dokładnie. Pierwsza to Boost.Data_Time, druga to Boost.Chrono. Tutaj będzie tylko o pierwszej.

Biblioteka, pozwala na pobieranie czasu, obliczanie czasu trwania i przesunięć w czasie. Wszystko to dla różnych systemów kalendarzowych np. UTC, kalendarz gregoriański. Pozwala też zapisywać i odczytywać datę w różnych formatach - z tego co czytałem, bo nie miałem czasu testować.

W tym programie pobieram trzykrotnie lokalny czas (UTC) z dokładnością co do milisekund (linijki 14, 20 i 30). Następny krok to obliczenie czasu trwania (linijki 21 i 31) inicjalizowania listy, oraz jej przeszukiwania.
#include <list>
#include <iostream>
#include <boost/foreach.hpp>
#include <boost/assign/list_of.hpp>

#include <boost/date_time/posix_time/posix_time.hpp>

using namespace std;
using namespace boost::assign;

int main()
{
    using namespace boost::posix_time;
    ptime time1 = microsec_clock::local_time();
    cout << "Start at:              " << time1 << endl;

    const int size = 400500;
    list<int> l = list_of(1337).repeat(size, 1337);

    ptime time2 = microsec_clock::local_time();
    time_duration td_init = time2 - time1;
    cout << "Memory Initialization: " << td_init << endl;

    int result = 0;
    BOOST_FOREACH(int value, l) {
        if (value > result)
            result = value;
    }

    ptime time3 = microsec_clock::local_time();
    time_duration td_find = time3 - time2;
    cout << "Searching:             " << td_find << endl;

    return result;
}
Wynik
Start at:              2012-Oct-14 17:13:36.716994
Memory Initialization: 00:00:00.125376
Searching:             00:00:00.021398