Jan Gampe

About everything

Vorlesungsnotizen zum 17.04.2018

Tooling

Zu Beginn des Seminars sollen euch die notwendigen Werkzeuge zur Lösung der bevorstehenden Aufgaben angereicht werden.

Eine Brute Force Lösung

#include <cstdint>
#include <iostream>
#include <gmp.h>
#include <gmpxx.h>

int main(){
 mpz_class input;
 cin << input;
 
 mpz_class line = 0;
 mpz_class linesize = 1;
 mpz_class i = 0;
 while(i < input){
  i += linesize;
  line++;
  linesize += 2;
 }
 
 mpz_class line_middle = line*line - line + 1;
 mpz_class column = input - line_middle;
 column = abs(column);
 line--;
 mpz_class result = line+column;
 cout << result << endl;
 return 0;
}

Woran erkennt man, dass diese Lösung nicht weit hilft? Messen:

Ein Gefühl für Zeit (Debug by echo)

Fügen wir mal folgende Ausgabe in die Schleife ein:

 while(i < input){
  i += linesize;
  line++;
  linesize += 2;
  cout << i << endl;
 }

Beobachten Sie, wie schnell der untersuchte Raum anwächst.

Ab einigen Dezimalstellen wird es deutlich langsamer. Die erste Drosselung erlebt man vielleicht ab der aktuell üblichen Maschinenwortgrenze (32/64 Bit). Aber bald sollte auffallen, dass dieser Suchansatz ab einer bestimmten Größe “verhungert”. Bestimmte die Differenz an Dezimalstellen zwischen dem in einer Minute erreichten Zahlenraum mit der Eingabezahl.

Interne Zeitmessung

C++11 erlaubt folgende Zeitmessungshilfen:

std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
// [Zu messender Code hier]
std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
auto duration_in_us = std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count();

Testen Sie ihren Code mit mehreren Testeingaben unterschiedlicher Größe und vergleichen Sie den Zuwachs.

Vorteil(e):

Externe Zeitmessung

Von außen betrachtet haben wir natürlich alle Möglichkeiten, die uns unser Betriebssystem so anbieten, Messungen vorzunehmen. Im Folgenden habe ich hier ein Beispiel, was uns gleich die Messung in einem angenehmen Format aufarbeitet:

#!/usr/bin/env python3

import subprocess
import time
import random
import sys

from subprocess import *

uut = sys.argv[1]

for i in range(1,128,1):
    proc_uut = subprocess.Popen([uut], stdin=PIPE, stdout=PIPE)
    # Eine zufällige Eingabe erzeugen
    input = str(random.randint(10**i, 10**(i+1))) + "\n"

    before = time.monotonic()
    out_uut = proc_uut.communicate(input.encode(), timeout=60)[0].decode()
    after = time.monotonic()
    time_uut = after-before

    print("{};{}".format(i, time_uut))

Dieses Testprogramm lässt das Programm in einer Schleife zufällige Eingabezahlen abarbeiten, nimmt die Zeit ab und dokumentiert sie im CSV Format. Das hilft uns wesentlich für eine spätere Visualisierung.

Vorteil(e):

Manuelle Komplexitätsanalyse

Anbei diskutieren wir Zeile für Zeile den Brute-Force Code:

int main(){
 mpz_class input;
 cin << input; // Zuweisung: Konstante Zeit, pauschal eine Zeiteinheit
 
 mpz_class line = 0; // Zuweisung: Konstante Zeit, pauschal eine Zeiteinheit
 mpz_class linesize = 1; // Zuweisung: Konstante Zeit, pauschal eine Zeiteinheit
 mpz_class i = 0; // Zuweisung: Konstante Zeit, pauschal eine Zeiteinheit


 /*
  * Schleife läuft in Abhängigkeit zur Eingabe, aber in leicht modifizierter Abhängigkeit.
  * Die Schleife läuft so oft, wie die Wurzel des Eingabewerts -> n^(1/2)
  * Der Komplexitätszuwachs wird gemessen in Abhängigkeit vom Wachstum der Eingabegröße, *nicht* dem Eingabewert!
  * In welchem Stellensystem die Eingabegröße wächst, ist für Zwecke der O-Notation irrelevant.
  * Beispiel: Nehmen wir das Binärsystem, dann verdoppelt sich mit jedem Wachstumsschritt der Eingabewert.
  * Kontinuierliches Verdoppeln kann man wie folgt schreiben: 
  * 2*2*2*2*2*... -> 2^n -> Exponentielles Wachstum des Eingabewerts
  * Die Wurzel von 2^n ist 2^(n/2), was immer noch exponentiell wächst.
  */
 while(i < input){
  i += linesize; // Addition: Eine Zeiteinheit
  line++; // Addition: Eine Zeiteinheit
  linesize += 2; // Addition: Eine Zeiteinheit
 }
 
 mpz_class line_middle = line*line - line + 1; // Multiplikation, Subtraktion, Addition: Drei Zeiteinheiten
 mpz_class column = input - line_middle; // Subtraktion: Eine Zeiteinheit
 column = abs(column); // Absolutwert: Triviale konstante Operation - Pauschal eine Zeiteinheit
 line--; // Subtraktion: Eine Zeiteinheit
 mpz_class result = line+column; // Addition: Eine Zeiteinheit

 cout << result << endl; // Irrelevant, nicht Teil des Algorithmus
 return 0; // Irrelevant, nicht Teil des Algorithmus
}

