UNIXwork

WTF C Teil 3

16. January 2020

Aus der beliebten Serie WTF C:

bool trigraphs_are_available() {
    // Are trigraphs available??/
    return false;
    return true;
}

Was passiert hier? Zwei return-Statements hintereinander ergeben doch keinen Sinn! Kann die Funktion jemals true zurückgeben?

Auch dieses mal handelt es sich um standardkonformes C und es gibt auch Fälle, in denen die Funktion true liefert. Der Trick ist hier, dass im Kommentar etwas versteckt ist, nämlich am Ende ein Trigraph. Es ist nämlich in C möglich, manche Sonderzeichen durch alternative Zeichenketten auszudrücken. Am Ende des Kommentars steht der Trigraph ??/, der zu einem Backslash aufgelöst wird und ein Backslash am Ende eines einzeiligen Kommentars erweitert diesen auf die nächste Zeile. Wenn also der Compiler Trigraphen unterstützt, sind die ersten beiden Zeilen der Funktion ein Kommentar und übrig bleibt nur return true.

Gefunden habe ich das Beispiel hier.

Autor: Olaf | 0 Kommentare | Tags: c, wtf

Raspberry Pi1 vs Raspberry Pi4 vs Fujitsu s920 vs Sun Ultra 45

24. December 2019

Vor Jahren hatte ich ein Raspberry Pi 1B als Server für ein paar kleine Dienste im Einsatz. Ersetzt habe ich das Teil aber später durch einen Fujitsu Futro s920, ein Thin-Client mit einer AMD G-Serie Quad-Core CPU (GX-415 GA). Die AMD G-Serie ist für embedded Computer entwickelt und daher hat der Fujitsu Thinclient auch eine recht niedrige Leistungsaufnahme. Zumindestens im Idle ist diese nicht viel höher als beim Raspberry Pi, aber dafür mit deutlich mehr Rechenleistung.

Ein Raspberry Pi 4 bietet ebenfalls eine Quadcore-CPU mit ähnlichem Takt. Daher hatte ich die Idee, mal ein bisschen die Hardware zu benchmarken. Und aus Spaß vergleiche ich das noch mit einer Sun Ultra 45 Workstation, die zumindestens einen ähnlichen CPU-Takt hat. Diese hat zwei UltraSPARC IIIi CPUs, ich glaube aus dem Jahr 2003.

Test-Hardware

Sowohl beim Raspberry Pi 1 als auch 4 kommt ein 32bit Raspbian zum Einsatz. Vielleicht nicht optimal für die Performance, auf grund des wenigen RAMs (2 GB) aber praktisch vielleicht die bessere Wahl für den Produktivbetrieb.

Benchmarks

Als Integer-Benchmark wird die Schach-Engine stockfish benutzt. Stockfish bietet sehr gute Optimierung für x86 und auch ARM, aber nicht für sparc. Daher habe ich das ganze ohne spezielle Architekturoptimierungen kompiliert, also nur mit normaler Compiler-Optimierung.

stockfish bench 128 <num-cpus>

Als Floatingpoint-Benchmark kommt c-ray zum Einsatz. Dies ist ein einfacher in C geschriebener Raytracer.

c-ray-mt -t <num-cpus> -s 1000x1000 -r 10 -i scene -o out.ppm

Ergebnisse Stockfish

Die Ausgabe von stockfish bench sieht so aus:

Total time (ms) : 14304
Nodes searched  : 5607358
Nodes/second    : 392013

Für den Vergleich nehm ich nur Nodes/second. Je größer hier der Wert ist, desto besser:

rpi1     u45-1cpu   u45-2cpu   rpi4-1cpu   rpi4-4cpu   s920-1cpu   s920-4cpu
----------------------------------------------------------------------------
26503    277715     559029     392013      1421621     403146      1595700
26655    277386     559604     393222      1402173     402682      1530797
26623    277002     549077     385544      1424797     403261      1580684
26303    277152     566223     391739      1458121     402191      1551561
26926    277317     548266     391411      1435041     403146      1503410

Mittelwert mit Standardabweichung:

rpi1           26.602   ±    203,68
u45-1cpu      277.314,4 ±    240,66
u45-2cpu      556.439,8 ±  6.832,99
rpi4-1cpu     390.785,8 ±  2.691,36
rpi4-4cpu   1.428.350,6 ± 18.298,75
s920-1cpu     402.885,2 ±    400,12
s920-4cpu   1.552.430,4 ±  3.293,49

Ergebnisse C-Ray

C-Ray liefert am Ende die Anzahl der Millisekunden, die das Rendern gedauert hat.

