Benutzung und Tuning von JMH

Wird ein JMH-Projekt von der Kommandozeile ohne weitere Parameter gestartet, so werden die Defaultwerte übernommen. Je nachdem, wie lange ein Benchmark dauert, ergibt sich eine unterschiedliche Anzahl an Durchläufen (eine Iteration läuft eine Sekunde lang). Das bedeutet wiederum, dass unklar ist, wann Hotspot-Optimierungen durchgeführt werden. Deshalb ist es sinnvoll, diese Werte zu überprüfen. Dieser Artikel beschreibt einige JMH-Optionen und deren Optimierung in der Praxis.

Anzahl der Warmup-Iterationen

JMH bietet dafür Profiler an. Um sicherzustellen, dass während des Benchmarks keine JIT-Kompilierung durchgeführt wird, können die Statistiken mit Hilfe des Profilers „hs_comp“ untersucht werden.
Hier ein Beispiel für die Ausgabe des Profilers:

Iteration  20: 309496556,530 ops/s
                 ·compiler.nmethodCodeSize:  604,750 Kb
                 ·compiler.nmethodSize:      1013,000 Kb
                 ·compiler.osrBytes:         0,507 Kb
                 ·compiler.osrCompiles:      3,000 methods
                 ·compiler.osrTime:          15,031 ms
                 ·compiler.standardBytes:    48,589 Kb
                 ·compiler.standardCompiles: 579,000 methods
                 ·compiler.standardTime:     393,727 ms
                 ·compiler.totalBailouts:    ≈ 0 methods
                 ·compiler.totalCompiles:    582,000 methods
                 ·compiler.totalInvalidates: ≈ 0 methods
                 ·compiler.totalTime:        408,759 ms

Steigen die Werte an, so werden Optimierungen in den Iterationen durchgeführt. Das verfälscht die Testergebnisse. Die Optimierungen sollen in der Warmup-Phase durchgeführt werden. Als Entwickler kann man dafür an den Stellschrauben „-w“ (für die Warmup-Zeit) und „-wi“ (Anzahl der Warmup-Iterationen) drehen.
Problem bei Profilern ist, dass der Profiler erst nach der Warmup-Phase läuft. Deshalb kann der Profiler selbst wieder JIT-Optimierungen verursachen (meine Vermutung, siehe den nächsten Absatz), da jetzt neuer Code ausgeführt wird. Als Alternative zum „hs_comp“-Profiler kann daher das JVM-Flag „-XX:+PrintCompilation“ genutzt werden. Auch damit gelingt es oft nicht vollständig, alle JIT-Kompilierungen in die Warmup-Phase zu verlagern. Mit der Zeit entwickelt sich aber ein Gefühl, wann der Code „aufgewärmt“ ist.

java -jar benchmarks.jar -jvmArgs -XX:+PrintCompilation -f 1 -wi 100

Vermutung: Profiler verursachen JIT-Compilation

Der Benchmark wird gestartet mit:
java -jar target/benchmarks.jar -jvmArgs -XX:+PrintCompilation -f 1 -wi 100 -prof hs_comp

Die Ausgabe (die Ausgabe des Profilers habe ich entfernt) sagt, dass in der ersten Iteration der ObjectOutputStream vom JIT-Compiler betroffen ist:

...
# Warmup Iteration  98: 310172820,693 ops/s
# Warmup Iteration  99:   99571  756       4       java.util.concurrent.ConcurrentHashMap::get (162 bytes)
311345201,664 ops/s
# Warmup Iteration 100:   99574  187       3       java.util.concurrent.ConcurrentHashMap::get (162 bytes)   made not entrant
300041258,685 ops/s
Iteration   1:  101573  758       3       sun.reflect.ClassFileAssembler::emitInt (46 bytes)
 101574  748   !   4       java.io.ObjectOutputStream::writeObject0 (619 bytes)   made not entrant
 101574  757       3       sun.reflect.ClassFileAssembler::opc_invokespecial (26 bytes)
 101574  469       3       net.modellierung.generated.InliningBenchmark_testBimorphBothTypes_jmhTest::testBimorphBothTypes_thrpt_jmhStub (58 bytes)   made zombie
288048392,852 ops/s
Iteration   2:  102576  759   !   3       java.io.ObjectOutputStream::writeObject0 (619 bytes)
288340959,529 ops/s
...

Zum Überprüfen der Vermutung wird derselbe Test nochmal mit 200 Warmup-Iterationen ausgeführt, auch hier taucht der ObjectOutputStream wieder auf:

...
# Warmup Iteration 194: 321640659,056 ops/s
# Warmup Iteration 195: 303901282,485 ops/s
# Warmup Iteration 196: 287955025,384 ops/s
# Warmup Iteration 197: 288741152,039 ops/s
# Warmup Iteration 198: 288386461,504 ops/s
 198626  858       1       java.lang.String::toString (2 bytes)
 198626  639       3       java.lang.String::toString (2 bytes)   made not entrant
# Warmup Iteration 199: 290359354,168 ops/s
# Warmup Iteration 200: 315150741,755 ops/s
Iteration   1:  201630  755   !   4       java.io.ObjectStreamClass::lookup (335 bytes)   made not entrant
 201630  860       3       sun.reflect.ClassFileAssembler::emitInt (46 bytes)
 201631  745   !   4       java.io.ObjectOutputStream::writeObject0 (619 bytes)   made not entrant
 201631  861       4       java.io.ObjectStreamClass::hasWriteObjectMethod (17 bytes)
321301694,150 ops/s
...

Das ist kein harter Beweis, aber der Verdacht besteht, dass durch „Scharfschalten“ des Profilers nach dem Warmup weitere JIT-Kompilationen verursacht werden. Wenn der Benchmark mit mehreren Threads, die nicht synchonisiert werden, ausgeführt werden, so kann das Ergebnis etwas verfälscht werden. Bei einem Thread bin ich mir nicht sicher, je nachdem, ob die Kompilierung während der Ausführung des „echten Codes“ oder des zusätzlichen Benchmarkcodes abläuft, oder wie der generierte Benchmarkcode die Zeiten summiert, kann es ebenfalls Beeinflussungen geben. Diese sind aber normalerweise nicht signifikant.

Eliminierung von externen Einflüssen

Der Benchmark sollte unter möglichst gleichen Bedingungen wie der Code in Produktion ablaufen. Falls das nicht erreicht werden kann, dann sollte für konstante Bedingungen gesorgt werden.

Bei Ausführung auf dem Entwicklerrechner sollte deshalb die IDE geschlossen werden (Hintergrundindizierung und Codeanalyse!) und, falls der Rechner ein Notebook ist, eine feste CPU-Frequenz vorgegeben werden.

Eliminieren der GC-Effekte

Ein Microbenchmark hilft bei der Performancebeurteilung. Da die GarbageCollection der JVM an jedem Safepoint stattfinden kann, kann dies auch während einer Iteration geschehen, und somit einen Extremwert bei den Durchlaufzeiten verursachen.
Nun gehört zur Bestimmung der Laufzeit des Codes auch das Aufräumen der allokierten Objekte. Es kann aber vorkommen, dass bei dem Vergleich von zwei Benchmarks bei dem einen Benchmark die GC z.B. in der Setup-Phase einer Iteration stattfindet, bei dem anderen Benchmark aber während der Ausführung der Messung. Das schränkt dann die Vergleichbarkeit ein.
Zum einen kann man versuchen, das Problem statistisch zu lösen, mit vielen Iteration und vielen Forks (der -f Parameter von JMH). Weiterhin kann der Speicher der JVM so angepasst (verringert) werden, dass die GarbageCollection häufiger, aber schneller stattfindet. JMH bietet ebenfalls den Parameter „-gc true“ an (im Hilfetext steht „use with care“).

MSETEX – command in Redis

Redis has multiple commands that work on a number of keys. For example, you can use „MSET“ to set a number of keys to individual values. And you can use „SET“ or „SETEX“ to set a single key to a value with expire time. However, there is no command that handles multiple keys with expire time.

Since you can run lua-scripts in Redis, it is possible to build a custom MSETEX-command with lua. Here is the lua-code:

for i=1, #KEYS, 1 do 
    redis.call('SET', KEYS[i], ARGV[(i*2)-1] , 'EX', ARGV[(i*2)]) 
end 
return 'OK'

Remarks:

  • in contrast to some programming languages, lua uses 1 as the first index of a collection
  • „return ‚OK'“ is needed for vert.x, as vert.x-redis requires a non-null result

I’m not a lua expert, so feel free to improve the script if you spot something.

