Algorithmische Performanceoptimierung

Werbung
77. XPUG Frankfurt
2011‐11‐09
Algorithmische
Performanceoptimierung
2011-11-09
Martin Müller-Rohde SCOOP Software GmbH
1
Inhalt
• Ich wollte eigentlich einen BrainDump zu Performance-Optimierung
allgemein machen
• Das kam mir aber alles so
ungeordnet vor
• Beim ordnen fand ich
– Langweilige Teile
– Übliche Teile
– Ein spannendes, irgendwie neuen Teil:
Optimierung durch Algorithmen
2
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
1
77. XPUG Frankfurt
2011‐11‐09
Inhalt
•
•
•
•
Warum Performance-Optimierung?
Wie geht man systematisch vor?
Versuch eine Ordnung zu finden
Algorithmische Performance-Optimierung
– Pattern
– Unterschiede zu den anderen Teilen
3
Warum Performance Optimierung?
• Die erste Implementierung ist selten auf Anhieb
schnell
– Ich bin meist schon froh, wenn die Fehler beseitigt sind
• Die Erwartung der Anwender/Kunden hat sich
deutlich geändert
– nicht mehr „soll der Kunde doch froh sein, dass er das nicht
mehr manuell machen muss“
– sondern klare Forderungen
• Bedienung wie ein iPhone
• viiiieeel schneller als bisheriges System
• Ausnutzung der besseren Hardware
4
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
2
77. XPUG Frankfurt
2011‐11‐09
Systematik
1.
Eine funktionierende Implementierung (es gibt viele, eine
reicht)
Funktionalität abgesichert durch gute Tests
2.
•
•
•
3.
Zu optimierender Use-Case kann möglichst einfach,
isoliert durchgeführt und beliebig oft wiederholt werden
(JUnit, Selenium, …)
Zeitmessung vor der Optimierung
Eine einzige Optimierung probieren und anhand
Zeitmessung überprüfen
4.
5.
•
•
6.
Auf fachliche Richtigkeit testen
Nicht die inneren Details testen
Nicht den Algorithmus testen
bei Verbesserung behalten, sonst verwerfen
mehrere Optimierungen könnten sich gegenseitig aufheben,
oder verschlechtern – welche war gut, welche nicht?
GOTO 5
5
Versuch einer Ordnung
• Wo steckt Optimierungspotenzial?
Java
Database
Algorithmen
6
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
3
77. XPUG Frankfurt
2011‐11‐09
Optimierungpotenzial: Java
• Wie bei jeder Programmiersprache gibt es eine
Lernkurve (die wir vermutlich alle mal durchgemacht haben)
– String+=  StringBuffer.append()  StringBuilder.append()
1/20
½
– logger.debug(a+“:“+b)  if (myLogger.isDebugEnabled()) …
– new Integer(x)  Integer.valueOf(x)
½
– java.util.Vector  java.util.LinkedList  java.util.ArrayList
– HashTable  HashMap
– InputStream  BufferedInputStream
•
LANGWEiLiG!
statische
Code-Analyse
– Verwendung•eines
SessionPools
(grep,
CheckStyle,
PMD,
Bei Java-Entwicklern
beliebt,
denn man bleibt
ja in der
wohlvertrauten Umgebung
FindBugs,…)
• mechanisch durchführbar
• geringes Risiko
7
Optimierungpotenzial: Java
• Wie bei jeder Programmiersprache gibt es eine
Lernkurve (die wir vermutlich alle mal durchgemacht haben)
– String+=  StringBuffer.append()  StringBuilder.append()
1/20
½
– logger.debug(a+“:“+b)  if (myLogger.isDebugEnabled()) …
– new Integer(x)  Integer.valueOf(x)
½
– java.util.Vector  java.util.LinkedList  java.util.ArrayList
– HashTable  HashMap
– InputStream  BufferedInputStream
– Verwendung eines SessionPools
• Bei Java-Entwicklern beliebt, denn man bleibt ja in der
wohlvertrauten Umgebung
8
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
4
77. XPUG Frankfurt
2011‐11‐09
Optimierungpotenzial: Java
• Das war einfach
• Aber: selbst sehr angestrengtes Code-Reading findet viele
Probleme oft nicht
 Profiler
