C++Guns – RoboBlog blogging the bot

27.07.2018

C++ Guns: use FORTRAN debug techniques in C++

Filed under: Allgemein — Tags: , — Thomas @ 15:07

On of the first things one learn about FORTRAN: you can very easy write stuff to files without getting worried about file creation of filenames.
Just write to a random number, the file handle. You can do this everywhere in the code. The output is append to the file. And the file gets automatic cleaned on program start.
Here is one example:

program debug
  integer :: x
  x = 1
  write(1001,*) "This is string", x
  write(1001,*) "The End"
end program

$ gfortran debug.f90
$ ./a.out
$ cat fort.1001
This is string 1
The End

As you can see, the file handle number get prefixed with "fort.". This is pretty nice for some nasty debug session, so lets do this with C++!

The implementation in C++ is surprisingly easy, just a single line of code and we are done:

#include <fstream>

template<int fhdl>
inline std::ofstream& write() {
    static std::ofstream ostrm("cpp."+std::to_string(fhdl), std::ios::out);
    return ostrm;
}

int main() {
    int x = 1;
    write<1001>() << "This is string " << x << "\n";
    write<1001>() << "The End\n";
}

$ g++ debug.cpp
$ ./a.out
$ cat cpp.1001
This is string 1
The End

The file handle number is passed as template parameter. I guess it is always know at compile time during the debug session. Then the write() function simply returns a reference to a static standard ofstream object. Which is created during the first call to write(). The created files are prefixed with "cpp." And you can use this with any custom type with overloaded operator<<. Thats all.

24.07.2018

C++ Guns: Rolladensteuerung 1

Filed under: Allgemein — Tags: — Thomas @ 21:07

Hier habe ich ein kleines Projekt eines Freundes zur Steuerung eines Rolladens mittels Mikrocomputer, selbst gebastelte Elektronik und natürlich C++.

Die ganze Anlage wird über Textdateien gesteuert. So ist der Zustand des Rolladenmotor durch vier Relais in einer Datei mit Buchstaben kodiert. Das Programm soll nun die Status Datei einlesen, modifizieren und wieder raus schreiben.

Klein muss man anfangen, hier mal die erste Version.
Man kann natürlich immer weiter verbessern. Die ausführlichen Schleifen zum Beispiel verstecken. So verhindert man ein paar Index Fehler. Oder ein paar Sensoren mit einbeziehen die bei zu viel Sonne die Motoren aktivieren.

Hier ein Video der funktionierende Hard+Software

https://www.youtube.com/watch?v=wBCo-W7wVNA

#include <iostream>
#include <fstream>
#include <chrono>
#include <thread>
#include <array>

using namespace std;
using namespace std::chrono;

// Operator << ueberladen um einfach eine Zeitspanne auf der Konsole auszugeben
std::ostream& operator<<(std::ostream& s, std::chrono::duration<double> dur) {
    // Sekunden, Minuten u.s.w. sind im Standard definiert. Ein Tag leider nicht. Aber das ist kein Problem.
    // Ein Tag hat bekanntlich 24 Stunden. Und eine Stunde hat 3600 Sekunden.
    using day = std::chrono::duration<int64_t, std::ratio<24*3600>>;

    const auto days = std::chrono::duration_cast<day>(dur);
    dur -= days;
    if(days.count() > 0) s << days.count() << "d ";

    const auto HH = std::chrono::duration_cast<std::chrono::hours>(dur);
    dur -= HH;
    if(HH.count() > 0) s << HH.count() << "h ";

    const auto min = std::chrono::duration_cast<std::chrono::minutes>(dur);
    dur -= min;
    if(min.count() > 0) s << min.count() << "m ";

    const auto sec = std::chrono::duration_cast<std::chrono::seconds>(dur);
    dur -= sec;
    if(sec.count() > 0) s << sec.count() << "s ";

    const auto msec = std::chrono::duration_cast<std::chrono::milliseconds>(dur);
    if(msec.count() > 0) s << msec.count() << "ms";

    return s;
}

// 4 Relais werden benutzt
static const int N = 4;
// Zeit bis das Relais von AN nach AUS wechselt
static const auto waittime = 10s;

