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

12 lutego 2020

[C++17] Parallel algorithms

Biblioteka standardowa w C++17 została rozbudowana o algorytmy, których praca może zostać zrównoleglona. O sposobie wykonania decyduje ExecutionPolicy przekazane do funkcji jako argument. Programista musi zapewnić, że funkcja przekazana do "algorytmu", będzie bezpieczna - nie będzie zależności między danymi (np. modyfikowanie danych, które mogą być odczytywane przez inny równoległy wątek). Dobre wytłumaczenie różnic na stackoverflow, a tutaj małe zestawienie:
  • std::execution::seq - standardowe wykonanie sekwencyjne, bez zrównoleglenia.
  • std::execution::par - równoległe wykonanie (choć nie ma obietnicy, że tak się stanie).
  • std::execution::par_unseq - równoległe wykonanie (choć nie ma obietnicy, że tak się stanie). Wymaga silniejszych gwarancji na to że przeplatane wywołanie funkcji będzie bezpieczne także w obrębie jednego wątku. Nowe procesory oferują taką możliwość dzięki instrukcjom do wektoryzacji - SIMD (Single-Instruction, Multiple-Data) parallelism.
Przykład z funkcją std::reduce, która w działaniu przypomina std::accumulate. Zsumowanie wartości w wektorze:
#include <iostream>
#include <numeric>
#include <execution>
#include <vector>

using namespace std;

int main() {
    vector<int> vec{1, 2, 3, 4};

    int result = std::reduce(std::execution::par,
                             begin(vec),
                             end(vec));

    cout << result << endl;
}
W przypadku gcc (9.2.1 20191008) wymagane było zainstalowanie dodatkowej paczki libtbb-dev (Threading Building Blocks).
$ sudo apt-get install libtbb-dev
$ g++ -std=c++17 main.cpp -ltbb
$ ./a.out

10

28 lutego 2015

[boost] Sprawdzenie typu za pomocą typeindex

Od wersji 1.56 boost, posiada fajną bibliotekę, dostarczająca informacje o typie zmiennej (boost_typeindex). Podobno radzi sobie lepiej, niż mechanizmy typeid() albo std::type_index (C++11). Z pewnością wyniki są bardziej czytelne dla człowieka.

W Ubuntu 14.10 dostępny jest tylko boost w wersji 1.55, więc trzeba ściągnąć źródła samodzielnie:
wget -c http://sourceforge.net/projects/boost/files/boost/1.57.0/boost_1_57_0.tar.gz/download
mv download boost_1_57.tar.gz
tar -zxv boost_1_57.tar.gz
Podczas kompilacji trzeba jeszcze wskazać gdzie są nasze pliki nagłówkowe:
g++ -std=c++14 -I/home/beru/boost_1_57_0/ main.cpp
A teraz testowanie. auto w trakcie dedukcji typu usuwa modyfikatory const i volatile.
#include <iostream>
#include <vector>
#include <boost/type_index.hpp>

using namespace std;

int main()
{
    const vector<int> val1;
    auto  a = val1;
    auto& b = val1;

    int val2 = 44;
    const int* p = &val2;
    auto c = p;

    cout << "For a: " << boost::typeindex::type_id_with_cvr<decltype(a)>().pretty_name() << endl;
    cout << "For b: " << boost::typeindex::type_id_with_cvr<decltype(b)>().pretty_name() << endl;
    cout << "For c: " << boost::typeindex::type_id_with_cvr<decltype(c)>().pretty_name() << endl;

    return 0;
}
Wynik:
For a: std::vector<int, std::allocator<int> >
For b: std::vector<int, std::allocator<int> > const&
For c: int const*

5 sierpnia 2014

[gcc + tutorial] x86 Assembly

Zobaczyłem fajny przełącznik (-masm) do gcc, który wyrzuca kod asemblera bardziej przypominający MASM niż NASM.
gcc main.cpp -S -masm=intel
Tutorial do asemblera x86:

18 lutego 2014

[C++11] Copy control i move semantic

Kolejny zbiór nowości w standardzie, z którymi nie do końca czuje się jeszcze pewnie. Myślę, że do tematu jeszcze powrócę, jak tylko stanie się on dla mnie bardziej klarowny.

Copy control


Konstruktor kopiujący

W przypadku konstruktora kopiującego parametr jest prawie zawsze referencją na const. Rzadko zdarza się sytuacja, abyśmy byli zmuszeni do modyfikacji obiektu, z którego kopiujemy.
struct Foo {
    Foo(const Foo&);
};

Zasada trzech/pięciu/zero (the rule of three/five/zero).

Istnieją trzy (pięć - w nowym standardzie) podstawowe operacje kontrolujące kopiowanie obiektu:
  • konstruktor kopiujący
  • operator przypisania
  • destruktor
  • move konstruktor
  • operator przeniesienia (move)
