C++Guns – RoboBlog blogging the bot

08.12.2018

C++ Guns: Level of Indirection ("dereferencing")

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

Die Performance ist ein Opfer von Level of Indirection. Jetzt auch bei mir. Ich will keinen langweiligen Code zeigen wie so etwas aussieht, oder wie man es verhindern kann. Viel interessanter ist es doch, wie das aus Assembler Ebene aussieht.
Dazu zwei Assembler Auszüge. Der erste mit zwei zusätzlichen Indirektionen. Es wird auf das i-th Element eines std::vector zugegriffen und zurück gegeben.

func(std::vector<T> const&, int):
        movq    %rdi, %rax       }
        movslq  %edx, %rdx       |
        salq    $4, %rdx         | Addressberechnung
        addq    (%rsi), %rdx     }
        movq    (%rdx), %rdx     * Indirektion 
        movq    (%rdx), %rdx     * Indirektion
        movdqu  (%rdx), %xmm0    }
        movups  %xmm0, (%rdi)    |
        movdqu  16(%rdx), %xmm1  | Kopieren der 4 double Variablen
        movups  %xmm1, 16(%rdi)  }
        ret

Ohne zweimalige Indirektion:

func2(std::vector<T> const&, int):
        movq    %rdi, %rax        }
        movslq  %edx, %rdx        |
        salq    $5, %rdx          | Addressberechnung
        addq    (%rsi), %rdx      }
        movdqu  (%rdx), %xmm0     }
        movups  %xmm0, (%rdi)     |
        movdqu  16(%rdx), %xmm1   | Kopieren der 4 double Variablen
        movups  %xmm1, 16(%rdi)   }
        ret

Es sind zwei Dinge zu erkennen.
Erstens gibt es zwei zusätzliche mov Anweisungen, welche eine Dereferenzierung eines Pointers bewirken. Die zusätzliche Arbeit den Befehl zu bearbeiten ist nicht das Problem. Vielmehr, dass die Daten nicht mehr hintereinander im RAM stehen. Es wird im RAM umher gesprungen. Dies geht bekanntlich auf die Performance, Cache misses u.s.w.

Bei dem konkreten Problem, aus dem dieses Beispiel hervorgegangen ist, lief das Programm nach dem Entfernen der zwei Indirektionen 10 mal schneller!

Zweitens ist zu erkennen, dass die zurück gegebenen Variablen aus 4 double Werten besteht, aber nur zwei Kopieraktionen zu erkennen sind. Von dem Register rdx, nach xmm0, nach rdi. Und noch einmal mit einem Offset von 16Byte (2 double Werte). Der Compiler hat erkannt, dass SIMD angewendet werden kann. Das Kopieren von zwei Werten mit einer Instruktion. Und das ohne zusätzlichen Bemühungen meinerseits C++ Code extra SIMD tauglich zu schreiben!

30.10.2018

C++ Guns: disable function with concepts without SFINAE std::enable_if

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

std::enable_if is dead; long live concepts!

Man beachte, dass die Requires clause NACH dem Funktionskopf kommen kann.

#include <type_traits>

template<typename T>
void func(T) requires std::is_integral_v<T> {
}

auto func2() {
    // error: cannot call function 'void func(T) requires  is_integral_v<T> [with T = double]'
    // constraints not satisfied  'is_integral_v<T>' evaluated to false
   func(2.0); // Nope
}

12.10.2018

C++ Guns: -Ofast in action

Filed under: Allgemein — Tags: — Thomas @ 10:10

Mit -Ofast wird bei folgenden Code die Power Funktion weg optimiert und durch die dritte Wurzel ersetzt.

auto func(double MANNING2, double qNorm, double H ) {
    return MANNING2 * qNorm / (pow(H,(7.0/3.0)));
}

Im Assembler Code ist nur noch cbrt zu erkennen, kein pow mehr.