// Ein Relais kann entweder an, oder aus sein. Dieser Sachverhalt laesst sich gut als strong typed enum ausdruecken.
enum class RelaisStatus{ AUS, AN };

// Operator << ueberladen um den Zustand eines Relais einfach auf der Konsole auszugeben
std::ostream& operator<<(std::ostream& s, const RelaisStatus& status) {
    switch(status) {
    // Entweder das Relais ist an...
    case RelaisStatus::AN: s << "AN"; break;
        // Oder es ist aus. Das ist auch der default Fall.
    default:
    case RelaisStatus::AUS: s << "AUS"; break;
    }

    return s;
}

// Lese die Statusdatei ein.
// Relais AN ist als "a" codiert. Alles anderes wird als "AUS" interpretiert.
auto read() {
    array<RelaisStatus,N> status{ RelaisStatus::AUS};
    ifstream ff;
    ff.open("datei.txt", ios::in);

    for(int i=0; i < N; ++i) {
        if(ff.eof()) {
            break;
        }
        string s;
        getline(ff, s);
        if(s == "a") {
            status[i] = RelaisStatus::AN;
        }
    }
    ff.close();

    return status;
}

// Schreibe Statusdatei.
// AN wird als der Buchstabe "a" kodiert. AUS als der Buchstabe "b"
void write(const array<RelaisStatus,N>& status) {
    fstream f;
    f.open("datei.txt", ios::out);
    for(int i=0; i < N; ++i) {
        if(status[i] == RelaisStatus::AN) {
            f << "a\n";
        } else {
            f << "b\n";
        }
    }
    f.close();
}

int main() {
    const auto start = system_clock::now();
    array<chrono::system_clock::time_point,N> warten;
    array<bool,N> aktiv{false};

    cout << "Default Wartezeit " << waittime << "\n";

    while(true) {
        cout << "Time " << system_clock::now()-start << "\n";
        array<RelaisStatus,N> status = read();

        // Wenn das Relais an geht, und nicht schon an war,
        // wird der Zeitpunkt ermittelt wann es wieder aus gehen soll
        for(int i=0; i < N; ++i) {
            cout << status[i] << " ";
            if(status[i] == RelaisStatus::AN and not aktiv[i]) {
                aktiv[i] = true;
                warten[i] = system_clock::now() + waittime;
            }
        }
        cout << "\n";
        // Fanzy Befehle um den Inhalt der Konsole zu loeschen
        //        cout << "\033[2J\033[1;1H";

        // Wenn der Zeitpunkt verstrichen ist wann das Relais aus gehen soll, Relais ausschalten.
        for(int i=0; i < N; ++i) {
            if(aktiv[i] and system_clock::now() >= warten[i]) {
                aktiv[i] = false;
                status[i] = RelaisStatus::AUS;
            }
        }

        write(status);

        // Program kurz pausieren um nicht zuviel CPU Last zu erzeugen
        this_thread::sleep_for(0.5s);
    }
    return 0;
}

22.07.2018

C++ Guns: apply generic lambda to tuple

Filed under: Allgemein — Tags: — Thomas @ 13:07

Part 1: print std::array with std::integer_sequence
Part 2: convert tuple to parameter pack
Part 3: print std::array with std::apply and fold
Part 4: fold over std::tuple und erzeugten Assembler Code
Part 5: fold over std::tuple of std::vector of Types ...
Part 6: apply generic lambda to tuple
Part 7: Play with std::tuple and std::apply

Hm, generic lambdas sind toll.

template<typename...Args, typename Func>
auto func(const std::tuple<Args...>& t, Func p) {     
    auto f = [&p](const auto&...x){ (p(x), ...); };
    std::apply(f, t);
}

auto func2(const std::tuple<int,double>& t) {
    auto print_dat_shiat = [](const auto& p) { std::cout << typeid(p).name() << std::endl; };     
    func(t, print_dat_shiat);
}

21.07.2018

Why C++ sucks (2016.02 edition) - Schwachsinn

Filed under: Allgemein — Tags: — Thomas @ 18:07