Nie ma wymagań by tworzyć wszystkie z nich, jednak powinny być one traktowane jako całość. Bardzo rzadko zdarza się potrzeba stosowania tylko jednej konstrukcji z pominięciem reszty. Sam Stroustrup zaleca by przy deklaracji chociaż jednej z operacji, stworzyć wszystkie pięć. W przeciwnym razie klasa nie powinna zawierać żadnej z tych konstrukcji - "the rule of zero".
Jeżeli nie chcemy użyć któregoś z mechanizmów, poniżej wymieniono kilka zasad, którymi należy się kierować:
  • Jeśli klasa wymaga destruktora, prawie na pewno wymaga konstruktora kopiującego i operatora przypisania
  • Jeśli klasa wymaga konstruktora kopiującego, prawie na pewno wymaga operatora przypisania i vice versa
  • Klasa która nie może być kopiowana powinna definiować konstruktor kopiujący i operator przypisania jako delete, zamiast tworzyć te dwie definicje prywatne
Tworząc operator przypisania należy pamiętać o dwóch rzeczach:
  • Operator przypisania (ma to też zastosowanie dla operatora move) musi pracować poprawnie, gdy próbujemy przypisać do siebie ten same element. Ma to szczególne znaczenie jeżeli, obiekt posiada dane zapisane w pamięci dynamicznej, a w wyniku przypisania tworzymy w pamięci nowe dane i kasujemy stare. W takim przypadku dobrą praktyką jest skopiowanie wartości po prawej stronie do zmiennej lokalnej, zniszczenie wartości stojącej po lewej i w końcu przypisanie jej wartości tymczasowej. [C++Primer, s. 512; s. 536]
  • Większość operatorów przypisania współpracuje z destruktorem i konstruktorem kopiującym
Storage& operator=(const Storage& r) {
    auto tmp = new string(*r.data);
    delete data;
    data = tmp;

    return *this;
}
W przeciwieństwie do mechanizmów copy-control swap nigdy nie jest wymagany, jednakże zdefiniowanie własnej metody, może być ważną optymalizacją dla klasy, która alokuje zasoby. Operatory przypisania, które korzystają z techniki "copy and swap", są automatycznie odporne na wyjątki i obsługują samoprzypisanie.
Należy pamiętać tylko o jednej rzeczy. Nigdy nie wolno wołać bezpośrednio std::swap(). Wymusza to bowiem korzystanie z wersji bibliotecznej swap(), tymczasem klasa może mieć zmienną, która posiada własną wersję tej funkcji. Najlepiej by wyboru dokonał kompilator, przez wciągnięcie przestrzeni nazw.
inline void swap(Storage &l, Storage& r) {
    using std::swap;
    swap(l.data, r.data);
    swap(l.counter, r.counter);
}

void swap(Counter &l, Counter &r) {
    Clock *tmp = l.clock;
    l.clock = r.clock;
    r.clock = l.clock;
}
Jeżeli klasa posiada chociaż jeden z mechanizmów tj.: konstruktor kopiujący, operator przypisania lub destruktor kompilator nie stworzy nam konstruktora i operatora move!

Move semantic


Najtrudniejsza rzecz, jaka przyszła mi do opanowania wraz z nowym standardem, czyli move semantic. Kilka linków:
Ponieważ kompilatory mogą optymalizować tworzenie zmiennych tymczasowych (copy elision), które służą tylko do zainicjowania innych zmiennych wyłączyłem tą opcję.
g++ -std=c++11 -fno-elide-constructors main.cpp

std::move i std::forward

Ciekawie o nich opowiedział Scott Meyers w swoim wykładzie An Effective C++11/14 Sampler na GoingNative 2013, z czego sporo zaczerpnąłem. Żadne z mechanizmów niczego nie obiecuje, ich zadaniem jest jedynie rzutowanie (podczas kompilacji) do rvalue:
  • std::move (rzutowanie bezwarunkowe) - przekazuje obiekt jako rvalue
  • std::forward (rzutowanie warunkowe) - przekazuje obiekt jako rvalue, tylko jeżeli oryginalny obiekt był rvalue inaczej będzie to lvalue
std::move nie gwarantuje tego że coś zostanie przesunięte. Np. jego działanie na obiekcie const spowoduje jego skopiowanie. Korzystając z tego mechanizmu trzeba być bardzo ostrożnym ponieważ kompilator raczej nie poinformuje nas gdy std::move nie będzie przesuwać ("rozświetliło" by to bibliotekę standardową).

Korzystając z std::move obiecujemy, że nie będziemy korzystać już z danego obiektu chyba, że zamierzamy do niego coś przypisać lub go zniszczyć. Po wykonaniu std::move nie mamy żadnej gwarancji co do wartości przesuniętego obiektu.