func(double, double, double):
        subq    $40, %rsp
        movsd   %xmm0, 8(%rsp)    # MANNING2
        movapd  %xmm2, %xmm0      # H nach xmm0 für cbrt
        movsd   %xmm1, 24(%rsp)   # qNorm
        movsd   %xmm2, 16(%rsp)   # H
        call    cbrt              # cbrt(H) nach xmm0
        movsd   16(%rsp), %xmm2   # xmm2 = H
        movsd   24(%rsp), %xmm1   # xmm1 = qNorm
        mulsd   8(%rsp), %xmm1    # xmm1 = MANNING2*qNorm
        addq    $40, %rsp 
        mulsd   %xmm2, %xmm2      # xmm2 = H*H
        mulsd   %xmm0, %xmm2      # xmm2 = H*H*cbrt(H)
        divsd   %xmm2, %xmm1      # xmm1 = MANNING2*qNorm /(H*H*cbrt(H))
        movapd  %xmm1, %xmm0
        ret

Der Assembler Code zurück nach C++ übersetzt gibt folgenden:

auto func(double MANNING2, double qNorm, double H ) {
    return MANNING2 * qNorm / (H*H*cbrt(H));
}

Die pow Funktion mit zwar bekannten, aber "unschönen" Exponenten wurde ersetzt durch eine Multiplikation und eine Power Funktion mit bekannte und "schönen" Exponenten: 1/3

Aber es könnte besser sein. Es fällt auf, dass die Funktionsargumente erst einmal auf den Stack gepusht werden. Offenbar braucht die cbrt Funktion die Register xmm0, xmm1, xmm2 selbst. Und so müssen die Werte eben gesichert werden. Da sie erst danach gebraucht werden. ...
Natürlich kann man MANNING2*qNorm berechnen bevor vor Nenner der Formel fertig berechnet ist. Mathematisch kann man das tun, die Klammern kann man setzten.

Aber den Compiler stört das nicht. Es wird immer erst cbrt vor MANNING2*qNorm. Ich denke cbrt braucht einfach mehr Zeit als eine Multiplikation. Und daher wird dieser Befehl als erstes ausgeführt. Und zwischenzeitlich können davon unabhängige Variablen bearbeitet werden.

Aber eine Sache kann dennoch optimiert werden. Indem H als ersten Parameter übergeben wird. Dann liegt es gleich im xmm0 Register für cbrt bereit und muss nicht erst dorthin kopiert werden. An der Anzahl der Instruktionen ändert dies nichts. Aber die mov Operation wird auch erst nach cbrt ausgeführt.

27.09.2018

C++ Guns: Are floating point numbers sortable?

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

Sind Gleitkommazahlen sortierbar? Wegen NaN gibt es keine Ordnung und das Sortieren mit NaN im Datensatz schlägt fehlt.
Aber man kann sich schnell selbst eine Ordnung bauen:

#include <vector>
#include <algorithm>
#include <iostream>
#include <limits>
#include <cmath>

using namespace std;
using nl = numeric_limits<double>;

int main() {
  cout << boolalpha;

  vector<double> v { 1.0, 2.0, 0.0, 1.0/3.0, -0.0,
                    nl::quiet_NaN(), nl::infinity(), nl::epsilon(), nl::denorm_min(),
                    nl::max(), nl::min(), nl::lowest()
  };

  sort(v.begin(), v.end());

  for(const auto x : v) {
    cout << x << " ";
  }
  cout << "\nis sorted " << is_sorted(v.begin(), v.end()) << " << offensichtlich falsch\n";

  auto comp = [](const auto lhs, const auto rhs) {
    if(isnan(lhs)) return false;
    if(isnan(rhs)) return true;
    return lhs<rhs;
  };

  sort(v.begin(), v.end(), comp);

  for(const auto x : v) {
    cout << x << " ";
  }
  cout << "\nis sorted " << is_sorted(v.begin(), v.end()) << "\n";
}
-1.79769e+308 0 -0 0.333333 1 2 nan 4.94066e-324 2.22507e-308 2.22045e-16 1.79769e+308 inf 
is sorted true << offensichtlich falsch
-1.79769e+308 0 -0 4.94066e-324 2.22507e-308 2.22045e-16 0.333333 1 2 1.79769e+308 inf nan 
is sorted true << richtig

