added benchmark
- fixed minor stuff in ausarbeitung - added garba-collector-benchmark (with graphs!)
79
doc/ausarbeitung/benchmark_raw.txt
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
sdressel@Behemoth#51:~/hgraph$ ./hgraph bmark1haskell.N4000E80000adj bmark1haskell.N4000E80000adj.atr sampledata.p +RTS -N4 > graphs && tail -n 2 graphs
|
||||||
|
read/parse CPU time: 1.301769s
|
||||||
|
calculation CPU time: 33.257483s
|
||||||
|
|
||||||
|
Speedup: 3,172 (Calculation) bzw. 3,084 (Total)
|
||||||
|
|
||||||
|
sdressel@Behemoth#52:~/hgraph$ ./hgraph bmark1haskell.N4000E80000adj bmark1haskell.N4000E80000adj.atr sampledata.p +RTS -N3 > graphs && tail -n 2 graphs
|
||||||
|
read/parse CPU time: 1.290025s
|
||||||
|
calculation CPU time: 39.151967s
|
||||||
|
|
||||||
|
Speedup: 2,694 (Calculation) bzw. 2,636 (Total)
|
||||||
|
|
||||||
|
sdressel@Behemoth#53:~/hgraph$ ./hgraph bmark1haskell.N4000E80000adj bmark1haskell.N4000E80000adj.atr sampledata.p +RTS -N2 > graphs && tail -n 2 graphs
|
||||||
|
read/parse CPU time: 1.237298s
|
||||||
|
calculation CPU time: 55.320123s
|
||||||
|
|
||||||
|
Speedup: 1.906 (Calculation) bzw. 1,885 (Total)
|
||||||
|
|
||||||
|
sdressel@Behemoth#54:~/hgraph$ ./hgraph bmark1haskell.N4000E80000adj bmark1haskell.N4000E80000adj.atr sampledata.p +RTS -N1 > graphs && tail -n 2 graphs
|
||||||
|
read/parse CPU time: 1.116368s
|
||||||
|
calculation CPU time: 105.474741s
|
||||||
|
|
||||||
|
Speedup: 1.00 (Referenz)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
sdressel@Behemoth#55:~/hgraph$ ./hgraph N2000E19990.adj N2000E19990.adj.atr sampledata.p +RTS -N1 > graphs && tail -n 2 graphs
|
||||||
|
read/parse CPU time: 0.231243s
|
||||||
|
calculation CPU time: 11.984707s
|
||||||
|
|
||||||
|
Speedup: 1.00 (Referenz)
|
||||||
|
|
||||||
|
sdressel@Behemoth#56:~/hgraph$ ./hgraph N2000E19990.adj N2000E19990.adj.atr sampledata.p +RTS -N2 > graphs && tail -n 2 graphs
|
||||||
|
read/parse CPU time: 0.252726s
|
||||||
|
calculation CPU time: 6.183972s
|
||||||
|
|
||||||
|
Speedup: 1,938 (Calculation) bzw. 1,898 (Total)
|
||||||
|
|
||||||
|
sdressel@Behemoth#57:~/hgraph$ ./hgraph N2000E19990.adj N2000E19990.adj.atr sampledata.p +RTS -N3 > graphs && tail -n 2 graphs
|
||||||
|
read/parse CPU time: 0.258124s
|
||||||
|
calculation CPU time: 4.358235s
|
||||||
|
|
||||||
|
Speedup: 2,750 (Calculation) bzw. 2,646 (Total)
|
||||||
|
|
||||||
|
sdressel@Behemoth#58:~/hgraph$ ./hgraph N2000E19990.adj N2000E19990.adj.atr sampledata.p +RTS -N4 > graphs && tail -n 2 graphs
|
||||||
|
read/parse CPU time: 0.263308s
|
||||||
|
calculation CPU time: 3.67729s
|
||||||
|
|
||||||
|
Speedup: 3,259 (Calculation) bzw. 3,100 (Total)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Best settings for Running time:
|
||||||
|
32.95s: +RTS -A524288 -H16777216
|
||||||
|
33.26s: +RTS -A524288 -H1048576
|
||||||
|
33.28s: +RTS -A524288 -H2097152
|
||||||
|
33.63s: +RTS -A524288 -H4194304
|
||||||
|
33.83s: +RTS -A524288 -H8388608
|
||||||
|
Output written to : /tmp/hgraph-time-gc-space.png
|
||||||
|
Best settings for Peak memory:
|
||||||
|
145.00MB: +RTS -A131072 -H1048576
|
||||||
|
145.00MB: +RTS -A131072 -H4194304
|
||||||
|
145.00MB: +RTS -A131072 -H8388608
|
||||||
|
145.00MB: +RTS -A131072 -H16777216
|
||||||
|
145.00MB: +RTS -A131072 -H33554432
|
||||||
|
Output written to : /tmp/hgraph-peak-gc-space.png
|
||||||
|
Best settings for Resident memory:
|
||||||
|
47.38MB: +RTS -A32768 -H134217728
|
||||||
|
47.41MB: +RTS -A32768 -H268435456
|
||||||
|
47.64MB: +RTS -A32768 -H536870912
|
||||||
|
48.32MB: +RTS -A32768 -H1073741824
|
||||||
|
48.32MB: +RTS -A16384 -H134217728
|
||||||
|
Output written to : /tmp/hgraph-residency-gc-space.png
|
||||||
|
Best settings for Residency*Time:
|
||||||
|
1698.14MBs: +RTS -A524288 -H16777216
|
||||||
|
1710.71MBs: +RTS -A524288 -H2097152
|
||||||
|
1712.76MBs: +RTS -A524288 -H1048576
|
||||||
|
1729.58MBs: +RTS -A524288 -H4194304
|
||||||
|
1739.02MBs: +RTS -A524288 -H8388608
|
@ -117,6 +117,7 @@ Im Rahmen dieses Programmierprojekts wurde ein Programm entworfen und entwickelt
|
|||||||
Die Suche nach DCB ist ein NP-schweres Problem. Da mit einem geeigneten Algorithmus jedoch voneinander unabhängige Lösungspfade einzeln verfolgt werden können, ist das Problem gut für eine parallele Berechnung geeignet, wodurch die Gesamtlaufzeit stark reduziert werden kann. Zudem sind real verwendete biologische Netze üblicherweise nur schwach vernetzt. Daher können durch die Forderung nach einer hohen Konnektivität der DCB viele Lösungskandidaten schnell ausgeschlossen werden und die schlimmstenfalls nichtdeterministisch polynomielle Laufzeit findet kaum Anwendung. \par
|
Die Suche nach DCB ist ein NP-schweres Problem. Da mit einem geeigneten Algorithmus jedoch voneinander unabhängige Lösungspfade einzeln verfolgt werden können, ist das Problem gut für eine parallele Berechnung geeignet, wodurch die Gesamtlaufzeit stark reduziert werden kann. Zudem sind real verwendete biologische Netze üblicherweise nur schwach vernetzt. Daher können durch die Forderung nach einer hohen Konnektivität der DCB viele Lösungskandidaten schnell ausgeschlossen werden und die schlimmstenfalls nichtdeterministisch polynomielle Laufzeit findet kaum Anwendung. \par
|
||||||
|
|
||||||
%TODO ein bisschen biologische Motivation?
|
%TODO ein bisschen biologische Motivation?
|
||||||
|
% ... Sind wir Biologen? :p
|
||||||
|
|
||||||
\en{
|
\en{
|
||||||
\subsection{Densely Connected Biclusters}
|
\subsection{Densely Connected Biclusters}
|
||||||
@ -138,10 +139,11 @@ Ein DCB $D_k = (V_k, E_k)$ ist ein Teilgraph von $G$, der durch die Parameter $\
|
|||||||
|
|
||||||
Die Wahl der Programmiersprache zur Verwirklichung des Projekts beeinflusst stark die Methoden der Programmierung und die Art der Parallelisierung. Klassischerweise werden für sequentielle und parallele Programme gleichermaßen imperative Sprachen wie C(++), Fortran oder auch Java verwendet, wofür Erweiterungen zur parallelen Programmierung existieren oder einige Werkzeuge direkt in die Sprache eingebaut sind. Bekannte Ansätze hierfür sind MPI und openMP. \par
|
Die Wahl der Programmiersprache zur Verwirklichung des Projekts beeinflusst stark die Methoden der Programmierung und die Art der Parallelisierung. Klassischerweise werden für sequentielle und parallele Programme gleichermaßen imperative Sprachen wie C(++), Fortran oder auch Java verwendet, wofür Erweiterungen zur parallelen Programmierung existieren oder einige Werkzeuge direkt in die Sprache eingebaut sind. Bekannte Ansätze hierfür sind MPI und openMP. \par
|
||||||
Unser Projekt geht in eine etwas andere Richtung. Bei imperativer Programmierung muss ein großes Augenmerk auf die Vermeidung unerwünschter wechselseitiger Beeinflussungen verschiedener Threads und Prozesse gelegt werden, die fehlerhafte Rechenergebnisse zur Folge haben. Außerdem muss bei der Thread-/Prozesskommunikation immer die Gefahr von Verklemmungen beachtet werden, die schlimmstenfalls zu einem kompletten Stillstand der Programmausführung führen. Beide Probleme sind schwierig zu detektieren und zu lokalisieren.\par
|
Unser Projekt geht in eine etwas andere Richtung. Bei imperativer Programmierung muss ein großes Augenmerk auf die Vermeidung unerwünschter wechselseitiger Beeinflussungen verschiedener Threads und Prozesse gelegt werden, die fehlerhafte Rechenergebnisse zur Folge haben. Außerdem muss bei der Thread-/Prozesskommunikation immer die Gefahr von Verklemmungen beachtet werden, die schlimmstenfalls zu einem kompletten Stillstand der Programmausführung führen. Beide Probleme sind schwierig zu detektieren und zu lokalisieren.\par
|
||||||
Die genannten klassischen Probleme des Parallelrechnens können mit pur funktionaler Programmierung gut vermieden werden. Nebenwirkungen treten in pur funktionalem Programmcode (einen korrekten Compiler/Interpreter vorausgesetzt) garantiert nicht auf. Da das DCB-Problem bis auf das Einlesen der Eingabedaten und die Ausgabe pur funktional realisierbar ist, ist es optimal für eine derartige Implementierung geeignet Die konkrete Wahl der funktionalen Programmiersprache fiel auf \emph{Haskell}. \par
|
Die genannten klassischen Probleme des Parallelrechnens können mit pur funktionaler Programmierung gut vermieden werden. Nebenbedingungen treten in pur funktionalem Programmcode (einen korrekten Compiler/Interpreter vorausgesetzt) garantiert nicht auf. Da das DCB-Problem bis auf das Einlesen der Eingabedaten und die Ausgabe pur funktional realisierbar ist, ist es optimal für eine derartige Implementierung geeignet Die konkrete Wahl der funktionalen Programmiersprache fiel auf \emph{Haskell}. \par
|
||||||
\medskip
|
\medskip
|
||||||
Für Haskell wurden Bibliotheken entwickelt, die eine einfache und effiziente Programmierung paralleler Programme erlauben. Wir verwenden das Paket \emph{parallel} in Verbindung mit \emph{repa}-Arrays. Durch \emph{parallel} können geeignete Datenstrukturen automatisch in kleine Teilprobleme, auch \emph{Sparks} genannt, aufgeteilt und von freien Threads abgearbeitet werden. Die \emph{repa}-Arrays bieten Funktionen, um die einzelnen Arrayelemente parallel zu berechnen. Damit lässt sich sequentieller Programmcode einfach parallelisieren, da hierfür nur wenige Änderungen erforderlich sind. Es müssen lediglich die Berechnungsfunktionen an die parallelisierende Funktion übergeben und die Funktion zur Auswertung der Arrayelemente ausgetauscht werden. \par
|
Für Haskell wurden Bibliotheken entwickelt, die eine einfache und effiziente Programmierung paralleler Programme erlauben. Wir verwenden das Paket \emph{parallel} in Verbindung mit \emph{repa}-Arrays. Durch \emph{parallel} können geeignete Algorithmen mit wenig Aufwand, aufgeteilt werden. Diese Funktionsaufrufe werden unevaluiert in ein Array gepackt und dort von freien Threads abgearbeitet. Diese Technik nennt man Work-Stealing und die noch nicht ausgewerteten Funktionen werden in Haskell \emph{Sparks} genannt. Man kann sich dies als einen auf den Funktionsaufruf beschränkten light-weight Thread vorstellen - mit weniger Overhead. Die \emph{repa}-Arrays bieten Funktionen, um die einzelnen Arrayelemente parallel zu berechnen. Mit diesen Techniken lässt sich sequentieller Programmcode einfach parallelisieren, da hierfür nur wenige Änderungen erforderlich sind. Es müssen lediglich die Berechnungsfunktionen an die parallelisierende Funktion übergeben und die Funktion zur Auswertung der Arrayelemente ausgetauscht werden. \par
|
||||||
Zwei wichtige Punkte müssen dennoch beachtet werden. Zum einen verwendet Haskell das Konzept \emph{\en{Lazy Evaluation}}. Befehle werden immer nur soweit berechnet, wie sie an anderer Stelle benötigt werden. Dadurch entstehen manchmal zur Laufzeit große Bäume nur teilweise ausgewerteter Befehle, welche die Ausführungszeit stark negativ beeinflussen. Es muss demnach darauf geachtet werden, die Berechnung später ohnehin erforderlicher Funktionen zu erzwingen. Zum anderen ist die Anzahl der Sparks standardmäßig nicht begrenzt. Dadurch kann es vorkommen, dass neue Sparks schneller generiert werden, als sie abgearbeitet werden. Deshalb ist es sinnvoll, die Anzahl zeitgleich existierender Sparks im Programm zu beschränken.
|
\medskip
|
||||||
|
Zwei wichtige Punkte müssen dennoch beachtet werden. Zum einen verwendet Haskell das Konzept \emph{\en{Lazy Evaluation}}. Befehle werden immer nur soweit berechnet, wie sie an anderer Stelle benötigt werden. Dadurch entstehen manchmal zur Laufzeit große Bäume nur teilweise ausgewerteter Befehle, welche die Ausführungszeit durch eine hohe Garbage-Collector-Auslastung stark negativ beeinflussen. Es muss demnach darauf geachtet werden, die Berechnung später ohnehin erforderlicher Funktionen frühzeitig zu erzwingen. Zum anderen ist die Anzahl der Sparks standardmäßig nicht begrenzt, sodass auch hier zu große Arrays entstehen können, deren Abarbeitung allerdings im Verlaufe des Programms durch o.g. Lazy Evaluation evtl. gar nicht erforderlich ist. Daher beschränken wir die Anzahl der möglichen Sparks (und somit der maximal Möglichen Worker-Threads) auf 1000. Erwähnenswert ist noch, dass diese Technik \emph{nicht} von Hyper-Threading profitiert (da nichtmal mehr ein Kontextwechsel der Threads nötig ist) und wir somit 1000 \glqq echte\grqq \ Kerne für eine maximale Auslastung benötigen. Die obere Grenze wird hier dann eher durch Amdahls Gesetz, denn durch die verfügbaren Kerne beschränkt.
|
||||||
|
|
||||||
|
|
||||||
\section{Der Algorithmus}
|
\section{Der Algorithmus}
|
||||||
|
BIN
gc-analysis/hgraph-integ-gc-space.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
gc-analysis/hgraph-peak-gc-space.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
gc-analysis/hgraph-residency-gc-space.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
gc-analysis/hgraph-time-gc-space.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
gc-analysis/hgraph2000-integ-gc-space.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
gc-analysis/hgraph2000-peak-gc-space.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
gc-analysis/hgraph2000-residency-gc-space.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
gc-analysis/hgraph2000-time-gc-space.png
Normal file
After Width: | Height: | Size: 12 KiB |
@ -30,10 +30,10 @@ executable hgraph
|
|||||||
DCB.DCB,
|
DCB.DCB,
|
||||||
DCB.Structures,
|
DCB.Structures,
|
||||||
DCB.IO
|
DCB.IO
|
||||||
if os(windows) {
|
if !os(windows) {
|
||||||
ghc-options: -Odph -rtsopts -threaded -fno-liberate-case -funfolding-use-threshold1000 -funfolding-keeness-factor1000 -optlo-O3
|
|
||||||
} else {
|
|
||||||
ghc-options: -Odph -rtsopts -threaded -fno-liberate-case -funfolding-use-threshold1000 -funfolding-keeness-factor1000 -optlo-O3 -fllvm
|
ghc-options: -Odph -rtsopts -threaded -fno-liberate-case -funfolding-use-threshold1000 -funfolding-keeness-factor1000 -optlo-O3 -fllvm
|
||||||
|
} else {
|
||||||
|
ghc-options: -Odph -rtsopts -threaded -fno-liberate-case -funfolding-use-threshold1000 -funfolding-keeness-factor1000 -optlo-O3
|
||||||
}
|
}
|
||||||
extensions:
|
extensions:
|
||||||
BangPatterns,
|
BangPatterns,
|
||||||
|
@ -100,15 +100,15 @@ expand adj attr d div req g@(ind,_,_) = --trace ("expanding graph "P.++ B.unpack
|
|||||||
-- returned as a 'Graph'.
|
-- returned as a 'Graph'.
|
||||||
preprocess :: Adj -- ^ original adjacency matrix
|
preprocess :: Adj -- ^ original adjacency matrix
|
||||||
-> Attr -- ^ table of the node’s attributes
|
-> Attr -- ^ table of the node’s attributes
|
||||||
-> MaxDivergence -- ^ maximum allowed ranges of the node’s attribute
|
-> MaxDivergence -- ^ maximum allowed ranges of the node’s attribute
|
||||||
-- values to be considered as consistent
|
-- values to be considered as consistent
|
||||||
-> Int -- ^ required number of consistent attributes
|
-> Int -- ^ required number of consistent attributes
|
||||||
-> (Adj, [Graph])
|
-> (Adj, [Graph])
|
||||||
preprocess adj attr div req =
|
preprocess adj attr div req =
|
||||||
let
|
let
|
||||||
(Z:.nNodes:._) = A.extent adj
|
(Z:.nNodes:._) = A.extent adj
|
||||||
results = map (initGraph attr div req) [(i, j) | i <- [0..(nNodes-1)], j <- [(i+1)..(nNodes-1)], adj!(ix2 i j) /= 0]
|
! results = map (initGraph attr div req) [(i, j) | i <- [0..(nNodes-1)], j <- [(i+1)..(nNodes-1)], adj!(ix2 i j) /= 0]
|
||||||
-- +|| (parBuffer 25 rdeepseq)
|
+|| (parBuffer 25 rdeepseq)
|
||||||
finalGraphs = lefts results
|
finalGraphs = lefts results
|
||||||
mask = A.fromUnboxed (A.extent adj) $V.replicate (nNodes*nNodes) False V.//
|
mask = A.fromUnboxed (A.extent adj) $V.replicate (nNodes*nNodes) False V.//
|
||||||
((map (\(i,j) -> (i*nNodes + (mod j nNodes), True)) $rights results)
|
((map (\(i,j) -> (i*nNodes + (mod j nNodes), True)) $rights results)
|
||||||
@ -154,7 +154,6 @@ constraintInit ! attr ! div req i j =
|
|||||||
in if nrHit >= req then Just (fulfill, constr) else Nothing
|
in if nrHit >= req then Just (fulfill, constr) else Nothing
|
||||||
|
|
||||||
-- | removes all duplicate graphs
|
-- | removes all duplicate graphs
|
||||||
|
|
||||||
-- nub has O(n^2) complexity.
|
-- nub has O(n^2) complexity.
|
||||||
-- with this variant of ordNubBy we need a simple bucket-extractor (first function) obeying Ord
|
-- with this variant of ordNubBy we need a simple bucket-extractor (first function) obeying Ord
|
||||||
-- and a second equality-check function for edge-cases (lin-search).
|
-- and a second equality-check function for edge-cases (lin-search).
|
||||||
|