rpi1     u45-1cpu   u45-2cpu   rpi4-1cpu   rpi4-4cpu   s920-1cpu   s920-4cpu
----------------------------------------------------------------------------
88444    29585      16133      10015       3128        13699       4282
86406    29013      16112      10028       3118        13698       4189
87455    29014      16134      10032       3138        14068       4186
86456    29000      16106      10032       3119        14058       4189
86676    28998      16033      10004       3110        13701       4185

Mittelwert mit Standardabweichung:

rpi1        87.087,4 ± 775,59
u45-1cpu    29.122   ± 231,59
u45-2cpu    16.103,6 ±  37,01
rpi4-1cpu   10.022,2 ±  11,03
rpi4-4cpu    3.122,6 ±   9,58
s920-1cpu   13.844,8 ± 178,19
s920-4cpu    4.206,2 ±  37,93

Auswertung

Der Unterschied zwischen Raspberry Pi 1 und 4 ist gigantisch. Allein im Single-Core Benchmark ist ein rpi4 14 mal schneller als ein rpi1. Dafür bietet ein rpi4 aber statt einem Core gleich 4. Auch die FP-Geschwindigkeit ist deutlich höher. Aber das war auch nicht anders zu erwarten.

Spannender ist der Kampf zwischen rpi4 und Fujitsu s920. Diese sind in etwa auf Augenhöhe. Im Integer-Benchmark führt die AMD-CPU, im FP-Benchmark die ARM-CPU. Interessanterweise ist beim Stockfish-Benchmark die Standardabweichung beim s920 deutlich geringer als beim rpi4. Eine mögliche Ursache könnten die thermischen Probleme beim rpi4 sein, die zu einer Reduzierung des CPU-Takts führen könnten. Doch bei C-Ray ist es genau umgekehrt. Ich habe noch keine Erklärung dafür, wieso beim rpi4 hier die Werte so viel stabiler sind, als bei den anderen Testkandidaten.

Etwas schwach finde ich die Resultate bei der Ultra 45. Die Hardware ist natürlich schon etwas in die Jahre gekommen, aber da es sich hier nicht um kleine embedded CPUs handelt, hätte ich doch mehr erwartet. Eine Ursache könnte aber auch sein, dass ich auf der Ultra 45 ein recht altes OS (Solaris 10) betreibe und zum kompilieren hatte ich nur gcc 5.5 zur Verfügung. Auf dem rpi4 z.B. kam gcc 8.3 zum Einsatz.

Das war jetzt ein einfacher CPU-Benchmark. Interessant wäre ein IO-lastiger Benchmark. Die Ultra 45 wird nicht kampflos aufgeben. Gerade die Bus-Architektur ist beim rpi4 immer noch Schrott, daher könnte die Ultra 45 hier punkten. Fortsetzung folgt (irgendwann).

Autor: Olaf | 0 Kommentare | Tags: benchmark, rpi, sun, fujitsu, x86, arm, sparc

Meine Lektionen aus der Entwicklung von dav 1.3

23. December 2019

Die Entwicklung von dav 1.3 ist ganz und gar nicht so verlaufen wie ich mir das vorgestellt hatte. Über ein Jahr später als geplant, konnte ich das Release dann vor kurzem endlich fertig stellen. Ich entwickel das jetzt allerdings im meiner Freizeit, daher variiert es natürlich, wie viel Zeit ich in die Entwicklung stecke. Trotzdem muss ich zugeben, dass die Zeit, die ich investiert habe, nicht unbedingt optimal genutzt wurde.

Schauen wir uns zunächst ein paar Zahlen der Entwicklung an.

version  date        loc          new-loc  n-month  lines/month
---------------------------------------------------------------
begin    2012-11-30      0 lines  0        0        0
1.0.0    2017-08-13  14633 lines  14633    57       256
1.1.0    2017-10-07  15416 lines  783      2        391      
1.2.0    2018-06-26  20465 lines  5049     8        631
1.3.0    2019-12-15  29743 lines  9278     18       515

Für die erste Version von dav habe ich natürlich relativ lange gebraucht. Es ist vermutlich deutlich einfacher, an eine bestehende Software, die man auch noch gut kennt, neues Zeug ranzubasteln, als etwas komplett neues aus dem Boden zu stampfen.

Bei der Entwicklung von dav 1.2 war ich am produktivsten. Aber wenn man nur nach lines/month geht, dann war dav 1.3 eigentlich kein Desaster (im Vergleich zu den anderen Releases). Was man aber sieht ist, dass sich die Code-Basis drastisch vergrößert hat. Viel zu viele neue Features haben den Weg in das Release gefunden. Genau das ist eines der Probleme.