Głównym zadaniem std::forward jest przekazanie argumentu (rvalue, lvalue, z const-em lub nie) w niezmienionej formie, do innej funkcji. Jeśli oryginalny obiekt, który jest wstawiony do std::forward był rvalue, zrobi z niego rvalue, a jeżeli lvalue, to lvalue - dlatego właśnie jest warunkowy.
void process(Storage& lvalue) {
    cout << "By lvalue reference" << endl;
}

void process(Storage&& rvalue) {
    cout << "By rvalue reference" << endl;
}

template <typename T>
void log(T&& param) {
    process(std::forward<T>(param));
}

int main() {
    Storage st;
    log(st);                     // przekazanie jako lvalue
    log<Storage>(std::move(st)); // przekazanie jako rvalue
}
Wynik:
Default constr.:  defaultData
By lvalue reference
By rvalue reference
Destructor:       defaultData

"Jeżeli oczekujesz szybkiego działania przekazuj przez wartość"

Zalecenia do poprzednich wersji standardu sugerowały by lvalue były opatrzone modyfikatorem const. Jest to już nieprawdą, bowiem powodowało by to kopiowanie obiektu. Jeżeli chcemy coś przesunąć, const nam w tym przeszkodzi.
void process(const Storage st) {
    Storage my = std::move(st); // Kopiowanie!
}

int main() {
    Storage st;
    process(st);
}
Wynik
Default constr.:  defaultData
Copy constructor: defaultData
Copy constructor: defaultData
Destructor:       defaultData
Destructor:       defaultData
Destructor:       defaultData

noexcept dla operacji move

Ponieważ z założenia operacje "move konstruktora" i "move operatora" kradną zasoby i nie alokują żadnych nowych, więc nie powinny też rzucać żadnych wyjątków. Powinniśmy poinformować o tym kompilator stosując noexcept. Co więcej, kontenery takie jak std::vector, sprawdzają, czy "move konstruktor" jest noexcept (podczas realokacji), jeżeli nie, zostanie zawołany zwykły konstruktor kopiujący.
#include <iostream>
#include <string>
#include <vector>

using namespace std;

struct Verbose {
    Verbose(std::string) { cout << "Constructor" << endl; }
    Verbose(const Verbose&) { cout << "Copy" << endl; }
    Verbose(Verbose &&) /*noexcept*/ {
        cout << "Move" << endl;
    }
    Verbose& operator =(const Verbose&) { cout << "assign=" << endl; }
    Verbose& operator =(Verbose&&) noexcept { cout << "move=" << endl; }
    ~Verbose() noexcept { cout << "Destructor" << endl; }
};

int main() {
    std::vector<Verbose> v;
    v.emplace_back("aaa");
    v.emplace_back("bbb");
}
Wynik:
Constructor
Constructor
Copy
Destructor
Destructor
Destructor

Uboczne działania destruktora

Move semantic niesie jeszcze jedno zwodnicze działanie, o którym należy pamiętać. Nigdy nie mamy pewności kiedy zostanie zawołany destruktor obiektu, z którego przenosimy zasoby. Jeżeli zdefiniowaliśmy własny destruktor, który ma jakieś działania uboczne, należy pamiętać o odpaleniu tego samego kodu w konstruktorze i operatorze move.

Przykłady


Pora na przykład, który pokazuje różne scenariusze, z którymi mamy do czynienia kopiując bądź przesuwając obiekty. Na początku "gadatliwa" klasa, która pokazuje, który z mechanizmów copy-control został użyty.
#include <iostream>
#include <string>
#include <vector>
#include <functional>

using namespace std;

struct Storage {
    Storage() : data("defaultData") {
        cout << "Default constr.:  " << data << endl;
    }
    Storage(std::string str) : data(str) {
        cout << "Constructor:      " << data << endl;
    }
    Storage(const Storage& st) : data(st.data) {
        cout << "Copy constructor: " << st.data << endl;
    }
    Storage(Storage&& st) noexcept : data(std::move(st.data)) {
        st.data = "old " + data + ", data was moved from here";
        cout << "Move constructor: " << data << endl;
    }
    Storage& operator=(const Storage& st) {
        if (&st != this) {
            cout << "operator= copy:   " << data << " := " << st.data << endl;
            data = "old " + data + ", now assigned " + st.data;
        } else {
            cout << "operator= copy:   " <<  data << " (this)" << endl;
        }
        return *this;
    }
    Storage& operator=(Storage&& st) noexcept {
        if (&st != this) {
            cout << "operator= move:   " << data << " := " << st.data << endl;
            string old_copy = data;
            data = std::move(st.data);
            st.data = "old " + data + ", data was moved from here";
            data = "old " + old_copy + ", now assigned " + data;
        } else {
            cout << "operator= move:   " << data << " (this)" << endl;
        }
        return *this;
    }
    ~Storage() noexcept {
        cout << "Destructor:       " << data << endl;
    }