OK, the script is there, how do I use it? I use vert.x, so the first thing to do is set up a redis connection. Use „scriptLoad“ to send the script to redis, and store the result-hash. Now you can use the „evalsha“ command to run the script with your values. Here is some java-code for vert.x:

        List<String> keys = new ArrayList<>(entries.size());
        List<String> values = new ArrayList<>(entries.size()*2);
        for (int i = 0; i < 2; i++) {
            keys.add(i, "key"+i);
            values.add((i*2),"value");
            values.add((i*2)+1,"60"); //TTL in seconds
        }
        redisConnection.evalsha(msetexHash,keys,values,handler);

Remarks:

  • Collections are 0-based in java, thus the index magic is differs from lua
  • For each key you must specify 2 values: first the payload to set, then the ttl
  • error handling not implemented here
  • msetexHash must be initialized with the result of the „script load“ command
  • while the script is evaluated, redis blocks. Run some tests before using a large number of keys

Der Performance auf der Spur – Teil 2 (Projektbeschreibung)

Herzstück des Beispielprojekts ist eine Klasse namens „Calculator“. Diese führt Berechnungen auf Interfaces durch. Das Interface hat entweder eine Implementierung (für den monomorphen Fall), zwei (bimorph) oder 3 (trimorph oder megamorph).

Der Code von Calculator:

package net.modellierung;

public class Calculator {

    public long add(Monomorph m1, Monomorph m2) {
        return m1.getValue() + m2.getValue();
    }

    public long add(Bimorph b1, Bimorph b2) {
        return b1.getValue() + b2.getValue();
    }

    public long add(Trimorph t1, Trimorph t2) {
        return t1.getValue() + t2.getValue();
    }
}

Die Klassen des Projekts – Interfaces mit einer bestimmten Anzahl von Implementierungen:

Klassendiagramm Performanceprojekt

Die Implementierungen der Interfaces besitzen einen Konstruktor mit einem Long-Parameter. Dieser Wert wird beim Aufruf von getValue() zurückgegeben.