Schaut mal https://dorinlazar.ro/why-c-sucks-2016-02-edition/
Dieser Mensch hat nicht verstanden was C++ ist. Das zeigt nur ein Zitat aus diesem Text und ihr könnt ihn gleich darauf in die Mülltonne werfen.

My point was that there is no clear flow to work with the language.

Er hat nicht verstanden, dass C++ eine general purpose Sprache ist.

Wenn er keinen Code schreiben kann, der seinen Ansprüchen genügt, dann ist das nicht der Fehler der Sprache. Denn die C++ Sprache erlaubt es sämtlichen Code zu schreiben, der überhaupt auf einem Computer ausführbar ist.

C+ Guns: Praxisbeispiel: Objekt v.s. Funktional

Filed under: Allgemein — Tags: — Thomas @ 15:07

Objekt v.s. Funktional
Keine Angst, ich will hier keinen Glaubenskrieg auslösen. Mich interessieren mögliche Performance Nachteile und wie sie entstehen.

Das Beispiel zeigt einen vereinfachten Codeausschnitt aus der Praxis, bei dem jede Variable als Funktionsargument übergeben wird. Meine Frage ist nun, ob dies Performancetechnich schlechter ist, als alle Funktionen als Memberfunktionen auszulegen und alle Variablen als Membervariablen.

Fazit:
Für das gewählte Beispiel mit drei Funktionsargumenten entsteht kein nennenswerter Performance Unterschied. Die Lesbarkeit des Code, und damit die Fehlerfreiheit und Wartbarkeit des Codes, leidet allerdings stark wenn jede benötigte Variable als Funktionsargument übergeben wird. Die Methode mit Memberfunktionen löst nicht alle Probleme. Die beste Kombination der Paradigmen von C++ habe ich noch nicht gefunden.

Hier der Kern des Programms ohne Memberfunktionen.

extern double calcT(const double Tmax, const vector<Vec3>& U, const M& m, bool& finish);
extern void calcF(const vector<Vec3>& U, const M&m, vector<Vec3>& F);
extern void calcE(const vector<Vec3>& U, int nE, const vector<E_t>& E, const M& m);
extern void calcQ(const vector<Vec3>& U, int nE, const vector<E_t>& E, const M& m, const vector<Vec2>& iN, const vector<int>& NCO, const vector<int>& CO, vector<Vec3>& Q);
extern void calcI(vector<Vec3>& U, double T, const vector<Vec3>& F, const M& m, const vector<Vec3>& Q);
extern void calcC(const vector<Vec3>& U, vector<Vec2>& C);

auto exec(double Tmax, const M& m, const vector<int>& NCO, const vector<int>& CO,
    const int nE, const vector<E_t>& E, const vector<Vec2>& iN,
    vector<Vec3>& U, vector<Vec2>& C, vector<Vec3>& F, vector<Vec3>& Q) 
{
    double Tinc = 0;
    while(Tinc < Tmax) {
        double T = calcT(Tmax, U, m);
        Tinc += T;
        calcF(U, m, F);
        calcE(U, nE, E, m);        
        calcQ(U, nE, E, m, iN, NCO, CO, Q);
        calcI(U, T, F, m, Q);
        calcC(U, C);
    }
}

auto func() {
    double Tmax;
    M m;
    vector<Vec3> U;
    vector<Vec3> F;
    int nE;
    vector<E_t> E;
    vector<Vec2> iN;
    vector<Vec3> Q;
    vector<Vec2> C;
    vector<int> NCO;
    vector<int> CO;
    double T;

    init(...);
    exec(Tmax, m, NCO, CO, nE, E, iN, U, C, F, Q);
}

Es gibt eine Reihe von Funktionen die nacheinander mit unterschiedlichen Variablen aufgerufen werden und teilweise die übergebenen Argumente verändern. Die Funktions- und Variablennamen sind schon stark gekürzt, einerseits um das Beispiel einfach zu halten, andererseits ist die Lesbarkeit durch die Übergabe jeder einzelnen Variable schon beeinträchtigt. Die Funktionen selbst sind als "extern" ausgelegt, da zu diesem Zeitpunkt nicht die genaue Implementierung interessiert. Weiterhin sei angenommen, dass die Funktionen selbst alle groß und komplex sind, so dass der Compiler den Funktionsaufruf nicht durch inline eliminiert.