Das größte Ärgernis dabei war, dass ich zwar relativ zügig neue Features implementiert hatte, aber sobald die minimal irgendwie funktioniert haben, bin ich zum nächsten neuen Feature übergegangen. Am Ende hatte ich haufen Neues, das aber nicht gut funktionierte.

Dazu kam auch ein größeres Refactoring der dav-sync Codebasis. Dieses hat natürlich bereits bestehende Funktionalität, die fehlerfrei lief, auch etwas zerstört.

Am Ende war ich Monatelang damit beschäftig, massenweise Tests zu schreiben, um überhaupt wieder Vertrauen in den Code zu entwickeln. Dies war jedoch recht erfolgreich, die Grundfunktionalität dürfte stabiler als vorher sein.

Was ich hoffentlich daraus gelernt habe: Dinge zuende entwickeln. Keine halben Sachen liegen lassen. Und am besten schließt man die Entwicklung eines neuen Features ab, in dem man diverse Tests dafür geschrieben hat, die alle problemlos durchlaufen.

Auch sollte man sich nicht zu viel auf einmal vornehmen. Ich hoffe ich kann den Spruch Release early, release often in Zukunft mehr anwenden. Das hat durchaus seine Vorteile.

Autor: Olaf | 0 Kommentare | Tags: dav, c

Fulltext Search Engines Teil 2: Apache Lucene Howto

22. December 2019

Um Apache Lucene mal auszuprobieren, habe ich meine Blog-Software, die auch in Java geschrieben ist, um eine Volltextsuche erweitert. Dies war mit recht wenig Handgriffen erledigt. Daher gibts jetzt hier ein paar Code-Snippets, um eine minimale Volltextsuche mit Apache Lucene zu implementieren.

Maven-Dependencies:

<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-core</artifactId>
    <version>8.3.1</version>
</dependency>
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-queryparser</artifactId>
    <version>8.3.1</version>
</dependency>

Zunächst benötigt Lucene ein Directory. Dies ist eine abstrakte Klasse für den Zugriff auf den Datenspeicher, den Lucene benutzt. Hier kann man sich dann für eine Implementierung entscheiden, z.B. FSDirectory, wenn Lucene seine Indexdaten im Dateisystem speichern soll.

Directory fsIndex;

...

fsIndex = FSDirectory.open(new File("/var/blogindex/").toPath());

Benötigt wird auch ein Analyzer, der benutzt wird, um Text zu analysieren und in einzelne Tokens zu teilen. Hier habe ich den StandardAnalyzer benutzt.

StandardAnalyzer analyzer;

...

analyzer = new StandardAnalyzer();

Jetzt ist die grundlegende Initialisierung auch schon abgeschlossen und wir können Dokumente zum Index hinzufügen. Meine Blog-Software hat eine Klasse Article mit den Gettern getTitle(), getTags(), getContent() und getID(). Diese drei Attribute möchten wir auch als Felder in einem Lucene-Document speichern. Hierfür benötigen wir zunächst einen IndexWriter.

IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
IndexWriter writer = new IndexWriter(fsIndex, indexWriterConfig);

Dann erstellen wir ein Lucene Document.

Document document = new Document();
document.add(new TextField("title", article.getTitle(), Field.Store.YES));
document.add(new TextField("tags", article.getTags(), Field.Store.YES));
document.add(new TextField("content", article.getContent(), Field.Store.NO));
document.add(new StringField("id", Integer.toString(article.getID()), Field.Store.YES));

Mit Field.Store.YES kann man angeben, dass der Originalwert im Index gespeichert werden soll. Bei Field.Store.NO wird dieser nicht gespeichert. Des Weiteren nutze ich hier auch verschiedene Feld-Typen. Der Unterschied zwischen TextField und StringField ist, dass beim TextField der Inhalt analysiert, also tokenized wird. Beim Inhalt des Blog-Artikels ist dies natürlich wichtig, bei der ID hingegen will man das Gegenteil. Die ID brauchen wir zum Einen, um aus dem Lucene-Index dann den passenden Artikel in der SQL-Datenbank zu finden, zum anderen muss die ID aber auch in Lucene indiziert sein, damit wir Documents aus dem Lucene-Index entfernen oder updaten können.

Es gibt noch jede Menge andere Feld-Typen, z.B. für numerische Werte, oder Werte, die nicht indiziert, aber gespeichert werden sollen. Siehe Field.