Erwartetes Verhalten des JIT-Compilers ist, dass er im ersten Fall Code generieren kann, der auf die eine Implementierung zugeschnitten ist, und damit den teuren virtuellen Methodenaufruf umgeht. Trotzdem müssen in diesem Fall Instruktionen zur Absicherung des Typs generiert werden, da zur Laufzeit eine weitere Implementierung nachgeladen werden kann. Der kompilierte Code muss das merken und dann eine Deoptimierung durchführen (also wieder den Bytecode ausführen und ggf. neuen Code (der beide Implementierungen berücksichtigt) generieren.
Im zweiten Fall weiß der JIT-Compiler, dass es zwei Implementierungen gibt. Der generierte Code kann aber mit einem Typcheck beide Implementierungen unterscheiden und somit auch den virtuellen Aufruf sparen. Auch hier muss der generierte Code Logik enthalten, um ggf. nachgeladene Implementierungen zu berücksichtigen.
Der dritte Fall soll hingegen nicht optimierbar sein, es müssen also virtuelle Aufrufe stattfinden.

Richard Warburton hat bereits mit einer etwas anderen Fragestellung einen interessanten Artikel geschrieben.

Der Performance auf der Spur – Teil 1

Bei Performanceproblemen von Anwendungen mit niedrigen Latenzzeiten merkt der Entwickler schnell, dass die Java Runtime Environment sehr viele versteckte Dinge macht. Sei es, dass BiasedLocking nach 4 Sekunden plötzlich eingeschaltet wird, oder dass der Just-in-time-Compiler (im folgenden „JITC“ genannt) ein Stück Code optimiert, auf jeden Fall kann es Sprünge in der Performance geben, die auf den ersten Blick nicht erklärbar sind.

An verschiedenen Stellen habe ich gelesen, dass der JITC Interfaces unterschiedlich behandelt. Gibt es nur eine Implementierung (der Classloader kann später ggf. weitere nachladen), dann muss kein Code für Polymorphismus generiert werden, sondern die Methode kann direkt aufgerufen werden.

Da es häufig vorkommt, dass zwei Implementierungen eines Interfaces (oder einer abstrakten Klasse) vorliegen, gibt es dafür auch noch eine Optimierung: Der generierte Code prüft mit „instanceof“, welche der Implementierung vorliegt, und muss dann ebenfalls keinen polymorphen Aufruf machen.

Darüber zu lesen ist eine Sache, es auszuprobieren eine andere. Da der JITC erst nach einigen tausend Aufrufen eine Methode optimiert, habe ich mich für das JMH-Framework entschieden, womit sich sehr einfach Microbenchmarks erstellen lassen.

Die ersten Ergebnisse überraschen:

# Run complete. Total time: 00:02:01

Benchmark                                 Mode  Cnt          Score          Error  Units
InliningBenchmark.testBimorphBothTypes   thrpt   20  318966702,179 ±  8259966,525  ops/s
InliningBenchmark.testBimorphSingleType  thrpt   20  320496503,653 ±  6204071,530  ops/s
InliningBenchmark.testMonomorph          thrpt   20  303301715,877 ± 13156878,442  ops/s

Die Performance mit nur einer Implementierung eines Interfaces ist etwas langsamer als der Test mit 2 Implementierungen.

Hat der JITC hier wirklich optimiert? Sind die JMH-Einstellungen (Warmup-Iterationen, Iterationen etc.) richtig gewählt? Ist der Benchmal fehlerhaft?

Diese Fragen möchte ich in den nächsten Blogposts klären, eine Projektbeschreibung mit Beispielcode wird es ebenfalls geben.

Das Java Memory Model

Als Java-Programmierer macht man sich im Allgemeinen keine Gedanken über die Hardware, auf der das Programm ausgeführt wird. Das ist eine riesige Erleichterung für Entwickler, aber das bedeutet auch, dass sich JVM und Compiler um das gleiche Verhalten auf unterschiedlicher Hardware (Multiprozessorsysteme) kümmern müssen. Mit Java 5 wurde der JSR-133 umgesetzt, dieser beschreibt das Java Memory Model.

Ein Programm mit mehreren Threads kann bei mehrmaliger Ausführung jeweils einen anderen Ablauf haben (je nachdem, wann ein anderer Thread aktiviert wird). Das Memory Model definiert also nicht einen bestimmten Ablauf für ein Programm, sondern bietet die Möglichkeit, eine bestimmten Ausführungsreihenfolge für ein Programm zu verifizieren. Der Entwickler kann sich einen Ablauf überlegen, in dem das Programm ein falsches Verhalten zeigt, und das Memory Model sagt dann, ob dieser für das Programm gültig ist. Da sowohl der Compiler, als auch der Prozessor pro Thread unabhängige Statements in eine andere Reihenfolge bringen können, gibt es sehr viele potentielle Fehler. Das Memory Model bietet aber über Synchronisierung die Möglichkeit, eine threadübergreifende Vorher-Nachher-Beziehung zwischen Programmteilen zu definieren.

Das Memory Model ist in der „Java Language and Virtual Machine Specification“  beschreiben, die für Java 8 im Abschnitt 17.4. Die Spezifikation ist relativ einfach zu Lesen mit vielen Beispielen, ich empfehle die Lektüre!

Weitere Hintergrundinformationen und Beispiele gibt es in dem Google Talk und auf der Seite http://www.cs.umd.edu/~pugh/java/memoryModel/.

Vert.x und der EventBus

Vert.x ist ein Framework zum Bauen von reaktiven Anwendungen. Eine zentrale Komponente ist der EventBus, über den Kommunikation zwischen den Komponenten des Systems abläuft. Der EventBus kann einerseits Events veröffenlichen (an viele Empfänger), oder mit einem Empfänger über einen Request/Response-Mechanismus kommunizieren.

Wenn mehrere Empfänger für eine Adresse registriert sind, und über den Request/Response-Mechanismus kommuniziert wird, dann bekommt ein Empfänger die Nachricht zugestellt.

If there is more than one handler registered at the address, one will be chosen using a non-strict round-robin algorithm.

Wie funktioniert das in der Praxis? Wie fair ist der Mechanismus? Ich habe dazu ein Beispiel geschrieben:

package net.modellierung.vertx;

import io.vertx.core.Vertx;
import io.vertx.ext.unit.Async;
import io.vertx.ext.unit.TestContext;
import io.vertx.ext.unit.junit.Timeout;
import io.vertx.ext.unit.junit.VertxUnitRunner;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(VertxUnitRunner.class)
public class EventBusTest {
 public static final String A_EVENT = "A";
 public static final int COUNT = 30;
 @Rule
 public Timeout timeout = Timeout.millis(18000);
 private Vertx vertx;

@Before
 public void setUp(TestContext context) {
   vertx = Vertx.vertx();
 }

@Test
 public void testEventReceiveFairness(TestContext context) {
 StringBuffer sb = new StringBuffer();
 Async eventsReceived = context.async(COUNT);
 vertx.eventBus().consumer(A_EVENT, event -> {
   sb.append("1");
   eventsReceived.countDown();
 });
 vertx.eventBus().consumer(A_EVENT, event -> {
   sb.append("2");
   eventsReceived.countDown();
 });
 for (int i = 0; i < COUNT; i++) {
   vertx.eventBus().send(A_EVENT, "");
 }
 eventsReceived.await();
 System.out.println(sb);
 }
}

Der Test registriert zwei Empfänger für ein Event, und sendet das Event dann COUNT mal. Die Empfänger schreiben dann eine 1 oder 2 in den StringBuffer. Am Ende wird der StringBuffer ausgegeben. Je nach Durchlauf wird eine andere Zeichenfolge ausgegeben.

Meine ersten beiden Durchläufe haben die Folgen „212222222222222211111111111111“ und „111111111111111222222222222222“ ergeben. Das ist eine gute Fairness.

Aktualisierung des Blogs

Im Moment finde ich keine Zeit für neue Blog-Beiträge. Ich schreibe gerade, neben meiner normalen Arbeit, an einer Webanwendung zur Wortschatzanalyse. Ein paar interessante Themen für den Blog sind mir dabei begegnet, z.B. das Java Memory Modell, aber ich komme zur Zeit einfach nicht zum Schreiben.

Ab dem Anfang nächsten Jahres plane ich, wieder aktiver an diesem Blog zu schreiben.

Wortstämme

Eine reine Suche nach exakten Wörtern ist bei Internet-Suchmaschinen überholt. Heutzutage werden als Ergebnisse auch Wortvarianten angezeigt. Z.B. liefert eine Suche nach „arbeiten agentur“ die Bundesagentur für Arbeit als ersten Treffer, obwohl auf deren Einstiegsseite das Wort „arbeiten“ gar nicht auftaucht. Das liegt daran, dass die Suche nicht auf den exakten Wörtern, sondern auf Wortstämmen stattfindet.
Ein Wort in der Deutschen Sprache lässt sich auf eine Grundform zurückführen. So haben die Wörter „Arbeiter“, „arbeitete“, „Verarbeitung“ alle die Grundform „Arbeit“. Anders gesagt: Wenn in einem Satz eine bestimmt Form eines Worts stehen muss, dann gibt es eine Bildungsregel, die aus der Grundform die gewünschte Form produziert (insofern besitzt jede Sprache auch ein Metamodell). Für die Suche ist das Gegenteil erforderlich: Bei der Analyse einer Webseite werden aus den Wörtern die Grundformen gebildet und in einer Datenbank abgelegt. Bei der Suche wird die Grundform des Suchbegriffes dann mit der Datenbank verglichen (teilweise können Datenbanken, z.B. MongoDB, schon selbst die Wortstämme bilden).

Natürlich gibt es in den Sprachen Ausnahmen: Neben unregelmäßigen Verben können noch die Bildungsregeln in die Irre führen. Wenn „Arbeit“ und „Verarbeitung“ denn gleichen Sinn besitzen, dann führt dieselbe Bildungsregel bei „Verfassung“ in die Irre, denn „Fass“ besitzt eine andere Bedeutung.

Bei der Internet-Suche ist der Vorteil, dass die Grundform der Wörter für den Nutzer nicht sichtbar ist. Deshalb muss gar nicht die linguistische Grundform gebildet werden, sondern es reicht, mit Reduzierungsregeln die Wörter in eine vergleichbare Form zu bringen. Ein verbreiteter Algorithmus dazu ist der Porter-Stemmer [http://snowball.tartarus.org/algorithms/german/stemmer.html]. Für diesen Algorithmus gibt es Portierungen in unterschiedliche Programmiersprachen, deren Ergebnisse sich aber teilweise von dem beschriebenen Algorithmus unterscheiden. Für die deutsche Sprache wird immer eine spezielle Variante der Implementierung benötigt, die dann auch funktioniert. Ich empfehle, mit ein paar Tests die Eignung des Stemmers zu testen, der Diminutiv wird in den meisten Implementierungen nicht beachtet („Schiffchen“ sollte dasselbe Ergebnis wie „Schiff“ haben), auch bei kurzen Wörtern kann es Probleme geben („ein“, „eine“, „einer“ sollten ebenfalls dasselbe Ergebnis produzieren). Eine Herausforderung ist auch, unterschiedliche Wörter nicht auf denselben Stamm zurückzuführen, „Schwer“ und „Schwester“ können hier zu Verwirrungen führen.

Falls Sie mit dem Gedanken spielen, eine eigene Implementierung eines Stemmers für die deutsche Sprache zu versuchen, so können Sie mich kontaktieren, ich kann Testdaten zur Verfügung stellen und habe auch eine eigene Implementierung eines Stemmers. Kontaktdaten unter http://www.modellierung.net/.

Alles in allem ist die Wortstammbildung noch nicht gänzlich von Computern beherrschbar. Vor dem Anwender kann das teilweise verborgen werden, in der Informationsverarbeitung führt das aber zu großen Problemen.

ProGuard optimizer and obfuscator in multi-module maven projects

ProGuard is a tool that processes java .class files and generates smaller, faster and harder to read bytecode.
I had to use it for a single module in a multi-module maven project, and found no documentation for that, thus I describe the steps here.

In the module that should be obfuscated, a build step has to be configured in maven. The important thing here is the configuration with „<attach>true“, so that the obfuscated file is exported to the repository and hence can be used by other modules.

    <build>
        <plugins>
            <plugin>
                <groupId>com.github.wvengen</groupId>
                <artifactId>proguard-maven-plugin</artifactId>
                <version>2.0.8</version>
                <dependencies>
                    <dependency>
                        <groupId>net.sf.proguard</groupId>
                        <artifactId>proguard-base</artifactId>
                        <version>5.2</version>
                    </dependency>
                </dependencies>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>proguard</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <proguardVersion>5.2</proguardVersion>
                    <attach>true</attach>
                    <obfuscate>true</obfuscate>
                    <options>
                        <option>-keep public class package.ParameterObject{ public *;}</option>
                        <option>-keep public class package.Implementation{public static void
                            process(package.ParameterObject);}
                        </option>
                        <option>-optimizationpasses 4</option>
                    </options>
                    <libs>
                        <lib>${java.home}/lib/rt.jar</lib>
                    </libs>
                </configuration>
            </plugin>
        </plugins>
    </build>

If you run the build, you can see that two artifacts are generated now by this module: A module-1.0.0-SNAPSHOT.jar that has the same content as before, and a module-1.0.0-SNAPSHOT-small.jar that contains the small and obfuscated code. You can pick another name instead of „small“ by specifying the „attachArtifactClassifier“-property in the configuration.

How do you use this obfuscated code from another module in the project? Pretty easy, the small-suffix (or however you named it) can be used as the classifier in the maven-dependency:

        <dependency>
            <groupId>...</groupId>
            <artifactId>...</artifactId>
            <classifier>small</classifier>
        </dependency>

Run the build and look at the packaged artifact (for example a .war), it will no longer contain the clear bytecode, but only the obfuscated library.

Wortschatz

Ab einer gewissen Menge an Text kann man Rückschlüsse auf den Wortschatz des Autors ziehen. Zum Beispiel besteht dieser Blog (dieser Artikel nicht mitgezählt) aus 9318 deutschsprachigen Wörtern.

Nicht nur für UML, sondern auch für natürliche Sprache kann ein Metamodell gebildet werden. Ein Wort hat neben seiner Ausprägung im Text noch Predikate (z.B. „ist ein Name“), und einen Wortstamm. Zum Beispiel ist die Stammform des Worts „neuen“ „neu“. Der Wortstamm „neu“ kann wiederum die folgenden Ausprägungen in einem Satz besitzen: „neues“, „neuen“, „neuem“, „neue“, „neuer“ und „neu“. Die Aufzählung ist wahrscheinlich nicht vollständig.

Um von einem Text auf den Wortschatz zu schließen eignet sich folgendes Vorgehen:

  • Der Text wird in Wörter zerlegt
  • Namen werden entfernt
  • Für jedes Wort wird der Wortstamm ermittelt

Bei diesem Blog basieren die 9318 Wörter auf 1883 Wortstämmen. Als Vergleichswert: Goethes „Faust“ (erster Teil) besteht aus 30561 Wörtern, diese lassen sich auf 4542 Wortstämme zurückführen.
Die gravierendsten Unterschiede zwischen Faust und diesem Blog, also Wörter, die in einer Quelle häufig vorkommen und in der Vergleichsquelle gar nicht:
In Goethes Faust kommen die Wörter „UML“, „Klasse“, „metamodell“, „beispiel“, „diagramm“, „modell“, „instanz“ und „information“ nicht vor, während sie in diesem Blog sehr häufig vorkommen. Andersherum benutzt Goethe oft die Wörter „du“, „faust“, „euch“, „dir“, „lieb“ und „geist“, während dieser Blog diese Wortstämme nicht verwendet.