13.09.2018

FORTRAN: GDB conditional watchpoint

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

To set a conditional watchpoint on local variable i.
Example Code:

subroutine func
  integer :: i

  do i=1, 100
    write(*,*) i
  end do
end subroutine

program test
  implicit none

  call func()
end program

Compile with -ggdb
Set a breakpoint on subroutine "func". After the debugger stop on this point, the local variable "i" is in scope so one can set a watchpoint.

$ gdb ./example
(gdb) break func
Breakpoint 1 at 0x8bb: file gdb.F90, line 4.

ODER
(gdb) break example.F90:4

(gdb) run
Starting program: /home/kater/example
Breakpoint 1, func () at example.F90:4
4 do i=1, 100
(gdb) watch i if i.gt.10
Hardware watchpoint 2: i
(gdb) c
continuing.
1
2
3
4
5
6
7
8
9
10

Hardware watchpoint 2: i

Old value = 10
New value = 11
0x0000555555554948 in func () at gdb.F90:4
4 do i=1, 100

30.08.2018

FORTRAN: 3x3 matmul intrinsic vs. hand crafted

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

Verglichen wird der erzeugte Assembler Code der internen Fortran matmul() Routine mit einer von Hand geschriebenen Matrix Multiplikation.
Getestet wird für 3x3 Matrizen mit zur Compilezeit bekannter Größe. Sonst könnte man den vom FORTRAN Compiler erzeugten Assembler Code gar nicht mehr nachvollziehen.

subroutine intrinsicmatmul(a,b,c) 
  implicit none
  real(8), intent(in) :: a(3,3), b(3,3)
  real(8), intent(inout) :: c(3,3)
  
  c = matmul(a,b)
end subroutine

Und hier von Hand

subroutine handmatmul(a,b,c)
  implicit none
  real(8), intent(in) :: a(3,3), b(3,3)
  real(8), intent(inout) :: c(3,3)    
  integer :: i,j,k
  
  c(:,:) = 0
  do i=1, 3
    do j=1, 3
      do k=1, 3
        c(j,i) = c(j,i) + a(j,k) * b(k,i)
      end do
    end do
  end do
end subroutine

Getestet wird mit gfortran 7.3.0 -O1. Die Hand Version hat 13 Instruktionen mehr. Hiervon sind 6 Instruktionen für das Sichern und Wiederherstellen von Register. Der Rest geht wohl auf eine andere Art Adressberechnung drauf. Da die intrinsic matmul Routine selbst bei -O1 geinlint wird, ich sehe kein Funktionsaufruf im Assembler Code, wird sie wohl schneller sein.
Tests mit Praxiscode zeigen aber, dass das nicht so sein muss. Es wurden mehrere Matrizen multipliziert.

Es gilt: messen, messen messen. Bei dem nächsten Compiler/CPU kann es wieder anders sein.

intrinsic

intrinsicmatmul_:
	movq	$0, (%rdx)  
	movq	$0, 8(%rdx) 
	movq	$0, 16(%rdx)
	movq	$0, 24(%rdx)
	movq	$0, 32(%rdx)
	movq	$0, 40(%rdx)
	movq	$0, 48(%rdx)
	movq	$0, 56(%rdx)
	movq	$0, 64(%rdx)
	leaq	24(%rdx), %rcx
	addq	$24, %rsi
	leaq	96(%rdx), %r9
	leaq	96(%rdi), %r10
.L4:
	leaq	24(%rdi), %rdx
	movq	%rsi, %r8
.L3:
	movsd	-24(%r8), %xmm1
	movl	$0, %eax
.L2:
	addq	$1, %rax
	movapd	%xmm1, %xmm0
	mulsd	-32(%rdx,%rax,8), %xmm0
	addsd	-32(%rcx,%rax,8), %xmm0
	movsd	%xmm0, -32(%rcx,%rax,8)
	cmpq	$3, %rax
	jne	.L2
	addq	$8, %r8
	addq	$24, %rdx
	cmpq	%r10, %rdx
	jne	.L3
	addq	$24, %rcx
	addq	$24, %rsi
	cmpq	%r9, %rcx
	jne	.L4
	rep ret