Jetzt wo wir das Document erstellt haben, kann dies zum Index hinzugefügt werden. Hierfür gibt es die IndexWriter-Methode addDocument(). Ich benutze hingegen updateDocument(), die das Dokument vorher löscht, falls es vorhanden ist. Dies ist wichtig, da beim Update von Blog-Artikeln somit im Lucene-Index nicht mehrere Documents für den Artikel gespeichert werden.

writer.updateDocument(new Term("id", Integer.toString(article.getID())), document);

Abschließend ist es wichtig, die Methode commit() des IndexWriter aufzurufen. Ebenfalls können wir die close() Methode aufrufen, da wir mit dem Indizieren eines Artikels fertig sind.

writer.commit();
writer.close();

Nun können wir uns noch Anschauen, wie man den Index durchsuchen kann. Hierfür benötigen wir einen IndexReader und IndexSearcher.

IndexReader indexReader = DirectoryReader.open(fsIndex);
IndexSearcher searcher = new IndexSearcher(indexReader);

Außerdem wird ein Query für die Suche benötigt. Hierfür gibt es unzählige Möglichkeiten, diesen zu erstellen. Ich habe mich für MultiFieldQueryParser entschieden, da dieser einen Query erstellen kann, der mehrere Felder durchsuchen kann. Diese Klasse gehört nicht mehr zur Lucene Core Lib, sondern zur Queryparser Lib.

String[] fields = {"title", "tags", "content"};
Query query = new MultiFieldQueryParser(fields, analyzer).parse(queryString);

Jetzt können wir mit dem Searcher und dem Query Dokumente suchen:

TopDocs topDocs = searcher.search(query, numResults);
for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
    Document doc = searcher.doc(scoreDoc.doc);

    // TODO: get fields from doc
}
indexReader.close();

Aus dem Document kann man dann die gespeicherten Werte der Fields wieder rausholen.

Die Suchfunktion lässt sich hier im Blog jetzt nutzen, in dem ein Parameter q=query an die URL angehängt wird. Ein Beispiel. Ist noch nicht besonders schön, aber das war jetzt auch nur schnell ran gefrickelt und ich habe eh vor, die Blogsoftware demnächst komplett neu zu schreiben.

Autor: Olaf | 0 Kommentare | Tags: apache, lucene, java, blog

Fulltext Search Engines Teil 1: Lucene, Solr und Elasticsearch

21. December 2019

Lucene

Apache Lucene ist ein Fulltext Search Engine, die als Java-Bibliothek in eigene Programme eingebunden werden kann.

Lucene arbeitet mit Dokumenten, die verschiedene Felder besitzen können. Felder haben einen Namen und einen Wert. Diese Dokumente können dann zu einem Index hinzugefügt werden. Die Suche nach Dokumenten kann sich dabei auf mehrere Felder beziehen.

Lucene bietet dabei sehr umfangreiche Suchmöglichkeiten. Unterstützt wird außerdem auch ein Ranking der Suchergebnisse.

Im Prinzip lässt sich sagen, dass Apache Lucene die Feature-Messlatte für Open-Source-Volltextsuchen angibt.

Apache Solr

Apache Solr ist ein sehr verbreiteter Suchserver, der auf Apache Lucene aufbaut und den Funktionsumfang davon weitgehend über eine REST-Schnittstelle zur Verfügung stellt. Apache Solr kann man auch als Cluster betreiben und erhält damit ein skalierbares und hochverfügbares System.

Während Lucene nur einfachen Text indizieren kann, ist es mit Apache Solr möglich, verschiedene Dokumententypen zu verarbeiten. Hierfür gibt es verschiedene Content Handler, die Text aus unterschiedlichen Dateitypen extrahieren können.

Der Zugriff auf die REST-Schnittstelle kann man mit Tools wie curl oder anderen HTTP-Clients erfolgen. Es gibt allerdings auch diverse Client-Libs für alle möglichen Programmiersprachen.

Elasticsearch

Genau wie Apache Solr ist Elasticsearch ein Suchserver auf Lucene-Basis. Allerdings handelt es sich um eine kommerzielle Software, wobei ein Teil auch unter einer Open Source Lizenz steht.

Elasticsearch kann ebenfalls als verteilte Searchengine betrieben werden. Zusätzlich bietet es auch diverse Analysewerkzeuge.

Fazit

Mit Apache Lucene steht einem eine sehr mächtige Volltextsuche zur Verfügung, die sich auch recht leicht verwenden lässt (dazu mehr in einem weiteren Artikel). Die selbe Funktionalität steht einem dann auch in größerer Dimension mit Apache Solr und Elasticsearch zur Verfügung.

Autor: Olaf | 0 Kommentare | Tags: apache, lucene, solr, java, db
Weiter