    std::string data;
};
W przykładach simpleExample5 i simpleExample6, rzutujemy (std::move) wartość do rvalue reference - jest to referencja, więc stare dane zostają na miejscu (nic nie jest przesuwane, ani kopiowane).
void simpleExample1() {
    Storage st("val_1");
    st = st;
}

void simpleExample2() {
    Storage st1("val_1");
    Storage st2("val_2");
    st1 = st2;
}

void simpleExample3() {
    Storage st1("val_1");
    Storage st2("val_2");
    st1 = std::move(st2);
}

void simpleExample4() {
    Storage st1("val_1");
    Storage st = std::move(st1);
}

void simpleExample5() {
    Storage st1("val_1");
    Storage&& st = std::move(st1);
}

void simpleExample6() {
    Storage&& st1 = std::move(Storage("val_1"));
}

int main() {
    cout << "simpleExample1():" << endl;
    simpleExample1();
    cout << "simpleExample2():" << endl;
    simpleExample2();
    cout << "simpleExample3():" << endl;
    simpleExample3();
    cout << "simpleExample4():" << endl;
    simpleExample4();
    cout << "simpleExample5():" << endl;
    simpleExample5();
    cout << "simpleExample6():" << endl;
    simpleExample6();
}
Wynik dla simpleExample*:
simpleExample1():
Constructor:      val_1
operator= copy:   val_1 (this)
Destructor:       val_1

simpleExample2():
Constructor:      val_1
Constructor:      val_2
operator= copy:   val_1 := val_2
Destructor:       val_2
Destructor:       old val_1, now assigned val_2

simpleExample3():
Constructor:      val_1
Constructor:      val_2
operator= move:   val_1 := val_2
Destructor:       old val_2, data was moved from here
Destructor:       old val_1, now assigned val_2

simpleExample4():
Constructor:      val_1
Move constructor: val_1
Destructor:       val_1
Destructor:       old val_1, data was moved from here

simpleExample5():
Constructor:      val_1
Destructor:       val_1

simpleExample6():
Constructor:      val_1
Destructor:       val_1
W przykładzie returnValue1, kompilator sam się domyśli, że stworzona lokalnie (w ret()) zmienna zaraz zostanie zniszczona, więc zamiast korzystać z operatora przypisania (jak to było dawnej) zoptymalizuje tą operację przesuwając stworzoną zmienną lokalną. Ponieważ wyłączona jest optymalizacja "copy elision", widać dwa przesunięcia. Pierwsze przesuwa lokalną zmienną do zmiennej na stosie, drugie ze zmiennej na stosie do docelowej zmiennej st.

W drugim przypadku (returnValue2), mamy podobną sytuację, z tą różnicą, że zamiast "move operatora" działa "move konstruktor". Gdyby "copy elision" było włączone, kompilator prawdopodobnie wyciągnąłby wywołanie z tak prostej funkcji jak ret() i wywołał po prostu konstruktor w ciele naszego przykładu.
Storage ret() {
    Storage tmp("local_val");
    return tmp;
}

void returnValue1() {
    Storage st("val_1");
    st = ret();
}

void returnValue2() {
    Storage st = ret();
}

int main() {
    cout << "returnValue1():" << endl;
    returnValue1();
    cout << "returnValue2():" << endl;
    returnValue2();
}
Wyniki dla returnValue*:
returnValue1():
Constructor:      val_1
Constructor:      local_val
Move constructor: local_val
Destructor:       old local_val, data was moved from here
operator= move:   val_1 := local_val
Destructor:       old local_val, data was moved from here
Destructor:       old val_1, now assigned local_val

returnValue2():
Constructor:      local_val
Move constructor: local_val
Destructor:       old local_val, data was moved from here
Move constructor: local_val
Destructor:       old local_val, data was moved from here
Destructor:       local_val
Ostatni przykład pokazuje, że do funkcji można przekazać parametr korzystając z std::move, a więc niejako "prosząc" o skorzystanie z mechanizmu move semantic. Gdyby parametr był oznaczony modyfikatorem const, nie było by to możliwe.
Storage set(Storage st_set) {
    return st_set;
}

void settingValue1() {
    Storage st1("val_1");
    Storage st = set(st1);
}

void settingValue2() {
    Storage st = set(Storage("val_1"));
}

void settingValue3() {
    Storage st1("val_1");
    Storage st = set(std::move(st1));
}

int main() {
    cout << "settingValue1():" << endl;
    settingValue1();
    cout << "settingValue2():" << endl;
    settingValue2();
    cout << "settingValue3():" << endl;
    settingValue3();
}
Wyniki dla settingValue*.
settingValue1() - "copy elision" OFF:
Constructor:      val_1
Copy constructor: val_1
Move constructor: val_1
Move constructor: val_1
Destructor:       old val_1, data was moved from here
Destructor:       old val_1, data was moved from here
Destructor:       val_1
Destructor:       val_1