Es gibt eine ganze Reihe von Nachteilen. Ich will sie nicht im Detail erläutern, nur kurz aufzählen:
- Erstes natürlich die redselige Form des Codes. Es wird oft an vielen Stellen immer der selbe Code wiederholt.
- Mit aussagekräftigen Variablen Namen wird der Code nur noch schlechter lesbar. Das ist ein Widerspruch. Der Grund hierfür ist sind die Funktionsdeklaration, die nun sehr lang werden, oder sich über mehrere Zeilen erstreckt.
- Es ist zwar ersichtlich, welche Funktion von welcher Variablen abhängt, aber nicht welche Variablen geändert werden. Natürlich kann man eine Konvention eingehen und nicht konstanten Argumente an das Ende der Argumente legen, aber das ist keine Garantie.
- Die Sache dass eine Funktion ihr Ergebnis als Rückgabewert liefert ist auch nicht so einfach. Die Ergebnisse sind große Arrays die auf keinen Fall zur Rechenzeit neu allokiert werden dürfen. Wie kann man das garantieren?
- Auch können die Argumente leicht verwechselt werden. Die Variablen F und Q haben den selben Typ. Natürlich lässt sich das Problem durch explizite Typen wie 'F_t' und 'Q_t' lösen, aber das erhöht wieder den Codebedarf.

Die Variante mit Memberfunktionen löst einige Probleme. Hier zum Vergleich der selbe Code etwas umgeschrieben:

struct Methode1 {
    const double Tmax;
    const M m;
    vector<Vec3> U;
    vector<Vec3> F;
    const int nE;
    vector<E_t> E;
    const vector<Vec2> iN;
    vector<Vec3> Q;
    vector<Vec2> C;
    const vector<int> NCO;
    const vector<int> CO;

    void init() { ... };

    double calcT(bool& finish);
    void calcF();
    void calcE();
    void calcQ();
    void calcI();
    void calcC();

    auto exec() {
        double Tinc = 0,
        while(Tinc < Tmax) {
            double T = calcT();
            Tinc += T;
            calcF();
            calcE();        
            calcQ();
            calcI();
            calcC();
        }
    }
};

Viel redundanter Code fällt weg. Allerdings ist auch nicht mehr ersichtlich welche Funktion von welcher Variablen abhängt oder sie ändert. Darauf gehen ich im nächsten Post ein. Momentan interessiert die Performance. Dazu schauen wir uns am besten den Assemblercode an. Für alle die kein Assembler lesen können, habe ich den Code etwas dokumentiert.

Hier ein Ausschnitt der Funktion "exec" vom ersten Code Beispiel:

.L2:                                         # Begin Schleife
        movq    %rbp, %rsi                   # copy address of variable m to rsi
        movq    %rbx, %rdi                   # copy address of variable U to rdi
        movsd   16(%rsp), %xmm0              # copy Tmax to xmm0
        call    calcT(Tmax, U, m)
        movsd   8(%rsp), %xmm1               # copy Tinc, 8(rsp) to xmm1
        movsd   %xmm0, 24(%rsp)              # copy result of calcT to 24(rsp) for later use
        addsd   %xmm0, %xmm1                 # Tinc = Tinc + T
        movsd   %xmm1, 8(%rsp)               # copy Tinc back to 8(rsp)

        movq    %r13, %rdx                   # F
        movq    %rbp, %rsi                   # m
        movq    %rbx, %rdi                   # U
        call    calcF(U, m, F)

        movq    %rbp, %rcx                   # m 
        movq    %r14, %rdx                   # E
        movl    %r15d, %esi                  # nE
        movq    %rbx, %rdi                   # U
        call    calcE(U, nE, E, m)

        pushq   %r12                         # Q  is passed per stack 
        pushq   48(%rsp)                     # CO is passed per stack
        movq    48(%rsp), %r9                # NCO
        movq    128(%rsp), %r8               # iN 
        movq    %rbp, %rcx                   # m
        movq    %r14, %rdx                   # E
        movl    %r15d, %esi                  # nE
        movq    %rbx, %rdi                   # U
        call    calcQ(U, nE, E, m, iN, NCO, CO, Q)

        addq    $16, %rsp                    # add 16 to rsp to adjust stack pointer
        movq    %r12, %rcx                   # Q
        movq    %rbp, %rdx                   # m
        movq    %r13, %rsi                   # F
        movsd   24(%rsp), %xmm0              # T
        movq    %rbx, %rdi                   # U
        call    calcI(U, T, F, m, Q)

        movq    128(%rsp), %rsi              # C
        movq    %rbx, %rdi                   # U
        call    calcC(U, C)

        movsd   16(%rsp), %xmm2              # note: rsp has changed. so the offset to T and Tinc is different
        comisd  8(%rsp), %xmm2               # Tinc < Tmax ?
        ja      .L4                          # jump to loop head