Hand

handmatmul_:
	pushq	%r12               # -
	pushq	%rbp               # |- Register r12, rbp, rbx für Laufvariablen i,j,k freimachen
	pushq	%rbx               # -
	movq	$0, (%rdx)         # -
	movq	$0, 8(%rdx)        # |
	movq	$0, 16(%rdx)       # |
	movq	$0, 24(%rdx)       # |
	movq	$0, 32(%rdx)       # |- c(:,:) = 0
	movq	$0, 40(%rdx)       # |
	movq	$0, 48(%rdx)       # |
	movq	$0, 56(%rdx)       # |
	movq	$0, 64(%rdx)       # -
	leaq	24(%rsi), %r9
	leaq	96(%rsi), %r12
	movq	%rdx, %rcx
	subq	%rsi, %rcx
	leaq	-24(%rcx), %rbp
	movq	%rcx, %rbx
.L4:                               # i Schleife Anfang
    leaq	0(%rbp,%r9), %r8
	movq	%rdi, %r10
	leaq	(%rbx,%r9), %rdx
.L3:                               # j Schleife Anfang
	movq	%r8, %r11 
	movsd	(%r8), %xmm1       # c(i,j) nach xmm1 laden
	movq	%rsi, %rax
	movq	%r10, %rcx
.L2:                               # k Schleife Anfang
	movsd	(%rcx), %xmm0      # -
	mulsd	(%rax), %xmm0      # |- c(j,i) + a(j,k) * b(k,i)
	addsd	%xmm0, %xmm1       # -
	addq	$24, %rcx          # 3x double = 24Byte
	addq	$8, %rax           # 1x double = 8byte. 
	cmpq	%r9, %rax          # rax wird gleichzeitig als Pointer und Laufvariable benutzt.
                               # In r9 steht der Wert den rax erreicht, wenn die Schleife terminiert
	jne	.L2                # k Schleife Ende
	movsd	%xmm1, (%r11)      # c(j,i) = xmm1 
	addq	$8, %r8
	addq	$8, %r10
	cmpq	%rdx, %r8
	jne	.L3                # j Schleife Ende
	addq	$24, %rsi
	addq	$24, %r9
	cmpq	%r12, %r9
	jne	.L4                # i Schleife Ende
	popq	%rbx               # -
	popq	%rbp               # |- Register aus dem Stack wieder herstellen
	popq	%r12               # -
	ret 

Für Optimierung O2 gilt 35 Instruktionen für intrinsic und 43 für Hand.

23.08.2018

C++ Guns: Solve std::forward_list no size() method problem

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

Why has forward_list no size() method?

In short: Its about performance. A single linked list size() function has a asymptotic complexity of O(n). We do not usually need something like that. So we don't have something.
The second reason is: For a O(1) size() function, it would increase the size of std::forward_list by 8 byte. We don't pay for what we don't need.

N2543 is the proposal, and it has a detailed discussion about size() [1]

The choice between Option 3 [not providing size()] and Option 2 [providing a O(1) size()] is more a matter of judgment. I have chosen Option 3 for the same reason that I chose insert-after instead of insert-before: Option 3 is more consistent with the goal of zero overhead compared to a hand-written C-style linked list. Maintaining a count doubles the size of a forward_list object (one word for the list head and one for the count), and it slows down every operation that changes the number of nodes. In most cases this isn't a change in asymptotic complexity (the one change in asymptotic complexity is in one of the forms of splice), but it is nonzero overhead. It's a cost that all users would have to pay for, whether they need this feature or not, and, for users who care about maintaining a count, it's just as easy to maintain it outside the list, by incrementing the count with every insert and decrementing it with every erase, as it is to maintain the count within the list.

But there is a very simple solution for this problem: A non-member size() function for std::forward_list as they are introduced in C++17.