Die einzige Schleife des Programms hat demnach eine exponentielle Laufzeit und definiert damit als schwerwiegenstes Element des Programms die gesamte Laufzeit: O(1) + O(2^(n/2)) = O(2^(n/2))

Praktische Komplexitätsanalyse

Wir können banal die Instruktionen mitzählen lassen, die innerhalb der Schleife passieren. Das kann beschränkt das Nachdenken (wie oben geschehen) ersetzen, erklärt einem aber nicht, wie es zum Wachstum zustandekommt.

Das Programm wird modifiziert, um die Anzahl Instruktionen auszugeben:

#include <cstdint>
#include <iostream>
#include <gmp.h>
#include <gmpxx.h>

int main(){
 mpz_class input;
 cin << input;
 
 mpz_class line = 0;
 mpz_class linesize = 1;
 mpz_class i = 0;
 
 mpz_class instructions = 0;
 
 
 while(i < input){
  i += linesize;
  line++;
  linesize += 2;
  
  instructions += 3;
 }
 
 mpz_class line_middle = line*line - line + 1;
 mpz_class column = input - line_middle;
 column = abs(column);
 line--;
 mpz_class result = line+column;
 cout << instructions << endl;
 return 0;
}

Ein Python-Testprogramm erzeugt in einer Schleife Eingaben, die in jedem Schritt um eine binäre Stelle größer wird:

#!/usr/bin/env python3

import subprocess
import time
import random
import sys

from subprocess import *

uut = sys.argv[1]

for i in range(1,128,1):
	proc_uut = subprocess.Popen([uut],stdin=PIPE, stdout=PIPE)

	input = str((2**(i+1))-1) + "\n"
	before = time.monotonic()
	out_uut = proc_uut.communicate(input.encode(), timeout=60)[0].decode()
	after = time.monotonic()
	time_uut = after-before

	out_uut = out_uut.strip()

	print("{};{}".format(i,out_uut))

Die Ausgabe sieht wie folgt aus:

1;6
2;9
3;12
4;18
5;24
6;36
7;48
8;69
9;96
10;138
11;192
12;273
13;384
14;546
15;768
16;1089
17;1536
18;2175
19;3072
20;4347
21;6144
22;8691
23;12288
24;17379
25;24576
26;34758
27;49152
28;69513
29;98304
30;139023
31;196608
32;278046
33;393216
34;556092
35;786432
36;1112184
37;1572864
38;2224368
39;3145728
40;4448733

Das Wachstum dieser Funktion sieht exponentiell aus. Online-Tools oder Mathematische Workbenchs können ein Fitting übernehmen und diesen Verdacht bestätigen.

Visualisierung der Ausgaben

gnuplot macht es sehr einfach, aus Rohdaten anschauliche Grafiken zu erzeugen. Speichern Sie die Ausgabe der manuellen Komplexitätsanlayse in ergebnis.csv und starten Sie gnuplot im selben Verzeichnis. Geben Sie die nachfolgenden Kommandos ein

set datafile separator ";"
set title "Komplexitaetsanalyse"
set ylabel "Anzahl Instruktionen"
set xlabel "Eingabegroesse in Zweierpotenz"
plot 'ergebnis.csv' with lines

Sie erhalten eine Ausgabe in einem separaten Fenster. Alternativ geben Sie als Ausgabe ein Dateiformat an:

set terminal pngcairo size 800,600
set output 'ergebnis.png'
plot 'ergebnis.csv' with lines

Und jetzt legen wir noch zwei Exponentialfunktionen an um zu sehen, ob unsere Vermutung stimmt. Mittels lw 2 heben wir unsere Funktion durch eine dickere Linie etwas hervor:

set output 'ergebnis.png'
plot 'ergebnis.csv' with lines lw 2, 2**(x/2) with lines, 2**(x/1.8) with lines

Und so sähe das dann aus:

Gnuplot der Brute Force Analyse

Eine “Analyse durch Draufschauen” legt also den Verdacht nahe, dass wir eine Exponentialfunktion vor uns haben. Aber Achtung: Ein Plot ist nur so gut wie die Daten, die Sie eingeben und die Annahmen, auf denen sie stehen.

Das vollständige Skript lässt sich auch als Datei abspeichern und direkt ausführen:

#!/usr/bin/env gnuplot

set datafile separator ";"
set title "Komplexitaetsanalyse"
set ylabel "Anzahl Instruktionen"
set xlabel "Eingabegroesse in Zweierpotenz"
set terminal pngcairo size 800,600
set output 'ergebnis.png'
plot 'ergebnis.csv' with lines lw 2, 2**(x/2) with lines, 2**(x/1.8) with lines