Vor jedem Funktionsaufruf werden die Adressen bzw. Werte der Argumente in ein Register kopiert. Das sind auf der Aufrufer Seite mindestens so viele Instruktionen wie Funktionsargumente. In der aufgerufenen Funktion selbst gibt es wohl auch noch zusätzliche Instruktionen. Genau um diesen Mehraufwand geht es mir. Ich vermute nun, dass dieser Aufwand eingespart werden kann, ohne die Funktion zu inlinen.
Dazu müssen wir allerdings genauer in den Code rein schauen.

Beispielsweise die einfache Funktion calcT:

double calcT(const double Tmax, const vector<Vec3>& U, const M& m) {
    double T_min = Tmax;
    for(int IP=0; IP < m.MNP; ++IP) {
        double T = m.A[IP]/(U[IP][1]/U[IP][0]+U[IP][2]/U[IP][0]);
        if(T < T_min) {
            T_min = T;        
        }
    }
    return T_min;
}

Im Prinzip wird hier nur ein Minimum gesucht. Wie genau sich das Berechnet ist egal. Zur Erinnerung: Die vier Argumente 'Tmax', 'U' und 'm' der Funktion wurden per Register xmm0, rdi, rsi übergeben. Das Ergebnis per Register xmm0 zurück gegeben.

Auf den Assemblercode will ich nicht im Detail eingehen. Den kann jeder für sich selbst einmal durch kauen. Wichtig ist der Unterschied zur selben Funktion nur als Member Funktion implementiert.

calcT(Tmax, U, m):
        movl    (%rsi), %ecx
        testl   %ecx, %ecx
        jle     .L1
        movq    8(%rsi), %rdx
        movq    (%rdi), %rax
        leal    -1(%rcx), %ecx
        leaq    8(%rdx,%rcx,8), %rcx
.L5:
        movsd   (%rax), %xmm3
        movsd   8(%rax), %xmm1
        divsd   %xmm3, %xmm1
        movsd   16(%rax), %xmm2
        divsd   %xmm3, %xmm2
        addsd   %xmm2, %xmm1
        movsd   (%rdx), %xmm2
        divsd   %xmm1, %xmm2
        movapd  %xmm2, %xmm1
        minsd   %xmm0, %xmm1
        movapd  %xmm1, %xmm0
        addq    $8, %rdx
        addq    $24, %rax
        cmpq    %rcx, %rdx
        jne     .L5
.L1:
        ret

exec():
.L12:                          # Loop head
  movq    %rbp, %rsi
  movq    %rbx, %rdi
  movsd   16(%rsp), %xmm0      # copy Tmax to xmm0
  call    calcT(Tmax, U, m)    # result T is stored in xmm0
  movsd   8(%rsp), %xmm1       # copy T to xmm1 for later use
  movsd   %xmm0, 24(%rsp)      # copy T to 24(rsp)
  addsd   %xmm0, %xmm1         # Tinc += T
  movsd   %xmm1, 8(%rsp)       # copy Tinc back to 8(rsp)
...
  movsd   16(%rsp), %xmm2      # copy Tmax to xmm2
  comisd  8(%rsp), %xmm2       # Tinc < Tmax ?
  ja      .L12                 # jump to loop head

Methode1::calcT():
        movsd   (%rdi), %xmm0       #  copy Tmax to xmm0
        movl    8(%rdi), %ecx
        testl   %ecx, %ecx
        jle     .L22
        movq    16(%rdi), %rdx
        movq    40(%rdi), %rax
        leal    -1(%rcx), %ecx
        leaq    8(%rdx,%rcx,8), %rcx