namespace std {
    template<typename T>
    size_t size(const std::forward_list<T>& list) {
        size_t i=0;
        for(const auto& x : list) {
            (void)x;
            i++;
        }
        return i;
    }
}

But there is one problem: The std::size() now take O(1) or O(n) time, depending on the passed container.

[1] https://stackoverflow.com/questions/31822494/c-stl-why-has-forward-list-no-size-method

19.08.2018

C++ Guns: Are lists evil?

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

The problem seems to be an interesting little exercise that John Bentley once proposed to me: Insert a sequence of random integers into a sorted sequence, then remove those elements one by one as determined by a random sequence of positions: Do you use a vector (a contiguously allocated sequence of elements) or a linked list?

http://www.stroustrup.com/bs_faq.html

* Generate N random integers and insert them into a sequence so that each is inserted in its proper position in the numerical order. 5 1 4 2 gives:
- 5
- 1 5
- 1 4 5
- 1 2 3 4
* Remove elements one at a time by picking a random position in the sequence and removing the element there. Positions 1 2 0 0 gives
- 1 2 4 5
- 1 4 5
- 1 4
- 4
* For which N is it better to use a linked list than a vector (or an array) to represent the sequence?

GoingNative 2012 Bjarne Stroustrup: C++11 Style

Challenge accepted. To be continued...

15.08.2018

C++ Guns: why C++ ist better than C. Or: keyword restrict is useless

Filed under: Allgemein — Tags: — Thomas @ 19:08

Das restricted keyword in C

restrict keyword...is basically a promise to the compiler that for the scope of the pointer, the target of the pointer will only be accessed through that pointer (and pointers copied from it).

In C++ geben wir keine Versprechen. Grundsätzlich nicht.
Das Beispiel von cppreference.com verdeutlich das Optimierungspotential ziemlich anschaulich. Gegeben ist folgende Funktion:

int foo(int &a, int &b) {
    a = 5;
    b = 6;
    return a + b;
}
foo(int&, int&):
        movl    $5, (%rdi)     # store 5 in a
        movl    $6, (%rsi)     # store 6 in b
        movl    (%rdi), %eax   # read back from a in case previous store modified it
        addl    $6, %eax       # add 6 to the value read from a
        ret

Welche Optimierungsmöglichkeiten gibt es hier? Natürlich, der Rückgabewert ist 11 (5+6), solange die Referenzen von a und b auf unterschiedliche Variablen zeigen. Sonst ist der Rückgabewert 12 (6+6). Mit dem restrict keyword kann man lügen und behaupten, dass die Referenzen von a und b nicht auf die selbe Variablen zeigen. Aber so etwas haben wir in C++ nicht nötigt; der Compiler weiss es besser als wir.
Je nachdem in welchen Kontext die Funktion foo aufgerufen wird, kann optimiert werden, oder auch nicht. Beispielsweise ist in Funktion func zur Compilezeit sichergestellt, dass die Referenzen auf unterschiedliche Variablen zeigen. Entsprechend wird der Rückgabewert von 11 voraus berechnet und im Code abgelegt.

auto func(std::vector<int>& arr, size_t i) {
    return foo(arr[i], arr[i+1]);
}
func(std::vector<int, std::allocator<int> >&, unsigned long):
        movq    (%rdi), %rax
        addq    $1, %rsi             # temp increment i
        movl    $5, -4(%rax,%rsi,4)  # store 5 in a
        movl    $6, (%rax,%rsi,4)    # store 6 in b
        movl    $11, %eax            # the result is 11, a compile-time constant
        ret

Zeigen die Referenzen hingegen auf die selbe Variable, wie in der Funktion func2, erkennt auch dies der Compiler und berechnet einen anderen Rückgabewert: 12. Auch in diesem Fall muss die Addition zur Laufzeit nicht ausgeführt werden.

auto func2(std::vector<int>& arr, size_t i) {
    return foo(arr[i], arr[i]);
}
func2(std::vector<int, std::allocator<int> >&, unsigned long):
        movq    (%rdi), %rax
        movl    $6, (%rax,%rsi,4)  # store 6 in a and b.
        movl    $12, %eax          # the result is 12, a compile-time constant
        ret