settingValue1() - "copy elision" ON:
Constructor:      val_1
Copy constructor: val_1
Move constructor: val_1
Destructor:       old val_1, data was moved from here
Destructor:       val_1
Destructor:       val_1

settingValue2():
Constructor:      val_1
Move constructor: val_1
Move constructor: val_1
Move constructor: val_1
Destructor:       old val_1, data was moved from here
Destructor:       old val_1, data was moved from here
Destructor:       old val_1, data was moved from here
Destructor:       val_1

settingValue3():
Constructor:      val_1
Move constructor: val_1
Move constructor: val_1
Move constructor: val_1
Destructor:       old val_1, data was moved from here
Destructor:       old val_1, data was moved from here
Destructor:       val_1
Destructor:       old val_1, data was moved from here
Do funkcji pass możemy przekazać tylko rvalue reference (przez rzutowanie std::move), wszelkie próby wsadzenia parametru lvalue ("jeśli zmienna ma nazwę to jest lvalue") zakończą się błędem. Taki parametr najczęściej pojawia się w metodach bibliotecznych i informuje, że możemy ukraść zasoby ze zmiennej. Towarzyszy jej bliźniacza metoda (przyjmująca const T&) będąca wersją służącą do kopiowania zasobów.
Storage pass(Storage&& st) {
    return st;
}

void passingValue1() {
    Storage st1("val_1");
    Storage st = pass(std::move(st1));
}

int main() {
    cout << "passingValue1():" << endl;
    passingValue1();
}
Wynik dla passingValue*:
passingValue1():
Constructor:      val_1
Copy constructor: val_1
Move constructor: val_1
Destructor:       old val_1, data was moved from here
Destructor:       val_1
Destructor:       val_1
Próba zdefiniowania własnego konstruktora kopiującego, operatora przypisania albo destruktora kończy się tym, że kompilator nie wygeneruje za nas operatora i konstruktora move.
struct StorageDestructor : public Storage {
    ~StorageDestructor() noexcept {
        cout << "Own destructor:   " << data << endl;
    }
};

void deriveWithDestructor1() {
    StorageDestructor st1;
    StorageDestructor st2;
    st1 = std::move(st2);
}

int main() {
    cout << "deriveWithDestructor1():" << endl;
    deriveWithDestructor1();
}
Wynik dla deriveWithDestructor1:
deriveWithDestructor1():
Default constr.:  defaultData
Default constr.:  defaultData
operator= copy:   defaultData := defaultData
Own destructor:   defaultData
Destructor:       defaultData
Own destructor:   old defaultData, now assigned defaultData
Destructor:       old defaultData, now assigned defaultData
Kolejny przykład. Choć klasa przez zdefiniowanie własnego destruktora zapobiega stworzeniu (implicit) konstruktora i operatora move, nadal mamy możliwość wymusić ich powstanie przez skorzystanie z default.
struct StorageWithDefaultMove : public Storage {
    // Destruktor zapobiega niejawnemu wygenerowaniu operatora move
    ~StorageWithDefaultMove() noexcept { }
    // Wymuszamy wygenerowania operatora move!
    StorageWithDefaultMove& operator=(StorageWithDefaultMove&&) = default;
};

void dervieWithOwnMoveAssign1() {
    StorageWithDefaultMove st1;
    StorageWithDefaultMove st2;
    st1 = std::move(st2);
}

int main() {
    cout << "dervieWithOwnMoveAssign1():" << endl;
    dervieWithOwnMoveAssign1();
}
Wynik dla dervieWithOwnMoveAssign1:
dervieWithOwnMoveAssign1():
Default constr.:  defaultData
Default constr.:  defaultData
operator= move:   defaultData := defaultData
Destructor:       old defaultData, data was moved from here
Destructor:       old defaultData, now assigned defaultData

Reference qualifier


Nowe definicje i mechanizmy wstecznej kompatybilności sprawiły, że biblioteka standardowa pozwala teraz na przypisywanie do rvalue! Czasami takie użycie może być zaskakujące. Poniższy kod jest prawidłowy.
string s1;
string s2;
s1 + s1 = "Hello world!";
Możemy się przed tym ustrzec w naszych klasach, zmuszając by operand lewostronny (obiekt, na który wskazuje this), był lvalue. Pozwala na to reference qualifier (& dla lvalue, oraz && dla rvlaue), który stosuje się tak jak const, gdy chcemy oznaczyć metody, które nie mogą modyfikować pól obiektu.

Tak jak const, kwalifikatory te można stosować dla funkcji nie statycznych, muszą też znajdować się w deklaracji i definicji, a jeżeli występują razem z const zapisuje się je na końcu. Jeżeli definiujemy dwie lub więcej metod, które mają takie same nazwy i listę parametrów, musimy wprowadzić reference qualifier dla nich wszystkich, albo dla żadnej.