• Sammlung und Auswertung von Laufzeitinformationen
– Call Stack mit Zeitmessung und/oder mit Sampling
9
Optimierungpotenzial: Java
• Das war einfach
• Aber: selbst sehr angestrengtes Code-Reading findet viele
Probleme oft nicht
 Profiler
• Sammlung und Auswertung von Laufzeitinformationen
– Call Stack mit Zeitmessung und/oder mit Sampling
– Memory Analyse
10
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
5
77. XPUG Frankfurt
2011‐11‐09
Optimierungpotenzial: Java
• Das war einfach
• Aber: selbst sehr angestrengtes Code-Reading findet viele
Probleme oft nicht
 Profiler
• Sammlung und Auswertung von Laufzeitinformationen
– Call Stack mit Zeitmessung und/oder mit Sampling
– Memory Analyse
– Concurrency Analyse
– Thread Analyse
11
Optimierungpotenzial: Java
• Das war einfach
• Aber: selbst sehr angestrengtes Code-Reading findet viele
Probleme oft nicht
 Profiler
• Sammlung und Auswertung von Laufzeitinformationen
– Call Stack mit Zeitmessung und/oder mit Sampling
– Memory Analyse
– Concurrency Analyse
– Thread Analyse
• Profiling verzerrt Ergebnisse
• Ziel: Hot Spots finden und Ineffizienzen aufdecken
– Einzelne Methoden beschleunigen
– Überflüssige Aufrufe, schlechte Schleifen, unnötige Allokationen,..
12
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
6
77. XPUG Frankfurt
2011‐11‐09
Optimierungpotenzial: Java
• Das war einfach
• Aber: selbst sehr angestrengtes Code-Reading findet viele
Probleme oft nicht
 Profiler