.L26:
        movsd   (%rax), %xmm3
        movsd   8(%rax), %xmm1
        divsd   %xmm3, %xmm1
        movsd   16(%rax), %xmm2
        divsd   %xmm3, %xmm2
        addsd   %xmm2, %xmm1
        movsd   (%rdx), %xmm2
        divsd   %xmm1, %xmm2
        movapd  %xmm2, %xmm1
        minsd   %xmm0, %xmm1
        movapd  %xmm1, %xmm0
        addq    $8, %rdx
        addq    $24, %rax
        cmpq    %rcx, %rdx
        jne     .L26
.L22:
        ret

Methode1::exec():
.L42:                       # Loop head
  movq    %rbx, %rdi        # copy address of this objekt to rdi. Its the same address of Tmax
  call    Methode1::calcT() # result T is stored in xmm0
  movsd   (%rsp), %xmm1     # copy Tinc to xmm1
  movsd   %xmm0, 8(%rsp)    # copy T to 8(rsp) for later use
  addsd   %xmm0, %xmm1      # Tinc += T
  movsd   %xmm1, (%rsp)     # copy Tinc to rsp
...
  movsd   (%rbx), %xmm0     # Copy Tmax to xmm0
  comisd  (%rsp), %xmm0     # Tinc < Tmax
  ja      .L42              # Jump to loop head

Nun, es gibt für dieses Beispiel praktisch keinen Unterschied. Die Variante mit Member Funktionen ist eine Zeile kürzer. Woran liegt das? Statt drei Funktionsargumente zu übergeben muss nur eines übergeben werden, die Adresse von 'this'. Da die Funktion ja nicht geinlint wurde, ist das notwendig. Und zufällig ist bei diesem Algorithmus bei der ersten Variante das erste Funktionsargument gleichzeitig der Rückgabewert, wenn die Schleife nicht läuft. Dieser Schritt mit dennoch bei der zweiten Variante ausgeführt werden. So bleibt nur eine Zeile übrig die eingespart wird.

15.07.2018

C++ Guns: Why C++ don't need Python PEP 572 Assignment Expressions

Filed under: Allgemein — Tags: — Thomas @ 09:07

It is often a failure to assign a variable inside a if() instead to compare it. One can easily mistype '==' to '='. Both times the result can be converter to bool. To prevent this kind of error one languages simply forbid assignments inside if(), others have or introduce new operators.

But this is all unnecessary. Just make the variable 'const' so it's value can not change after the first assignment. No need for new operators or heavy discussions. But as far as I know there is no keyword 'const' to make the value immutable in python and the new language julia.
In C++ it is always possible to make a variable 'const'. The compile decides to reuse the memory the variable occupied. You don't have to do this by your own.

Example:

const auto x = func();
if(x = 5) {}

error: assignment of read-only variable 'x'

Advanced: Memory reuse for POD types

This is a simple,silly example how the compiler decide to reuse memory of one variable when it is no longer needed. Consider this code:

extern int func();
extern int func2();
auto example() {
    const auto x = func();
    int result =  x;    

    const auto y = func2(); // Memory of x is reused by y
    result = result + y;

    return result;
}

The two function 'func' and 'func2' are marked as 'extern' so they can't optimized away. The memory of variable x is reused by variable y, because x is no longer needed. The compiler can decide this without any hint from the user. There is no need to explicit define a scope or lifetime of variable x.
Let's introspect the generated assembler code:

example():
        call    func()      # call func store return value in x
        movl    %eax, %ebx  # copy x to result
        call    func2()     # call func2 store return value in y
        addl    %ebx, %eax  # add result to y
        ret                 # return y 

The result of a call to 'func' in line 1 is stored in register eax. This is the variable x. And the result of a call to 'func2' in line 3 is stored in register eax too. This is variable y now. The variable 'result' is stored in register ebx. No more memory is in use.

We have three variables but only two register in use. This is because I wrote a pretty silly example. You may also notice that in the C++ code the variable 'result' is returned. But in assembler the variable 'y' is returned. But the semantic is the same.