Przykład. Utworzona został operator=, ale tylko dla lvalue. Dodatkowo stworzone zostały dwie metody info, wołające różny kod w zależności od tego na jakim obiekcie zostały zawołane.
#include <iostream>
using namespace std;

class Foo {
public:
    Foo operator+(const Foo &) {
        return *this;
    }
    Foo &operator=(const Foo &) & {
        return *this;
    }

    void info() const & {
        cout << "info: lvalue" << endl;
    }
    void info() && {
        cout << "info: rvalue" << endl;
    }
};

Foo retFoo() {
    return Foo();
}

int main() {
    Foo f1;
    Foo f2;
    Foo f3;

//  (f1 + f2) = f3;     // Error - zapis do rvalue, ale brak takiego operatora
//  retFoo() = f3;      // Error - zapis do rvalue, ale brak takiego operatora

    f1.info();
    retFoo().info();
}
Wynik:
info: lvalue
info: rvalue

3 listopada 2013

[C++11] Współbieżność

Pierwsze podejście do współbieżności, wprowadzonej do biblioteki standardowej wraz z nowym standardem.
#include <thread>
#include <iostream>

void hello()
{
    std::cout << "Hello World!" << std::endl;
}

int main()
{
    std::thread t(hello);
    t.join();
    return 0;
}
Było trochę kłopotów przy próbie kompilacji (gcc - 4.7.3, clang - 3.2-1). gcc potrzebuje dodatkowej flagi w postaci pthread, natomiast clang w wersji 3.2 jeszcze nie radzi sobie z wątkami, ale i na to znalazł się sposób.
Budowanie z linii poleceń:
$ g++ -pthread --std=c++11 main.cpp
$ clang++ -pthread --std=c++11 \
     -D__GCC_HAVE_SYNC_COMPARE_AND_SWAP_1 \
     -D__GCC_HAVE_SYNC_COMPARE_AND_SWAP_2 \
     -D__GCC_HAVE_SYNC_COMPARE_AND_SWAP_4 \
     -D__GCC_HAVE_SYNC_COMPARE_AND_SWAP_8 main.cpp
Poniżej ustawienie dla CMakeList.txt (cmake):
project(thread_hello)
cmake_minimum_required(VERSION 2.8)
aux_source_directory(. SRC_LIST)
add_executable(${PROJECT_NAME} ${SRC_LIST})
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread")

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ę.

5 lutego 2013

Deklaracja zmiennych i trochę o const/constexpr

Najpierw małe przypomnienie, jak wygląda deklaracja zmiennych w C++. Rzadko stosuję i spotykam deklaracje zmiennych jakiegoś typu po przecinku, stąd czytając książkę, sam byłem zaskoczony, co tak naprawdę znaczył ten kod.
int i, *ip = nullptr; // i jest typu int, ip jest wskaźnikiem na int.
int* p1, p2;          // p1 jest wskaźnikiem na int, p2 jest typu int.
int* a, &b = *ip;     // a jest wskaźnikiem na int, b jest referencją na wartość 
                      // wskazywaną przez ip.
A teraz trochę o const. Zawsze jest to dla mnie mylące, przynajmniej po dłuższym okresie czasu, gdy wiedza powolutku wyparowuje.
const int c = 8;  // c jest stałą typu int, jego wartość nie może być zmieniona.
const int &r = c; // ok, referencja na stały obiekt typu int - mówimy co będziemy mogli 
                  // robić przy  pomocy referencji - odwołując się do r nie damy rady 
                  // zmodyfikować c (chociaż sam w sobie nie jest const).
int &r2 = c;      // error, referencja na niestały obiekt.
Nie ma czegoś takiego jak stałe referencje (z technicznego punktu widzenia), są tylko referencje na stałe. Ponieważ nie można zmusić referencji by wskazywała na inny obiekt w jakimś sensie wszystkie referencje są stałe.
double dval = 3.14;
const double *pt = &dval; // ok (tak jak wyżej), ale nie możemy zmieniać dval, przy pomocy pt
A teraz co powstanie, gdy const znajdzie się w różnych miejscach? Pewne oznaczenia - cytując za książką:
Używamy terminu top-level const do oznaczenia, że sam wskaźnik jest stały. Kiedy wskaźnik może wskazywać na stały obiekt, odnosimy się do tego jako low-level const
int i = 0;
const int c1 = 88;        // c1 jest stałym obiektem typu int
int const c2 = 99;        // (TO SAMO) c2 jest stałym obiektem typu int

int *const p1 = &i;       // p1 jest stałym wskaźnikiem na obiekt typu int (top-level const).
const int ci = 42;        // ci jest stałym obiektem typu int (top-level const).
const int *p2 = &ci;      // p2 jest wskaźnikiem na stały obiekt typu int (low-level).
int const *p3 = &ci;      // (TO SAMO) p3 jest wskaźnikiem na stały obiekt typu int.
const int *const p4 = p2; // p3 jest stałym wskaźnikiem na stały obiekt typu int
                          // (prawy const jest top-level; lewy nie)