Fazit: Hinweise an den Compiler mit dem restrict keyword in C sind in C++ absolut überflüssig und führen nur zu undefinierten Verhalten des Programms und damit zu schwer findbaren Bugs. Da die Debugzeit 1/3 bis gerne 2/3 der gesamten Entwicklungszeit ausmachen (in der Praxis, nicht in der Theorie), ist der Einsatz von C statt C++ unwirtschaftlich.

[1] https://en.cppreference.com/w/c/language/restrict
[2] https://stackoverflow.com/questions/776283/what-does-the-restrict-keyword-mean-in-c

05.08.2018

C++ Guns: semantics is important

Filed under: Allgemein — Tags: — Thomas @ 19:08

Einfach N Zufallszahlen ziehen ist einfach. Eigentlich gibt es darüber nicht viel zu sagen, aber es ist ein schönes Beispiel für Semantik. (Dank an Ben für die Inspiration).

Wir müssen immer den Spagat zwischen Lesbarkeit und Performance machen. Manchmal sind die Verantwortlichkeiten der Funktionen nicht klar zu definieren. Soll die Funktion, welche N Zufallszahlen zieht, auch die Container erstellen in denen sie gespeichert werden? Das wäre für die Lesbarkeit förderlich, aber für die Performance schlecht. Da bei jedem Aufruf ein neuer Container initialisiert werden muss. Und wenn die Funktion einen bereits initialisierten Container übergeben bekommt und auch auf diesen Weg die Daten zurück gegeben werden, wäre der Lesefluss gestört. Vor allem muss beim ersten Aufruf von Hand ein neuer Container initialisiert werden.

Für beide Varianten gilt: Keine von Hand geschriebenen Schleifen. Sie enthalten einfach unnötige potentielle Fehler. Ein einfacher std::for_each Aufruf ist besser.
Hier mein Versuch beide Welten abzudecken. Durch die fehlenden Concepts in C++17 bläht sich der Template Code etwas auf. Aber dieses Problem löst sich ja von alleine.

// 7 loc to just call for_each... semantics is important
template<typename Container, typename PRNG, typename Distribution>
auto drawSamples(Container& data, PRNG& r, Distribution& U) {
    static_assert(std::is_arithmetic_v<typename Container::value_type>);
    static_assert(std::is_invocable_v<PRNG>, "PRNG is not invocable");
    static_assert(std::is_invocable_v<Distribution, PRNG&>, "Distribution is not invocable with PRNG");
    static_assert(std::is_same_v<typename Container::value_type, typename Distribution::result_type >, "Return type of the distribution and value type of container are not the same");

    std::for_each(std::begin(data), std::end(data), [&](auto& x){x = U(r);} );
    return data; // good idea? yes. consistent with the other drawSamples overload
                 // NO! bad feeling. pass by reference and return by value - it must be a copy somewhere. even with return optimazion.
}

template<typename PRNG, typename Distribution>
auto drawSamples(int N, PRNG& r, Distribution& U) {
    static_assert(std::is_invocable_v<PRNG>, "PRNG is not invocable");
    static_assert(std::is_invocable_v<Distribution, PRNG&>, "Distribution is not invocable with PRNG");    

    std::vector<typename Distribution::result_type> data(N);
    drawSamples(data, r, U);
    return data;
}


auto func() {
    std::minstd_rand r;
    std::uniform_int_distribution<int> U(1, 6);
    std::vector data = drawSamples(10, r, U);
    data = drawSamples(data, r, U);
    return data;
}

Bin gespannt ob sich dies in der Praxis bewährt.

update:
Ich glaube pass by reference und return by value ist immer eine Kopie. Und damit ineffizient. Selbst mit return Optimierung. Bin mir aber nicht sicher.
Vielleicht sieht das ganze mit Iteratoren und Ranges besser aus.

« Newer PostsOlder Posts »

Powered by WordPress