• Sammlung und Auswertung von Laufzeitinformationen
– Call Stack mit Zeitmessung und/oder mit Sampling
– Memory Analyse
– Concurrency Analyse
SCHON ETWAS SPANNENDER!
Profiling verzerrt Ergebnisse
• Erfordert Tools
Ziel: Hot Spots finden und Ineffizienzen aufdecken
• Schlüsselloch-Perspektive
– Einzelne Methoden beschleunigen
• Aufrufe,
Starkes
Refactoring
– Überflüssige
schlechte
Schleifen, unnötigeverstärkt
Allokationen,..
Schlüsselloch-Perspektive
– Thread Analyse
•
•
13
Versuch einer Ordnung
• Wo steckt Optimierungspotenzial?
Java
Database
Algorithmen
14
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
7
77. XPUG Frankfurt
2011‐11‐09
Optimierungpotenzial: Database
• Das ungeliebte Kind (bei den Entwicklern)
„die Datenbank ist langsam“
Tatsächlich:
Call: < 1ms (Netzwerk-Latenz!)
Query : < 10ms
Festplatten IO: > 20.000 IOPS
15
Optimierungpotenzial: Database
• Das ungeliebte Kind (bei den Entwicklern)
– Erfordert Kenntnisse wie relationale
Datenbanken funktionieren
• Ziele
– Minimierung der Aufrufe (Latenz!)
– Erhöhung der Cache-Hits
– Richtige Wahl der Indices
16
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
8
77. XPUG Frankfurt
2011‐11‐09
Optimierungpotenzial: Database
• Das ungeliebte Kind (bei den Entwicklern)
– Erfordert Kenntnisse wie relationale
Datenbanken funktionieren
• Ziele
– Minimierung der Aufrufe (Latenz!)
– Erhöhung der Cache-Hits
– Richtige Wahl der Indices
17
Optimierungpotenzial: Database
Cache-Hit-Ratio von 97% ist
schlecht!
• Das ungeliebte
Kind (bei den Entwicklern)
– Erfordert Kenntnisse wie relationale
98% ist nicht „nur 1% besser“,
Datenbanken funktionieren
sondern satte 33% besser !
• Ziele
– Minimierung der Aufrufe (Latenz!)
– Erhöhung der Cache-Hits
– Richtige Wahl der Indices
18
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
9
77. XPUG Frankfurt
2011‐11‐09
Optimierungpotenzial: Database
Wie?
1. langweilige, statische Prüfung
– Cache-Größe an RAM anpassen
– Disk-Performance und RAID-Level prüfen
• Logs sind schreib-lastig
• Daten sind lese-lastig (Level 5)
– Netzwerk-Performance ApplikationDB prüfen
– Dynamisches SQL vermeiden (SQL Area Flooding)
– Fetch-Size erhöhen
2.
schwierigere Untersuchungen dynamisch
– Statistiken lesen
– Ausführungspläne lesen und optimieren
– Relational denken, denn idiomatisches Java führt zu
schlechterer Performance
– Batching einbauen
Entwickler und DBA gemeinsam?
19
Versuch einer Ordnung
• Schnittmengen
Java
Database
Algorithmen
20
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
10
77. XPUG Frankfurt
2011‐11‐09
Optimierungpotenzial: Algorithmen
• In Java alles optimiert? Die DB optimiert? Alles im
Speicher und trotzdem dauert es 20 Sekunden?
• Dann kann es noch am Algorithmus liegen!
• Aber woher nehmen wir einen besseren Algorithmus
(ohne akademische Forschung)?
• Idee: wir benutzen Pattern!
21
Pattern für bessere Algorithmen
• Pattern #1
•
•
•
•
Pattern #2
Pattern #3
Pattern #4
Pattern #5
You can‘t always get what you
(Rolling Stones)
want
Nothing else matters (Metallica)
Hanging by a thread(Mike & the Mechanics)
(Adele)
Fool that I am
No, no, no
(Destiny‘s child)
22
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
11
77. XPUG Frankfurt
2011‐11‐09
Pattern #2: You
can‘t always get what you want
(Rolling Stones)
• Es gibt keine ultimative Genauigkeit!
• Wann ist ein Wert „genau 0.0“?
System.out.println(0.1 + 0.2 - 0.3);
10-5? 10-10? 10-100? 10-300?
• Aus anderen Disziplinen:
– Maschinenbauer messen Bauteile mit
Instrumenten die 10* genauer messen können als
das Bauteil genau sein muss
– Bauingenieure geben einen Sicherheitsaufschlag
von „Faktor 2“ auf die Berechnung
23
Pattern #2: You
can‘t always get what you want
(Rolling Stones)
• Beispiele:
– Wenn ich als kleinste Dauer ¼ Stunde anbiete, reicht eine
Berechnung auf Minuten-Genauigkeit
– Für eine Staulänge „in Kfz“ reicht eine Berechnung in dm
• Solche GUI Anzeigen
„Ihr Termin beginnt um 09:21:33,527 Uhr“
„die Stauprognose für Montag: 264,42516 Kfz“
wirken irgendwie unprofessionell
• Vorsicht: „precision lost“ bei floating point Operationen
• Aber eine Null-Stelle auf 300 Nachkommastellen zu
berechnen ist selten sinnvoll
• Der größte „precision lost“ sind oft Eingabefelder und
Importformate (CSV, XML, …)
24
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
12
77. XPUG Frankfurt
2011‐11‐09
Pattern #2: Nothing
else matters (Metallica)
• Hier geht es um Normierung:
• Konsequente Festlegung auf ein Koordinatensystem,
eine Zeitzone, feste Abstände etc. erspart
Umrechnungen
• Umrechnungen sollten nur an den Systemgrenzen
(GUI, Schnittstellen) stattfinden:
– direkt nach Eingabe wird umgewandelt,
– direkt vor der Ausgabe wird umgewandelt
25
Pattern #2: Nothing
else matters (Metallica)
• Beispiel 1: normierte Zeitzonen
Alle Datum-Objekte grundsätzlich in UTC
– jeder Tag hat jetzt 24 Stunden
 DST-Wechsel müssen nicht abgefragt werden
– Keine Prüfung ob zwei Objekte in der gleichen Zeitzone
liegen
– Keine Umrechnung innerhalb des Systems
CET
EDT
GUI
UTC+7
DB
GUI
UTC
DB
Asia/Tokyo
26
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
13
77. XPUG Frankfurt
2011‐11‐09
Pattern #2: Nothing
else matters (Metallica)
• Beispiel 2: normierte Abtastrate
Messung von Verkehrsaufkommen auf
einer Autobahn liegen als Kurve vor.
– Leider hat die Kurve nur Werte für
jeweils 15 Minuten und teilweise
Lücken. Die Abtastrate ist also nicht
konstant.
– Berechnungen mit der Kurve
(Staugefahr, Staulänge) erfordert
lineare Interpolation
Kapazität
Stau!
Durch einmalige Interpolation und
Normierung auf Abtastraten im 1Minuten-Abstand
•
•
keine weiteren Interpolationen zur
Nullstellensuche
direkt nutzbar zur numerischen
Integralberechnung
27
Pattern #3: Hanging
by a thread
(Mike & the Mechanics)
• Früher war Multi-Threading den pthreadCracks auf teuren Multi-CPU-Servern
vorbehalten
• Heute hat jeder Desktop mehrere CPUKerne, sogar Mobiltelefone !
• Und Java bringt Thread-Unterstützung schon
in der Sprache mit
Was seriell ausgeführt wird, kann parallel oft
deutlich schneller sein
28
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
14
77. XPUG Frankfurt
2011‐11‐09
Pattern #3: Hanging
by a thread
(Mike & the Mechanics)
• Beispiel:
void calcLoading() {
for (Train train : trains) {
calcTrainLoading(train);
}
}
• Parallel mit Hilfe von java.util.concurrent:
final static int N_PROCS = Runtime.getRuntime().availableProcessors();
final static ExecutorService EXECUTOR_SERVICE
= new ThreadPoolExecutor(N_PROCS, N_PROCS, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(50));
private Future<Void> asyncCalcTrainLoading(final Train train) {
return EXECUTOR_SERVICE.submit(new Callable<Void>() {
@Override
public Void call() {
calcTrainLoading(train);
return null;
}
});
}
29
Pattern #3: Hanging
by a thread
(Mike & the Mechanics)
• Beispiel:
void calcLoading()
calcLoadingParallel()
{
{
for (Train train : trains) {
calcTrainLoading(train);
asyncCalcTrainLoading(train);
}
}
Pattern
#3b:
• Parallel
mit Hilfe von java.util.concurrent:
Fire & Forget !
final static int N_PROCS = Runtime.getRuntime().availableProcessors();
final static ExecutorService EXECUTOR_SERVICE
= new ThreadPoolExecutor(N_PROCS, N_PROCS, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(50));
private Future<Void> asyncCalcTrainLoading(final Train train) {
return EXECUTOR_SERVICE.submit(new Callable<Void>() {
@Override
public Void call() {
calcTrainLoading(train);
return null;
}
});
}
30
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
15
77. XPUG Frankfurt
2011‐11‐09
Pattern #3: Hanging
by a thread
(Mike & the Mechanics)
void calcLoadingParallel()
void calcLoading()
calcLoadingParallel()
{
{
• Beispiel:
List<Future<Void>>
for (Train train
futures
: trains)
= new ArrayList<Future<Void>>();
{
calcTrainLoading(train);
asyncCalcTrainLoading(train);
// parallele
}
Berechnungen starten
for (Train
}
train : trains) {
futures.add(asyncCalcTrainLoading(train));
}
// warten bis alles abgearbeitet
(Future<Void>
future : futures) {
final static int for
N_PROCS
= Runtime.getRuntime().availableProcessors();
try {
final static ExecutorService
EXECUTOR_SERVICE
future.get();
= new ThreadPoolExecutor(
N_PROCS, N_PROCS, 0, TimeUnit.SECONDS,
} catch (Exception e) {
new ArrayBlockingQueue<Runnable>(50));
System.err.println("failed
to execute");
}
private Future<Void>
} asyncCalcTrainLoading(final Train train) {
}
.submit(new Callable<Void>() {
return EXECUTOR_SERVICE
@Override
public Void call() {
calcTrainLoading(train);
return null;
}
});
}
• Parallel mit Hilfe von java.util.concurrent:
31
Pattern #3: Hanging
by a thread
(Mike & the Mechanics)
• Fallstricke
– Kommutativ-Gesetz muss gelten, denn Reihenfolge
nicht mehr sicher
– Thread-safety
– Synchronized Methoden & Blöcke
• Ausführung nun in mehreren Threads
•  potentiell blocking
–…
32
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
16
77. XPUG Frankfurt
2011‐11‐09
Pattern #3: Hanging
by a thread
(Mike & the Mechanics)
– Schachtelung von Parallelität
• Potentiell warten alle Worker-Threads endlos
•  seperate Queues verwenden
main() {
f();
g();
}
f() {
x();
y();
}
g() {
z();
z();
}
Seriell: Main-thread:
main, f, x, y, g, z, z
Parallel 1: Main-thread: main
g,f
queue
Worker1-thread:
Worker2-thread:
Parallel 2: Main-thread: main
queue
g,f
z,z,y,x
Worker1-thread:
Worker2-thread:
f, x, y
g, z, z
f
g
33
Pattern #4: Fool
that I am (Adele)
• Der Idiot, der ich bin, versucht immer wieder
Optimierung durch „Caching“
• große Verlockung: Faktor 1 Mio schneller als
Network+Disk-Zugriffe
• nur auf den ersten Blick eine „einfache“
Optimierung
– Cache-Kohärenz
Cache 1
update
Cache 2
Original
34
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
17
77. XPUG Frankfurt
2011‐11‐09
Pattern #4: Fool
that I am (Adele)
• Der Idiot, der ich bin, versucht immer wieder
Optimierung durch „Caching“
• große Verlockung: Faktor 1 Mio schneller als
Network+Disk-Zugriffe
• nur auf den ersten Blick eine „einfache“
Optimierung
– Cache-Kohärenz
– Write-Through-Cache
update
the Cache
Original
35
Pattern #4: Fool
that I am (Adele)
• Der Idiot, der ich bin, versucht immer wieder
Optimierung durch „Caching“
• große Verlockung: Faktor 1 Mio schneller als
Network+Disk-Zugriffe
update
• nur auf den ersten Blick eine „einfache“
Cache 1
Optimierung
Original
– Cache-Kohärenz
– Write-Through-Cache
– Cluster-Betrieb: Netzwerk IO
Cache 2
36
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
18
77. XPUG Frankfurt
2011‐11‐09
Pattern #4: Fool
that I am (Adele)
• Der Idiot, der ich bin, versucht immer wieder
Optimierung durch „Caching“
• große Verlockung: Faktor
1 Mio schneller
In der Datenbank
haben wirals
Network+Disk-Zugriffe
(meist) nicht nur (B)Locking
sondern
Multi-Versioning
• nur auf den ersten Blick
eine
„einfache“ !
Optimierung
–
–
–
–
Cache-Kohärenz
Write-Through-Cache
Cluster-Betrieb: Netzwerk IO
Transaktionsserialisierung: ACID
• Atomicity, Consistency, Isolation, Durability
37
Pattern #4: Fool that I am (Adele)
• Der Idiot, der ich bin, versucht immer
wieder Optimierung durch „Caching“
• große Verlockung: Faktor 1 Mio schneller
als Network+Disk-Zugriffe
• nur auf den ersten Blick eine „einfache“
Optimierung
–
–
–
–
Cache-Kohärenz
Write-Through-Cache
Cluster-Betrieb: Netzwerk IO
Transaktionsserialisierung: ACID
• Atomicity, Consistency, Isolation, Durability
– Partitionierung der Daten
38
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
19
77. XPUG Frankfurt
2011‐11‐09
Pattern #5: No, no, no (Destiny‘s child)
Mein Lieblingspattern:
• Manchmal ist die offensichtliche Lösung für
einen Algorithmus schön einfach, übersichtlich
und gut lesbar - aber leider langsam
• Dann kann es sich lohnen, statt der
eigentlichen Lösung die Nicht-Lösung zu
suchen und dann die Lösung daraus
abzuleiten
• Also suche alle Nicht-Treffer und ziehe diese
von der Gesamtmenge ab, dann hast Du die
gesuchte Treffermenge
39
Pattern #5: No, no, no (Destiny‘s child)
Beispiel 1
• Wann überlappen sich zwei Zeitintervalle
(start1, end1) und (start2, end2)?
• Erste Lösung:
(start1 >= start2 && start1 <= end2)
|| (end1 >= start2 && end1 <= end2)
|| (start2 >= start1 && start2 <= end1)
• Negieren: wann überlappen sie NICHT?
(end1 < start2) || (end2 < start1)
• Doppelt-Negieren: wann überlappen sie NICHT-Nicht?
(end1 >= start2) && (end2 >= start1)
40
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
20
77. XPUG Frankfurt
2011‐11‐09
Pattern #5: No, no, no (Destiny‘s child)
Beispiel 2
• Prüfung von Arztrechnungen nach GOÄ:
Bestimmte Leistungen (GOÄ-Ziffern) dürfen „nicht neben“ anderen
abgerechnet werden.
• Aufgabe: welche Rechnungspositionen müssen gestrichen werden,
so dass der Arzt die höchstmögliche, GOÄ-konforme Rechnung stellt
• Erste Lösung: Optimierung in Anlehnung an Dijksrtras Algorithmus
zur Bestimmung kürzester Wege
Position
A
B
C
GOÄ-Ziffer Betrag [€]
5030
73,87
5031
140,42
5035
52,83
NNZ-Hinweis
Nicht neben Pos B, C
Nicht neben Pos A, C
Nicht neben Pos A, B
A
B
73,87€
140,42€
C
• Optimierung: doppelte Negierung !
52,83€
• welche Rechnungspositionen müssen erhalten bleiben, so dass der
Arzt die höchstmögliche, GOÄ-konforme Rechnung stellt
41
Nochmal: Pattern für bessere Algorithmen
#1 You can‘t always get what you want
• nicht präziser als nötig, Abbruchbedingung sinnvoll setzen
(iterative Verfahren, numerische Integration, …)
#2 Nothing else matters
• Normierung erspart Sonderfälle und Umrechnungen
#3 Hanging by a thread
• Parallelisierung, Ausnutzung der CPU-Kerne
#4 Fool that I am
• Caching
#5 No, no, no
• Doppelte Negierung
42
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
21
77. XPUG Frankfurt
2011‐11‐09
Versuch einer Ordnung
• Schnittmengen
ConnectionPool
Java
Database
Gesammeltes Informatikwissen über
Datenstrukturen, Sortierverfahren etc.
• java.util
• Apache Commons Collections
• Google Guava
Collections
Caching
Algorithmen
43
Zusammenfassung
•
•
Alte Regel: um Performance kümmert man sich erst wenn es nötig ist
Gegenargumente
–
–
–
wenn alles langsam ist, wird späte Performanceoptimierung sehr schwer
der Algorithmus ist grundlegend und kann später nur schwer ausgetauscht werden
erfahrene Entwickler spüren früh, wenn etwas zu langsam sein wird
•
tumbes Refactoring versperrt den Blick auf den Algorithmus
•
langweilige statische Prüfung führen zu Java-Optimierung und DB-Tuning
•
dynamische Prüfung und Optimierung funktionieren oft auch ohne intime
Kenntnisse der Applikation
•
für Optimierung durch Algorithmen muss man sich tiefer in das (fachliche)
Problem eindenken, Schlüsselloch-Perspektive verlassen
Änderung des Algorithmus macht den Code oft schwerer verständlich
–
–
–
•
–
oft erkennbar an kleinen Methoden mit vielen Parametern
für Anfänger geeignet
aber tiefe Java und DB Kenntnisse nötig
„da fährt keiner nicht über Grün“
44
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
22
77. XPUG Frankfurt
2011‐11‐09
Fragen ?
45
© 2011 Martin Müller‐Rohde, SCOOP Software GmbH
23
Herunterladen