const int &r = i;         // referencja na stały obiekt typu int.
                          // const w typach referencyjnych zawsze jest low-level.
W odróżnieniu od referencji, wskaźniki są obiektami, więc mogą być const, co znaczy, że po ustawieniu w momencie inicjalizacji nie mogą na nic innego wskazywać.

A teraz o nowościach. const expression jest wyrażeniem, którego wartość nie może zostać zmieniona, i dla którego wartość powinna być (ale nie musi) obliczona podczas kompilacji. W C++11 pojawia się nowy modyfikator constexpr, który zbada, czy tak rzeczywiście się stanie. Funkcje z constexpr muszą być na tyle proste by kompilator poradził sobie z nimi podczas kompilacji, ale w kolejnych wersjach standardu konstrukcje będą stawać się coraz bardziej skomplikowane (pętle, algorytmy STL, kontenery itp.). W zamyśle constexpr ma stać się alternatywą dla metaprogramowania w szablonach.
#include <iostream>
#include <stdio.h>

using namespace std;

constexpr int get_size()
{
    return 6;
}

int main()
{
    constexpr int sz = get_size() + 80; // ok, pod warunkiem, że funkcja też jest constexpr.
    printf("%d", sz);
    return 0;
}
I podgląd tego co wyprodukował kompilator (g++ -std=c++17 -S -masm=intel main.cpp). Jak widać w pliku binarnym pojawia się już obliczona wartość (80+6=86).
main:
.LFB1824:
    .cfi_startproc
    endbr64
    push    rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    mov     rbp, rsp
    .cfi_def_cfa_register 6
    sub     rsp, 16
    mov     DWORD PTR -4[rbp], 86
    mov     esi, 86
    lea     rdi, .LC0[rip]
    mov     eax, 0
    call    printf@PLT
    mov     eax, 0
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc

26 grudnia 2012

C++11/17 - inicjalizacja zmiennych

Nowy standard dostarcza nowy sposób inicjalizacji zmiennych (można nareszcie wygodnie inicjalizować kontenery). Co ciekawe, w przypadku typów prostych i konwersji, która może doprowadzić do utraty danych kompilator wyświetli ostrzeżenie.
#include <iostream>

using namespace std;

int main()
{
    long double ld = 3.1415926536;
    int curly_from_ld_1{ld};    // warning and data lost
    int curly_from_ld_2 = {ld}; // warning and data lost
    cout << ld << " " << curly_from_ld_1 << " " << curly_from_ld_2 << endl;

    int round_from_ld_1(ld);    // no-warning, data lost
    int round_from_ld_2 = ld;   // no-warning, data lost
    cout << ld << " " << round_from_ld_1 << " " << round_from_ld_2 << endl;

    long long ll = 5000000000;
    int curly_from_ll_1{ll};    // warning and data lost
    int curly_from_ll_2 = {ll}; // warning and data lost
    cout << ll << " " << curly_from_ll_1 << " " << curly_from_ll_2 << endl;

    int i = 56;
    long long curly_from_int{i};
    cout << curly_from_int << endl;

    return 0;
}

Testowane z g++ (Ubuntu 9.2.1-9ubuntu2) 9.2.1 20191008,
$ g++ -std=c++17 main.cpp 

main.cpp: In function ‘int main()’:
main.cpp:8:25: warning: narrowing conversion of ‘ld’ from ‘long double’ to ‘int’ [-Wnarrowing]
    8 |     int curly_from_ld_1{ld};    // warning and data lost
      |                         ^~
main.cpp:9:28: warning: narrowing conversion of ‘ld’ from ‘long double’ to ‘int’ [-Wnarrowing]
    9 |     int curly_from_ld_2 = {ld}; // warning and data lost
      |                            ^~
main.cpp:17:25: warning: narrowing conversion of ‘ll’ from ‘long long int’ to ‘int’ [-Wnarrowing]
   17 |     int curly_from_ll_1{ll};    // warning and data lost
      |                         ^~
main.cpp:18:28: warning: narrowing conversion of ‘ll’ from ‘long long int’ to ‘int’ [-Wnarrowing]
   18 |     int curly_from_ll_2 = {ll}; // warning and data lost
      |                            
Wynik:
3.14159 3 3
3.14159 3 3
5000000000 705032704 705032704
56

Uniform initialization

W C++17 prawie się udało wprowadzić ujednolicony sposób inicjalizacji. Ciągle jeszcze są pewne problemy (linijka 14, a także coś z atomics i agregatami), ale zdaje się że zostaną one naprawione w przyszłych wersjach standardu. W każdym razie programiści są zachęcani, by wszędzie tam gdzie to możliwe stosować klamerki w celu inicjalizacji [1].
#include <iostream>
#include <boost/type_index.hpp>