12.07.2018

C++ Guns: create compile time lookup table

Filed under: Allgemein — Tags: — Thomas @ 20:07

Eine lookup table LUT fest im Programm compilieren sollte mit C++ und constexpr kein Problem sein. Zur Kontrolle ob es funktioniert der erzeugte Assembler Code

Beispiel 1 nutzt std::array da es zur Compilezeit eine bekannte Länge hat und eine normale ausgeschriebene Schleife. Unschön ist, dass das Array erst deklariert und danach erst initialisiert wird. Also kein RAII. Zusätzlich muss von Hand der return Type der Funktion f bestimmt werden. Auf jeden Fall funktioniert es. Im Assembercode sind die vorraus berechneten Konstanten zu sehen, die einfach in das Array kopiert werden.

template<size_t N, typename Function>
constexpr auto makeLookUpTable(Function f) {
    using T = std::invoke_result_t<Function,size_t>;

    std::array<T,N> arr{};
    for(size_t i=0; i < N; ++i) {
        arr[i] = f(i);
    }
    return arr;
}

auto func() {
    auto f = [](size_t i){ return std::sin(i/4.0*M_PI); };
    constexpr auto arr = makeLookUpTable<4>(f);
    return arr;
}
func():
        movq    %rdi, %rax
        movq    $0x000000000, (%rdi)
        movq    .LC1(%rip), %rdx
        movq    %rdx, 8(%rdi)
        movq    .LC2(%rip), %rcx
        movq    %rcx, 16(%rdi)
        movq    .LC3(%rip), %rsi
        movq    %rsi, 24(%rdi)
        ret
.LC1:
        .long   1719614412
        .long   1072079006
.LC2:
        .long   0
        .long   1072693248
.LC3:
        .long   1719614413
        .long   1072079006

Die nächste Beispiel nutzt fold expressions und keine explizite Schleife mehr. Damit ist auch RAII wieder Möglich. Der erzeugte Assemblercode ist identisch, da ja auch das selbe gemacht wird. Nur die Syntax ändert sich ein wenig. Was gibt es noch zu sagen? Eigentlich nichts. Ich frage mich wie viel Konstanten erzeugt werden können, bevor die Binary platz.

template<size_t N, typename Function>
constexpr auto makeLookUpTable2(Function f) {
    auto impl = [&f]<std::size_t... Ints>(std::index_sequence<Ints...>) {
        return std::array{(f(Ints))...};
        };
    return impl(std::make_index_sequence<N>{}); 
}

auto func2() {
    auto f = [](size_t i){ return std::sin(i/4.0*M_PI); };
    constexpr auto arr = makeLookUpTable2<4>(f);
    return arr;
}
func2():
        movq    %rdi, %rax
        movq    $0x000000000, (%rdi)
        movq    .LC1(%rip), %rdx
        movq    %rdx, 8(%rdi)
        movq    .LC2(%rip), %rcx
        movq    %rcx, 16(%rdi)
        movq    .LC3(%rip), %rsi
        movq    %rsi, 24(%rdi)
        ret
.LC1:
        .long   1719614412
        .long   1072079006
.LC2:
        .long   0
        .long   1072693248
.LC3:
        .long   1719614413
        .long   1072079006

11.07.2018

C++ Guns: ArrayND multidimensionaler Container

Filed under: Allgemein — Tags: — Thomas @ 13:07

Programmierübung:
Array Verallgemeinerung von 1D zu 2D zu ND. Interessant wird es die Indizierung zu programmierten, da die Anzahl der Dimensionen variabel, aber zur Compilezeit bekannten ist.
Der Index für ein 2D Array berechnet sich einfach: idx1D = dimsize[0]*j + i Das Muster wiederholt sich einfach. Für 3D ist es idx = dimsize[1]*dimsize[0]*k + dimsize[0]*j + j
Der Term dimsize[1]*dimsize[0] lässt sich bei der Initialisierung des Arrays vorraus berechnen, so dass einige Multiplikationen beim Arrayzugiff gespart werden.

Mit template parameter pack sollte das alles gut funktionieren. Hier ist meine erste Version:

template<typename T, size_t dimensionen, typename ValArray=std::vector<T> >
struct ArrayND : ValArray {
    using Base = ValArray;

    using const_reference = typename Base::const_reference;

    std::array<int, dimensionen> muldimsizes;

    /// \todo replace with std::reduce as soon it is supported by GCC
    auto helper(const std::array<int, dimensionen>& dims) {
        int size = 1;
        for(size_t i=0; i < dimensionen; ++i) {
            size *= dims[i];
        }
        return size;
    }

    ArrayND(const std::array<int, dimensionen>& dims)
        : Base(helper(dims))
    {
        muldimsizes.back() = dims.back();
        for(size_t i=dimensionen-1; i > 0; --i) {
            muldimsizes[i-1] = muldimsizes[i]*dims[i-1];
        }
    }

    template<typename Int>
    auto pos2idx(const std::array<Int,dimensionen>& pos) const {
        int idx = pos.back();
        for(size_t i=0; i < dimensionen-1; ++i) {
            idx += muldimsizes[i+1]*pos[i];
        }
        return idx;
    }

    template<typename...Ints>
    const_reference operator()(const Ints&...pos) const {
        static_assert(sizeof...(Ints) == dimensionen, "Given position array must be same size as this matrix dimensions");
        // It's a fold expression
        static_assert((std::is_integral_v<Ints> and ...), "All given positions must be integral type");
        return (*this) [pos2idx(std::array{pos...})];
    }
};

So funktioniert das schonmal. Wie sieht der erzeugte Assembler Code aus?

1D std::vector

Zum Vergleich der Assembler Code bei einem normalen Zugriff über std::vector

auto func( std::vector<double> &arr, int idx) {
    return arr[idx];
}

func(std::vector<double, std::allocator<double> >&, int):
  movslq %esi, %rsi          # convert int32 to int64
  movq (%rdi), %rax
  movsd (%rax,%rsi,8), %xmm0 # xmm0 is return register of the double value
  ret

Drei mov Instruktionen. Die erste Instruktion ist eine Konvertierung von int zu size_t. Besser geht es eigentlich nicht.

1D ArrayND

Nun der 1D Fall für Array1D. Es muss im Prinzip der selbe Assembler Code erzeugt werden, wenn nicht sogar identischer! Da im Prinzip bei 1D keine Index Umrechnung statt findet.

auto func1D( ArrayND<double,1> &arr, int idx) {
    return arr(idx);
}

func1D(ArrayND<double, 1ul, std::vector<double, std::allocator<double> > >&, int):
  movslq %esi, %rsi
  movq (%rdi), %rax
  movsd (%rax,%rsi,8), %xmm0
  ret

HA! Identisch! Wenn das kein guter Start ist.

2D ArrayND

Für 2D müsste theoretisch nur eine Multiplikation und eine Addition dazu kommen.

auto func2D( ArrayND<double,2> &arr, int idx1, int idx2) {
    return arr(idx1,idx2);
}

func2D(ArrayND<double, 2ul, std::vector<double, std::allocator<double> > >&, int, int):
  imull 28(%rdi), %esi
  addl %edx, %esi
  movslq %esi, %rsi
  movq (%rdi), %rax
  movsd (%rax,%rsi,8), %xmm0
  ret

Ja, bestätigt.

4D ArrayND

Für jede weitere Dimension kommt wohl nur genau eine Multiplikation und eine Addition hinzu. Springen wir gleich zu 4D.

auto func4D( ArrayND<double,4> &arr, int idx1, int idx2, int idx3, int idx4) {
    return arr(idx1,idx2,idx3,idx4);
}

func4D(ArrayND<double, 4ul, std::vector<double, std::allocator<double> > >&, int, int, int, int):
  imull 28(%rdi), %esi
  addl %r8d, %esi
  imull 32(%rdi), %edx
  addl %esi, %edx
  imull 36(%rdi), %ecx
  addl %edx, %ecx
  movslq %ecx, %rcx
  movq (%rdi), %rax
  movsd (%rax,%rcx,8), %xmm0
  ret

Drei Kopien, drei Multiplikationen und drei Additionen.

ArrayND ready to use ;)

Powered by WordPress