using namespace std;

int main()
{
    auto var1{17};          // std::initializer_list<int> w C++11, w C++17 int
//  auto var2{1, 3, 6};     // std::initializer_list<int> w C++11, w C++17 ERROR

    cout << boost::typeindex::type_id_with_cvr<decltype(var1)>().pretty_name() << endl;
//  cout << boost::typeindex::type_id_with_cvr<decltype(var2)>().pretty_name();

    auto var3 = {1, 3, 6};  // Ups nieintuicyjne, nadal std::initializer_list<int>

    cout << boost::typeindex::type_id_with_cvr<decltype(var3)>().pretty_name() << endl;
}
Wynik:
int
std::initializer_list<int>
Inne ciekawostki, to inicjalizacja FDT (fundamental data types) domyślną wartością (0, false, nullptr), gdy nawiasy klamerkowe będą puste. Struktury (także dziedziczące), możemy inicjalizować za pomocą zagnieżdżonych nawiasów, ale dodano także wygodne odwoływanie się do nazw pól:
#include <iostream>

using namespace std;

struct Base {
    int age;
    double high;
};

struct Child : public Base {
    string fun() { return name; };
    string name;
};

int main()
{
    int val{};
    cout << "FDT (fundamental data type): " << val << endl;

    Child child1{};     // czyli to samo co child1{0, 0, ""};
    cout << child1.age << " " << child1.high << " " << child1.name << endl;

    Child child2{{.age=1, 2}, .name="Tom"};
    cout << child2.age << " " << child2.high << " " << child2.name << endl;
}
Wynik:
FDT (fundamental data type): 0
0 0 
1 2 Tom

23 grudnia 2012

Prosty Makefile

Musiałem zbudować, bardzo mały plik Makefile. Tutaj przydatny tutorial:
http://www.cs.colby.edu/maxwell/courses/tutorials/maketutor/

Make bez argumentów uruchomi, pierwszą napotkaną regułę, w tym przypadku będzie to main. Po dwukropku możemy podać listę (oddzieloną spacjami) zależność np. nazwy reguł na zbudowanie plików *.o, zanim, będziemy z nich korzystać podczas linkowania. Komendy, które składają się na daną regułę muszą być poprzedzone tabulatorem!
CXX=g++

main:
        $(CXX) -o main main.cc Lion.cc Lungs.cc -I.

Istnieje możliwość przekazania parametrów, do skryptu, w poniższym przykładzie, zmieniamy kompilator z domyślnego gcc, na clang-a.
$ make CXX=clang++
clang++ -o main main.cc Lion.cc Lungs.cc -I.

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.

7 listopada 2012

Struct - inicjalizacja zerami i -Wextra (część II)

A co gdyby, do inicjalizacji użyć pustych klamerek? Na stronie http://www.ex-parrot.com/~chris/random/initialise.html można przeczytać:
It would be neater to be able to say struct foo bar = {}, and some compilers (e.g. GCC) permit this, but the ANSI grammar requires an initialiser list to be non-empty.
Czyli generalnie nie można, bo niezgodne ze standardem. Trzeba więc przekonać się na własnej skórze.
struct Stru2 {
    int c;
    int d;
};

struct Stru1 {
    int a;
    Stru2 b;
};

int main()
{
    struct Stru1 aaa = {};
    return aaa.b.c;
}
Mimo, że GCC nam na to pozwala, to chciałem wymusić zgodność ze standardem ANSI, (chociaż bardziej mnie interesuje co się stanie w C++, a nie w C).
gcc -Wall -pedantic -ansi main.cpp
Hmm, obyło się bez protestów. Jeszcze jedna próba tym razem z -Wextra
gcc -Wall -Wextra -pedantic -ansi main.cpp
Tutaj się już coś dzieje. Ciekawe sprawa, bo nawet gdy skorzystam z zapisu "Stru1 aaa = {0}", to dalej są protesty o nie zainicjowane zmienne.
main.cpp:36:25: warning: missing initializer for member ‘Stru1::a’ [-Wmissing-field-initializers]
main.cpp:36:25: warning: missing initializer for member ‘Stru1::b’ [-Wmissing-field-initializers]
Zasięgnąłem opinii na stackoverflow.com i z odpowiedzi wynika, że -Wextra, martwi się bardziej niż pozawala na to standard i choć wszystko zostanie zainicjalizowane zerami, to teraz kompilator chciałby, żebyśmy sami zainicjalizowali każde pole z osobna. Jeszcze rzut oka, na to co wygenerował kompilator. Wszystko na zero ;)
.LFB0:
    .cfi_startproc
    pushl    %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    subl    $16, %esp
    movl    $0, -12(%ebp)
    movl    $0, -8(%ebp)
    movl    $0, -4(%ebp)
    movl    -8(%ebp), %eax
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc