The web page of IT Sky Consulting GmbH is now also available English.
RISC und CISC
Vor 20 Jahren gab es einen starken Trend, Mikroprossoren mit RISC-Technologie (reduced instruction set computer), zu bauen. Jeder größere Hersteller wollte so etwas bauen, Sun mit Sparc, Silicon Graphics mit MIPS, HP mit PA-Risc, IBM schon sehr früh mit RS6000, was später, als man auf die Idee kam, das zu vermarkten, als PowerPC rebranded wurde, DEC mit Alpha u.s.w. Zu einer Zeit, als man zumindest in Gedanken noch für möglich hielt Assembler zu programmieren (auch wenn man es kaum noch tat), tat das noch richtig weh. Und Software war doch sehr CPU-abhängig, weil plattformunabhägige Sprache, die es damals selbstverständlich schon lange vor Java gab, einfach wegen des Overheads der Interpretation für viele Zwecke zu langsam waren. So behalf man sich mit C-Programmen mit wahren Orgien an ifdfefs und konnte die mit etwas Glück für die jeweilige Plattform aus CPU und einem UNIX-Derivat kompilieren. Der Wechsel der CPU-Architektur eines Herstellers war damals eine große Sache, z.B. bei Sun von Motorola 680×0 zu Sparc. Und die Assemblerprogrammierung der RISC-CPUs war ein Albtraum, an den sich auch erfahrene Assemblerprogrammierer kaum herangewagt haben. Zum Glück gab es damals schon sehr gut optimierende Compiler für C und Fortran und so konnte man das Thema einem ganz kleinen Personenkreis für die Entwicklung von kleinen Teilen des Betriebssytemkerns und kleinen hochperformanten Teilen großer Libraries überlassen.
Eigentlich sollte RISC ermöglichen, mit derselben Menge an Silizium mehr Rechenleistung zu erzielen, insgesamt also Geld und vielleicht sogar Strom zu sparen. Vor allem wollte man für die richtig coole Server-Applikation, die leider immer etwas zu ressourchenhungrig für reale Hardware war, endlich die richtige Maschine kaufen können. RISC war der richtige Weg, denn so hat man einen Optmierungsschritt beim Compiler, der alles optimal auf die Maschinensprache abbildet, die optimiert dafür ist, schnell zu laufen. Ich wüsste nicht, was daran falsch ist, wenn auch diese Theorie vorübergehend nicht zum Zuge kam. Intel konnte das Problem mit so viel Geld bewerfen, dass sie trotz der ungünstigeren CISC-Architektur immer noch schneller waren. Natürlich wurde dann intern RISC benutzt und das irgendwie transparent zur Laufzeit übersetzt, statt zur Compilezeit, wie es eigentlich besser wäre. Tatsache ist aber, dass die CISC-CPUs von Intel, AMD und Co. letztlich den RISC-CPUs überlegen waren und so haben sie sich weitgehend durchgesetzt.
Dabei hat sich die CPU-Abhängigkeit inzwischen stark abgemildert. Man braucht heute kaum noch Assembler. Die Plattformen haben sich zumindest auf OS-Ebene zwischen den Unix-Varianten angeglichen, so dass C-Programme leichter überall kompilierbar sind als vor 20 Jahren, mit cygwin sogar oft unter MS-Windows, wenn man darauf Wert legt. Applikationsentwicklung findet heute tatsächlich weitgehend mit Programmiersprachen wie Java, C#, Scala, F#, Ruby, Perl, Python u.ä. statt, die plattformunabhängig sind, mittels Mono sogar F# und C#. Und ein Wechsel der CPU-Architektur für eine Hardware-Hersteller ist heute keine große Sache mehr, wie man beim Wechsel eines Herstellers von PowerPC zur Intel-Architektur sehen konnte. Man kann sogar mit Linux dasselbe Betriebssystem auf einer unglaublichen Vielfalt von Hardware haben. Die große Mehrheit der schnellsten Supercomputer, die allermeisten neu vekrauften Smartphones und alle möglichen CPU-Architekturen laufen mit demselben Betriebssystemkern, kompiliert für die jeweilige Hardware. Sogar Microsoft scheint langsam fähig zu sein, verschiedene CPU-Architekturen gleichzeitig zu unterstützen, was lange Zeit außerhalb der Fähigkeiten dieser Firma zu liegen schien, wie man an NT für Alpha sehen konnte, was ohne große finanzielle Zuwendungen seitens DEC nicht aufrechterhalten werden konnte.
Aber nun, in einer Zeit, in der die CPU-Architektur eigentlich keine Rolle mehr spielen sollte, scheint alles auf Intel zu setzen, zum Glück nicht nur auf Intel, sondern auch auf einige konkurrierende Anbieter derselben Architektur wie z.B. AMD. Ein genauerer Blick zeigt aber, das RISC nicht tot ist, sonder klammheimlich als ARM-CPU seinen Siegeszug feiert. Mobiltelefone habe häufig ARM-CPUs, wobei das aus den oben genannten Gründen heute fast niemanden interessiert, weil die Apps ja darauf laufen. Tablet-Computer und Netbooks und Laptops sieht man auch vermehrt mit ARM-CPUs. Der Vorteil der RISC-Architektur manifestiert sich dort nicht in höherer Rechenleistung, sondern in niedrigerem Stromverbrauch.
Ist die Zeit reif und CISC wird in den nächsten zehn Jahren wirklich durch RISC verdrängt?
Oder bleibt RISC in der Nische der portablen stromsparendenen Geräte stark, während CISC auf Server und leistungsfähigen Arbeitsplatzrechnern dominierend bleibt? Wir werden es sehen. Ich denke, dass früher oder später der Vorteil der RISC-Architektur an Relevanz auf leistungsfähigen Servern und Arbeitsplatzrechnern gewinnen wird, weil die Möglichkeiten der Leistungssteigerung von CPUs durch mehr elektronischen Elementen pro Quadratmeter Chipfläche und pro Chip an physikalische Grenzen stoßen werden. Dann die bestmögliche Hardwarearchitektur mit guten Compilern zu kombinieren scheint ein vielversprechender Ansatz.
Die weniger technisch interessierten Nutzer wird diese Entwicklung aber kaum tangieren, denn wie Mobiltelefone mit Android werden Arbeitsplatzrechner mit welcher CPU-Architektur auch immer funktionieren und die Applikationen ausführen, die man dort installiert. Ob das nun plattformunabhängig ist, ob man bei kompilierten Programmen ein Binärformat hat, das mehrere CPUs unterstützt, indem einfach mehrere Varianten enthalten sind oder ob der Installer das richtige installiert, interessiert die meisten Benutzer nicht, solange es funktioniert.
Databases and Immutable Objects
A common approach in functional programming and even a good practice in object oriented programming is to prefer having objects immutable.
Especially in applications with multithreading this is extremely helpful, but in principal it helps having the information flow under control and avoiding unexpected side effects. I do not want to postulate this as a dogma, since there are actually legitimate uses of mutable objects, but it is a good idea to keep in mind what mutability means and implies where it is used and to question if it is really worth it in the particular case.
It is important to differentiate between really immutable objects and those that are handed out via a wrapper or a second interface in an immutable way while still being accessible for changes by those parts of the code that control the mutable interface.
Example:
import java.util.*;
public class A {
List
public void addStr(String s) {
l.add(s);
}
public List
return Collections.unmodifiableList(l);
}
}
Even though the list provided by getList() is by itself immutable, it can still be changed by the addStr(..)-method of A.
The whole story gets more interesting when involving databases.
Database contents are in some way mapped to objects. If this mapping is done via automatisms like OR-mappers or Active-Record or if it is done explicitely by using mechanisms like JDBC or DBI is secondary for the moment. These objects can be based on specifically written code for the particular usage or generic DB-content-objects or just plain old collections, which should be considered as an implementation detail for the moment. Important is that such objects or data structures expressing DB content exist in the software. For simplicity’s sake they will be referred to as objects throughout this text.
Now DB content can change. This can happen due to activities of the software with which we are dealing right now. But there can also be accesses to the database by other software or by an DB administrator which can cause changes in the database content while the software is running. This already results in some complications, because the objects processed by the software are already outdated while being processed. This would be acceptable if there were no caching mechanisms for database content on software and framework level, leaving the caching to the database software. Other than framework and software caches the caches of the database itself can be truly transaction conform. Off course it is good having the DB in terms of network connectivity not too far away from the application, maybe even on the same machine, depending on the access patterns. If the network connectivity is poor, this approach ends up having the cache on the wrong side of the network connection.
If the objects within the software have a life time that is so short that it can be avoided that they are out of sync with the database contents they are representing, things should be fine. This can be achieved by using the transaction isolation of „serializable“ or „phantom-read“ and by discarding all these objects before the end of the transaction. Within the transaction these objects that have been read during the same transaction are guaranteed to be up to date, as a result of the concept of transaction isolation used here. If certain OR-mapping patterns needed to map collections to database contents are not involved it is possible that „repeatable-read“ is already enough to guarantee this. As long as objects are only read from the database (SELECT or READ or FIND) immutable objects work just fine.
Even deleting (DELETE) can be imagined, but in this case it is important to ensure that the corresponding object is not in use in other parts of the software even though the database content represented by it have already been deleted. When ensuring that objects representing database content do not live longer than the corresponding transaction this should be possible to deal with. Also creating new objects (INSERT or CREATE) and the corresponding database content should be possible.
What is most problematic is changing of data (UPDATE). Common OR mapper do this by reading the object, changing it and then saving it, hoping that the framework will figure out the differences and run the appropriate update, possibly multiple updates in conjunction with inserts and deletes. This becomes difficult with immutable objects, because that blocks the typical approach just described. Ways can be found to accomplish this anyway, but they tend to get this done at the expense of the elegance of the framework. Even more important it becomes relevant to deal with the situation that objects expire even within the transaction and need to be replaced by new objects representing the changed database contents.
The provocative question is if updates are at all needed. Off course they are needed, since update is one of the four basic database operations. But it is worth taking a closer look in which cases updates really make sense and in which cases other approaches are attractive alternatives. A classic is an application that is dealing with some kind of bookings and some kind of subjects to which accounts are attached, typically this can be persons or companies or other entities, but we can go more abstract than that. For simplicity we can assume persons as an example. This can be a banking software with accounts, account owners and whatever is needed on top of that. Or a billing system of a phone company that calculates, creates and manages invoices to customers based on their phone usage and subscriptions. It is commonly seen that there is such a booking table to which records can be added, but never deleted or changed. That would falsify the book keeping. For canceling a booking, a cancel-entry can be added which is actually a booking in the reverse direction marked as cancel for the original booking. Maybe there is an archiving process that moves old data to a data warehouse, thus actually performing a delete in the booking table and replacing the old bookings by some summary entry, but that is really a detail not relevant for this article as long as this archiving process is done in a good way not compromising the regular usage of the software and database, maybe by actually taking the system down for maintenance during this archiving process once a year. Usually some kind of balance is needed quite often. In principal this can easily be calculated any time by just adding up the bookings of that particular account. This is in line with normalization requirements. Unfortunately this will slow down software to an extent that it is not useful for any practical purposes, but in theory this approach is extremely beautiful. So a more efficient way of finding balances is needed. Possibly each booking could contain the resulting balance reducing the task to looking for the newest booking of a particular account. That can be dangerous if two bookings generated simultaneously refer to the same previous balance. With transaction isolation of „serializable“ that would not happen, but this might slow down the database quite a bit because it reduces the performance of access to the largest table and thus worsening the bottle neck that might already exist there. It could be solved on database level for example with triggers or materialized views so that each insert to the booking table influences the balance automatically. This should work find for all kinds of accesses to the system by any software as long as it is constrained from doing any delete and update to the booking table. Actually granting only INSERT and SELECT rights to the booking table for the user under which the software is running is a very good idea. It could be seen as a disadvantage that part of the business logic is moved to the database. Maybe this is acceptable by just understanding the balance being the sum of the bookings of the account as some kind of high level constraint which is actively enforced by the database. Another approach would be to provide a functionality in the software that inserts a booking or more likely a set of bookings comprising a transaction and that always keeps the balance updated. This can go wrong if simultaneous bookings for the same account can happen, meaning that a transaction for a second booking start while the transaction of a first booking of the same account has not completed. This seems to be simple to avoid by just providing some queue for each account and working through the queue, but since the transactions usually include bookings on several different account this can become quite messy to implement while still avoiding deadlocks. It must be quite interesting to build such a system in a way that it will work fine even under high load. In any case objects containing the balance of an account tend to age extremely fast so they have to be handled with care as short lived objects within a transaction or even shorter, if that account is touched by bookings to that account in the same tranaction.
On the other hand the table with persons and addresses is commonly kept up to date using updates whenever data changes. People marry, change their phone number or move to another address. But is this really the best approach? In reality moving to another address could be known in advance and maybe it is not a good requirement that this change of address has to be entered on the exact date when the person actually moves. Or who wants to spend his wedding day updating all kind of name and address fields in all kinds of web applications? Ok, this can be cheated on because it is usually not a big deal if the change is done a little bit too late. But for some purposes exact data are really desirable. And then again, is this really the right approach? Sometimes it is required to be able to answer questions like name and address of the customer while booking 23571113a was being performed. Maybe it is a good idea to store address changes as some kind of bookings as well, with a valid-since field. For finding a customer’s current address it is sufficient to look for the newest entry for that particular customer that is not in the future. With this approach new entries need to be created for these changes and they can even be prepared in advance. Even deleting a customer can be accomplished by added a „deleted“-entry with a particular valid-since timestamp to that table.
The beatiful part is that now certain data in the database can be considered immutable since no updates can occur to them. So dealing with them as immutable in the software becomes a valable approach and actually also a must. These objects can even be kept in the system longer than the duration of a transaction. It remains important to be careful and to understand the OR-mapping since complex objects represeting collections can even change due to inserts. An example would be an account object containing the account and all its bookings. Customer address needs to be dealt with in conjunction with the timestamp that was used for acquiring it. So it is a good practice to freeze the „now“-timestamp at the beginning of a large operation and to consistently use it throughout that operation to have a consistent view of the data. If this timestamp gets too old, it might be a good idea to refresh it and to reread all data attached to the timestamp. Other than for bookings the times of address changes usually need not be accurate to the micro second as long as consistency is guaranteed and no weird mixture of old and new addresses ever occurs.
Datenbanken und unveränderliche Objekte
Ein beliebter Ansatz in der funktionalen Programmierung, aber auch teilweise in der objektorientierten Programmierung ist es, Objekte nach Möglichkeit unveränderlich (engl. „immutable“) zu machen.
Speziell für Applikationen mit Multithreading ist das sehr nützlich, aber grundsätzlich erleichtert es auch im Griff zu haben, wie der Informationsfluss ist und unerwartete Seiteneffekte zu verhindern. Ich will das hier nicht als Dogma postulieren, denn es gibt durchaus legitime Verwendungen von veränderlichen Objekten, aber man sollte sich auf jeden Fall bewusst sein, was das bedeutet und ob sich die Veränderbarkeit (engl. „mutability“) in dem Fall überhaupt lohnt.
Unterscheiden muss man noch die echt unveränderlichen Objekte und diejenigen, die nur durch einen Wrapper oder ein zweites Interface unveränderlich weitergegeben werden, obwohl es noch für einen Teil des Codes Zugriff auf ein Interface gibt, das Veränderungen zulässt. Der typische Fall sind zum Beispiel Collections in Java, die in einem Objekt leben und mit Methoden dieses Objekts verändert werden können, aber die nur als immutable gewrappt herausgegeben werden.
Beispiel:
import java.util.*;
public class A {
List
public void addStr(String s) {
l.add(s);
}
public List
return Collections.unmodifiableList(l);
}
}
Obwohl also die mit getList() herausgegebenen Liste selbst immutable ist, kann sie mittels add aus A noch verändert werden.
Interessant wird es aber nun, wenn man eine Datenbank ins Spiel bringt.
Datenbank-Inhalte werden in irgendeiner Weise auf Objekte abgebildet. Ob das nun mit Automatismen passiert oder explizit mit Mechanismen wie JDBC, DBI o.ä. und ob es spezifische Objekte oder generische universelle DB-Content-Objekte oder Collections sind, spielt keine so große Rolle, es gibt solche Objekte oder Datenstrukturen.
Nun können sich Datenbankinhalte ändern. Das kann durch die Software, auf die wir uns hier beziehen, selbst passieren, aber auch von außen. Allein das ist schon eine gewisse Komplikation, wenn man es genau anschaut. Die Objekte, mit denen sich die Software befasst, sind dann nämlich schon veraltet. Das kann so akzeptabel sein, wenn man darauf verzichtet, auf Software- oder Frameworkebene ein Caching von solchen Objekten aufzubauen und das Caching allein der Datenbank überlässt, die das im Gegensatz zu Frameworks oder anderer Software, die die Datenbank nur nutzt, problemlos transaktionskonform kann. Schön ist es dann natürlich wieder, wenn die DB netzwerkmäßig nicht zu weit von der Applikation weg ist, sonst ist der Cache leider auf der falschen Seite der Netzwerkverbindung…
Wenn also in der Software die Objekte so kurzlebig sind, dass man ausschließen kann, dass sie veralten, dann kann man sagen, dass sie hinreichend mit dem Stand in der Datenbank übereinstimmen. Mit Transaktionsisolation „serializable“ oder „phantom-read“ und dann innerhalb von einer Transaktion sind diese Objekte, die man in derselben Transaktion gelesen hat, garantiert noch aktuell. Wenn nicht bestimmte OR-mapping-Muster für Collections vorkommen, ist das schon bei „repeatable-read“ der Fall. Man lebt also gut mit „immutable“-Objekten, solange diese nur aus der Datenbank gelesen (SELECT oder READ) werden. Auch das Löschen (DELETE) kann man sich noch vorstellen, aber in diesem Fall muss man schon darauf achten, dass das betreffende Objekt nicht noch an zu vielen Stellen der Software in Gebrauch ist, obwohl es in der Datenbank schon gelöscht ist. Wenn man aber sowieso sicherstellt, dass diese Objekte, die Datenbankinhalte darstellen, nicht länger leben als die entsprechenden Transaktionen, dann sollte auch das handhabbar sein. Auch das Erzeugen neuer Objekte und das Anlegen der entsprechenden Daten in der Datenbank (INSERT oder CREATE) ist möglich.
Schwierig wird es aber mit dem Ändern von Daten in der Datenbank (UPDATE). Bei gängigen OR-Mappings funktioniert das so, dass man das entsprechende vorher gelesen Objekt ändert und dann speichert, in der Hoffnung, dass das Framework die Unterschiede herausfindet und ein entsprechendes Update, eventuell auch eine Kombination aus Updates, Inserts und Deletes initiiert. Das wird mit unveränderbaren (immutable) Objekten schwierig. Man kann auch da Wege finden, aber das geht dann leicht auf Kosten der Eleganz des Frameworks und man hat sich auf jeden Fall damit auseinanderzusetzen, dass die Objekte jetzt auch innerhalb der Transaktion veralten und durch solche mit den aktualisierten Inhalten ersetzt werden müssen.
Die provokante Frage ist, ob man das Update überhaupt braucht. Natürlich braucht man das, aber auch hier kann man genauer schauen, welches die Fälle sind, wo das wirklich so ist und wann andere Ansätze besser sind.
Ein klassisches Beispiel ist eine Software, die irgendwelche Buchungen und irgendwelche Personen verwaltet. Das kann eine Bankensoftware sein, wo man Konten hat. Oder ein Billingsystem einer Telefongesellschaft, wo man die Rechnungen für Kunden generiert und verwaltet. Man hat dort oft so eine Buchungstabelle, in der grundsätzlich nur Datensätze eingefügt werden und niemals Daten gelöscht oder verändert werden, wenn wir einmal Fragen der Archivierung sehr alter Daten ausklammern. Dazu hätte man gerne noch einen Kontostand. Den kann man strenggenommen immer berechnen, indem man einfach alle Buchungen für das Konto aufaddiert. Leider ist die Software dann nachher langsam und für keinerlei praktische Zwecke brauchbar, aber theoretisch sehr schön. Man braucht also schon einen effizienten Mechanismus, um Kontostände zu ermitteln. Vielleicht kann jede Buchung den aus ihr resultierenden Kontostand beinhalten und man muss nur nach der neuesten Buchung suchen. Das ist gefährlich, weil zwei etwa gleichzeitig erzeugte Buchungen sich auf denselben Vorgängerkontostand beziehen. Mit Transaction-Isolation „serializable“ wäre das nicht passiert, aber das bremst natürlich die Datenbank schon sehr aus, ausgerechnet bei der größten Tabelle, in der die ganze Arbeit läuft und wo es sowieso schon den Performance-Flaschenhals gibt. Man kann auch auf Datenbankebene eine Lösung implementieren, etwa mit Triggern oder Materialized Views, wo jedes Insert in der Buchungstabelle ein Update des entsprechenden Kontostands bewirkt. Das hat den Vorteil, dass beliebige DB-Zugriffe von beliebiger Software richtig verarbeitet werden, solange man in der betreffenden Buchungstabelle Delete- und Update-Zugriffe unterbindet. Der Nachteil ist aber, dass die Businesslogik sich jetzt auf die Datenbank und die Applikation verteilt. Das kann man akzeptieren, wenn man das als eine Art abstrakten Constraint ansieht, der von der Datenbank „aktiv“ eingehalten wird. Oder man kann es vermeiden, indem man auf Applikationsebene eine entsprechend Funktionalität zum Einfügen einer Buchung einführt, die den Kontostand anpasst. Wiederum wird das zu falschen Ergebnissen führen, wenn man erlaubt, dass eine zweite Buchung für dasselbe Konto eingefügt wird, während schon eine Transaktion für das Einfügen einer Buchung für das Konto läuft. Man sieht also, dass die richtige Lösung dieser Buchungstabelle nicht einfach ist. Wenn dann noch hinzukommt, dass die Buchungen wiederum zu Transaktionen gruppiert sind, also z.B. die Gutschrift auf einem Konto in derselben Transaktion wie die Abbuchung von einem anderen Konto laufen soll, dann wird es schon interessant, wie man das System so baut, dass es absolut zuverlässig und korrekt ist, auch unter Last, und doch auch performant ist. Und in der Applikation muss man nun mit Konto-Objekten, die den Kontostand enthalten, sehr vorsichtig sein, weil dieser Kontostand veraltet, sobald eine Buchung dazukommt.
Die Tabelle oder die Tabellen mit den Personen, Adressen werden gerne mit Updates aktualisiert, wenn sich Daten ändern. Leute heiraten, ändern die Telefonnummer oder ziehen um. Aber ist das wirklich der richtige Weg? In Wirklichkeit weiß man von dem Umzug schon etwas vorher und hat vielleicht am Umzugstag selbst keine Zeit, das zu erfassen. Und in Wirklichkeit muss das System auch Fragen beantworten können, wie den Namen und die Adresse des Kunden zu der Zeit, als Buchung 23571113a stattfand. Vielleicht kann man Adressänderungen auch als „Buchungen“ speichern, mit einem Gültigkeitsbeginn. Dann wird die Adresse für einen Kunden ermittelt, indem man den Adresseintrag für die Kundennummer sucht, der das größtmögliche Gültigkeitsdatum hat, das gerade noch nicht in der Zukunft liegt. Mit dem Ansatz muss man in dieser Tabelle vielleicht tatsächlich nur neue Datensätze einfügen, wenn sich etwas geändert hat. Und wenn ein Kunde gelöscht wird, dann fügt man einen Datensatz ein, der beinhaltet, dass der Kunde ab einem bestimmten Datum „gelöscht“ ist, aber man kann noch bei Bedarf Daten liefern.
Das Schöne ist nun aber, dass Daten, die in der Datenbank „immutable“ sind, also keine Updates erhalten dürfen, in der Software auch problemlos immutable sein können (und sollten) und dass man sie auch langlebiger als eine Transaktion machen kann. Vorsicht ist aber immer geboten, weil komplexe Objekte in der Applikation sich auch allein durch Inserts in einer Datenbanktabelle ändern würden, wenn etwa eine Collection enthalten ist. Man muss also das OR-Mapping genau kennen, egal wie faszinierend diese Automatismen auch sein mögen. Im Fall von einer Kundenadresse muss man die gültige Adresse immer in Verbindung mit einem Timestamp behandeln, der beim ermitteln der gültigen Adresse eingesetzt wurde. Wenn dieser Timestamp veraltet ist, muss man auch die Adresse neu abfragen, wobei es im Gegensatz zu den Buchungen die Adressänderungen selten auf die Mikrosekunde des Umzugszeitpunkts ankommt, solange man die Konsistenz wahrt und nicht in der Woche des Umzugs wilde Mischungen aus der alten und der neuen Adresse verwendet.
Mehrfache Interfaces mit inneren Klassen
Die im Beitrag über Multidispatch angedeutete Möglichkeit, mit inneren Klassen oder Wrappern anderen „equals()“ und „hashCode()“-Methoden anzubieten, sollte vielleicht etwas genauer gezeigt werden. Als Beispiel dient hier einmal Java, aber die Thematik ist auch in Scala, Ruby, Perl, C# und anderen Sprachen vorhanden, weil es gängig ist, dass die Hashtabellen gerne mit einer einer bestimmten Kombination aus hashCode() und eqals() zusammenarbeiten. Das zwingt dazu, diese beiden Methoden nach der gängigsten Verwendung in Hashtabellen zu definieren.
Die Idee lässt sich aber auch anwenden, um verschiedene Interfaces mit derselben Klasse zu implementieren.
Das Prinzip sieht so aus:
public interface A {
public int fa(int x);
}
public interface B {
public int fb(int x);
}
public class X implements A {
....
public int fa(int x) {
....
}
private class Y implements B {
public int fb(int x) {
return fa(x) % 999;
}
}
public B getAsB() {
return new Y()
}
}
Man kann also die Implementierung von B in der inneren Klasse Y vorsehen und dabei auf alle Methoden und Attribute der umgebenden Klasse zugreifen. Die Referenz auf die umgebende Klasse ist bei allen Instanzen der (nicht-statischen) inneren Klasse implizit dabei.
Hier ein Beispiel für verschiedene hashCode() und equals-Methoden:
public interface StringPair {
public String getLhs();
public String getRhs();
}
public class StringPairImpl implements StringPair {
private final String lhs;
private final String rhs;
public StringPairImpl(String lhs, String rhs) {
assert lhs != null;
assert rhs != null;
this.lhs = lhs;
this.rhs = rhs;
}
public String getLhs() {
return lhs;
}
public String getRhs() {
return rhs;
}
public int hashCode() {
return lhs.hashCode() * 91 + rhs.hashCode();
}
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (! obj instanceof StringPairImpl) {
return false;
} else {
StringPairImpl other = (StringPairImpl) obj;
return this.lhs.equals(other.lhs) && this.rhs.equals(other.rhs);
}
}
private class IgnoreCase implements StringPair {
public String getLhs() {
return lhs;
}
public String getRhs() {
return rhs;
}
public int hashCode() {
return lhs.toUpperCase().hashCode() * 91 + rhs.toUpperCase().hashCode();
}
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (! obj instanceof StringPairImpl) {
return false;
} else {
StringPairImpl other = (StringPairImpl) obj;
return this.lhs.toUpperCase().equals(other.lhs.toUpperCase())
&& this.rhs.toUpperCase().equals(other.rhs.toUpperCase());
}
}
}
public StringPair getWithCaseIgnored() {
return new IgnoreCase();
}
}
Die innere Klasse bietet also eine Implementierung desselben Interfaces, aber hashCode() und equals() ignorieren diesmal Groß- und Kleinschreibung.
Eine andere Möglichkeit wäre es bei Collections, eine immutable-Variante über eine innere Klasse anzubieten und eine getImmutable()-Methode einzubauen. Das ist aber bekanntlich nicht der Weg, den die JDK-library gewählt hat.
Was man aber dabei auch sieht: Leider muss man sehr viel „unnötigen“ Code schreiben, der eigentlich offensichtlich ist, aber eben doch nicht fehlen darf. Das erschwert sowohl das Schreiben als auch das Lesen des Codes, vor allem aber die Wartbarkeit. So wird die an sich gute Idee der inneren Klassen teilweise wieder verwässert.
Snapshot too old – Behandlung langlaufender SELECTs
Wer größere Datenbank-Applikationen entwickelt, wird sich mit dem Problem auseinandersetzen müssen, was bei langlaufenden Abfragen eigentlich passiert.
Man hat also ein „SELECT“ am laufen, dass mehrere Sekunden oder sogar Minuten dauert, vielleicht sogar eine Stunde. Das kann durchaus sinnvoll sein, aber es lohnt sich natürlich, dieses mit gutem Wissen über die Datenbank-Software genau anzuschauen und zu optimieren.
Trotz aller Optimierungen muss man aber prinzipiell davon ausgehen, dass sich die Daten während des Lesezugriffs ändern. Es finden dauernd Schreibzugriffe statt, die natürlich erst nach Abschluss der Transaktion mit einem „COMMIT“ wirklich sichtbar werden. Nun können aber während des Lesezugriffs jede Menge Transaktionen stattfinden. Würde man diese berücksichtigen, müsste der Lesezugriff dauernd von vorne anfangen oder er würde inkonsistente Daten zurückliefern, die von verschiedenen Ständen der Datenbank stammen.
Die Lösung ist, den Stand der Datenbank vom Beginn des Lesezugriffs quasi einzufrieren und für die Dauer des Lesezugriffs zur Verfügung zu stellen. Zugriffe, die später beginnen, bekommen einen anderen Stand zu sehen, auch wenn sie früher fertig werden. Im Fall von Oracle-Datenbanken werden hierfür sogenannte Snapshots verwendet. Es werden also ähnliche Mechanismen wie für Transaktionen verwendet, um mehrere Stände der Datenbank parallel zu speichern.
Das bedeutet aber, dass bei einer sehr aktiven Datenbank immer größere Unterschiede zwischen den beiden Ständen entstehen. Bei vielen Transaktionen oder vielen parallelen langlaufenden Lesezugriffen sind sogar oft viel mehr als zwei Stände. Dafür gibt es Bereiche in der Datenbank, sogenannte Rollback-Segmente. Diese muss man definieren und bereitstellen und wenn sie zu klein sind, laufen sie irgendwann über. Dann kommt es im Fall von Oracle zu dem Fehler „ORA-01555: Snapshot too old“ und der lange Lesezugriff scheitert. Wenn man Pech hat nach ein paar Stunden. Wenn es ein Zugriff durch eine Software ist, wird es interessant, ob die Fehlerbehandlung für diesen Fall funktioniert oder ob es eine obskure „Exception“ in der Log-Datei gibt, die niemand bemerkt.
Mehr dazu findet man unter anderem hier:
Grundsätzlich muss diese Fragestellung bei allen ernsthaften transaktionalen Datenbanken gelöst werden und es ist sicher interessant, wie das im Detail aussieht. Vielleicht ist bei anderen Datenbanken so etwas wie eine „Lese-Transaktion“ („read transaction“) die Antwort. Als Isolationslevel muss man mindestens „repeatable read“ oder „serializable“ wählen. Wenn man mit niedrigeren Isolationsleveln arbeitet, muss man genau wissen, was man tut, da es sonst überraschende Fehler gibt.
Bei PostgreSQL muss man das mit
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
oder
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
explizit einstellen, sonst wird „READ COMMITTED“ gewählt.
Bei mariaDB und mysql hängt es von der DB-Engine ab. InnoDB verwendet als Default „REPEATABLE READ“, DB2 scheint eher auf „READ COMMITTED“ zu setzen, MS-SQL-Server auch auf READ COMMITTED.
Höhere Isolation-Level bedingen natürlich auch das Risiko von Verklemmungen („Deadlocks“), was erkannt werden sollte und zum Rollback einer der beiden Transaktionen führen sollte.
Es lohnt sich also, bei anspruchsvollen DB-Applikationen genau zu schauen, wie man mit diesen Transaktion-Isolationen umgehen will und ob man mit den Default-Eintstellungen der verwendeten Datenbank-Software zufrieden ist.
Vor einigen Jahren habe ich übrigens feststellen müssen, dass man diese niedrigeren Isolationslevel bei Oracle wohl einstellen konnte, dass die Datenbank oder der JDBC-Treiber sich dann aber fehlerhaft verhalten haben. Man sollte also auch herausfinden, ob die entsprechende Eintstellung von der verwendeten Datenbanksoftware unterstützt wird und zwar nicht nur auf dem Papier, sondern real und absolut zuverlässig. Eine unzuverlässige transaktionale Datenbank kann man billig durch einen Zufallszahlengenerator ersetzen.
2014
Καλή Χρονια — Feliz ano novo — Gelukkig nieuwjaar — Šťastný nový rok — Boldog új évet — عام سعيد — bun di bun an — Onnellista uutta vuotta — С новым годом — 新年好 — Godt nytt år — Feliĉan novan jaron — Hääd uut aastat — Ath bhliain faoi mhaise — Akemashite omedetô — Срећна нова година — Gullukkig niuw jaar — Szczęśliwego nowego roku — laimīgu jauno gadu — Srechno novo leto — Gott nytt år — Bonne année — Щасливого нового року — Godt nytår — سال نو مبارک — Un an nou fericit — Felix sit annus novus — Felice anno nuovo — Feliz año nuevo — Sretna nova godina — Laimingų naujųjų metų — Šťastný nový rok — Frohes neues Jahr — Gleðilegt nýtt ár
OR-mappings: Hibernate & Co.
Die meisten Software-Projekte verwenden eine Datenbank. Es ist immer eine Herausforderung, die Datenbank und die Software zusammenzubringen, weil das zwei verschiedene Welten sind, die ihre eigene Entwicklung genommen haben.
Hierzu haben sich ganz verschiedene Ansätze etabliert.
- DB-zentrisches Vorgehen: Ein starker DBA oder ein starkes DB-Team fängt zunächst an, das Datenmodell zu erstellen. Die Software muss sich daran anpassen. Für die performance-kritischen Zugriffe erhalten die Entwickler Unterstützung von einem DBA.
- Eine Variante davon ist die Verwendung des Active-Record-Patterns, wo die Objekte aus der Datenbankstruktur generiert werden. Dieses Active-Record-Pattern verwendet man bei Ruby on Rails.
- OR-zentrisches Vorgehen: Man verwendet objekt-relationale Mapper und definiert Klassen in seiner Software und ein paar XML-Dateien oder Annotationen dazu und hofft dann, dass der OR-Mapper nicht nur relationale Daten in Objekte und Objekte in relationale Daten wandelt, sondern dass auch noch gleich das Datenmodell und die Zugriffe automatisch generiert werden. Das ist der typische Ansatz bei Hibernate, JDO, Eclipselink etc.
- Auch in der Rails-Welt versucht man es oft und gerne ohne DBA und lässt das DB-Schema generieren.
- Man verzichtet auf die relationale SQL-Datenbank und arbeitet mit NoSQL-Datenbanken, die oft etwas näher an der Software liegen
Letztlich muss man sich aber die Fragen beantworten, ob man die Datenbank in den Mittelpunkt stellt und die Software passend zur Datenbank entwickelt oder ob man die Software in den Mittelpunkt stellt und die Datenbank nur als eine Art Persistenzmechanismus dafür verwendet.
Dann kommt die zweite Frage ins Spiel, wie man die objektorientierte, prozedurale und funktionale Welt der Software mit der relationalen Welt der SQL-Datenbanken oder der jeweiligen Struktur der NoSQL-Datenbanken zusammenbringt.
Eine dritte Frage taucht auf, ob man auf Softwareebene Caching-Mechanismen einbaut. Das muss man nicht, aber Hibernate verspricht einem so etwas gut zu können und auch bietet sogar Caching auf der Platte an, nicht nur im Memory.
Die erste Frage ist legitim und kennt wohl keine allgemeingültige Antwort. Man kann durchaus von einer Datenbank im Mittelpunkt ausgehen, wenn diese von mehreren Applikationen genutzt wird oder wenn man davon ausgeht, dass die Daten wesentlich langlebiger als die Software sind. Das könnte bei einer Kundendatenbank so sein. Oft behilft man sich mit dem Versprechen, dass es eine Export-Funktionalität gibt und wenn man dann irgendwann die Software umstellen will, kann man die Daten als XML exportieren und gleich in die neue Software übernehmen. Das kann Routine sein, ich wäre aber skeptisch und würde diese Migration sorgfältig planen und testen. Wahrscheinlich wird es nicht so einfach und so billig, wie man gehofft hat.
Die zweite Frage ist auch recht relevant. Man hat ja nicht nur Objekte in Relationen zu übersetzen, sondern muss sich auch mit Transaktionen auseinandersetzen und mit der Frage, wie tief man große Objekte mit vielen Unterobjekten wirklich lesen und schreiben will. Hier werden gute Antworten schwierig. Liest man sie nur in kleine Portionen, stößt man später auf Probleme, weil Unterobjekte fehlen, z.B. LazyLoadingException in Hibernate. Man muss hierfür eine Lösung finden und diese konsequent umsetzen. Ein radikaler Ansatz, der etwas umständlich ist, aber recht zuverlässig funktioniert, ist die aggregierten Objekte zu entkoppeln und stattdessen nur Schlüsselfelder zu speichern. Beim Zugriff muss man sie dann explizit mit dem Schlüsselfeld holen und stellt so sicher, dass sie aktuell sind. Hier stellen sich natürlich auch interessante Konsistenzfragen, wenn man Daten aus der Datenbank miteinander kombiniert, die von verschiedenen Zeitpunkten stammen und die deshalb nicht mehr notwendigerweise miteinander konsistent sind.
Ein beliebter Ansatz ist, recht viel in den Hauptspeicher zu laden und darauf zuzugreifen. Damit landen wir bei dem dritten Thema, dem Caching. Eines sollte zunächst einmal klargestellt werden: Wenn man eine relationale SQL-Datenbank benutzt und sich dabei auf das Caching des OR-Mapping-Frameworks verlässt, dann verlässt man die transaktionale Welt. Man handelt sich sehr viele Probleme ein, weil Fragen der Konsistenz nicht mehr einfach handhabbar sind, schon gar nicht auf DB-Ebene. Das lässt sich scheinbar in den Griff bekommen, wenn man sagt, alle Zugriffe laufen über die Applikation und die Datenbank ist nur noch eine Persistenzschicht. Nun laufen aber mehrere Threads gleichzeitig und man stößt dann auch noch in diesen heiklen Bereichen auf Hibernate-Fehler. Eclipselink, JDO, etc. sind da wohl nicht wesentlich anders. Kurz gesagt, das Caching ist für veränderliche Daten bestenfalls kaum handhabbar, aber ich gehe sogar soweit, dass das konzeptionell nicht korrekt ist. Nun kann man natürlich so etwas wie „Stammdaten“ haben, die sich selten verändern. Die lassen sich tatsächlich im Memory halten, wenn es einen Prozess gibt, diesen Cache aufzufrischen, wenn sich die Stammdaten ändern, vielleicht sogar mit einem Neustart der Applikation. Ja, das will niemand zugeben, dass man so etwas heute braucht, aber besser zweimal im Jahr die Applikation neu starten als zwei Monate im Jahr obskure Fehler suchen, die durch unsauberes Caching und Fehler im OR-Mapping-Framework entstehen.
Zusammenfassend kann man sagen, dass diese OR-Mapper durchaus zumindest in der Java-Welt ihren Bereich haben, wo sie nützlich sind. Aufpassen sollte man aber bei sehr großen Tabellen und bei sehr stark miteinander verknüpften Tabellen und vor allem beim Caching von veränderlichen Daten. Hier lohnt es sich, etwas mehr Aufwand zu treiben, denn der scheinbare Komfortgewinn durch den OR-Mapper existiert hier nicht oder nur zu einem unverhältnismäßig hohen Preis.
Weihnachten — Christmas — Jul — Navidad 2013
καλά Χριστούγεννα — Feliz Natal — Zalig Kerstfeest! — Veselé Vánoce — Kellemes Karácsonyi Ünnepeket — ميلاد مجيد — Bella Festas daz Nadal! — Hyvää Joulua! — С Рождеством — 圣诞快乐 — God Jul! — Feliĉan Kristnaskon — Häid jõule — Nollaig Shona Dhuit! — クリスマスおめでとう ; メリークリスマス — Срећан Божић — Prettige Kerstdagen — Wesołych Świąt Bożego Narodzenia — Priecîgus Ziemassvçtkus — Vesele bozicne praznike! — God Jul — Joyeux Noël — З Рiздвом Христовим — Glædelig Jul — کريسمس مبارک — Crăciun fericit — Natale hilare — Buon Natale — Feliz Navidad — Sretan božić — Merry Christmas — Su Šventom Kalėdom — Vesele Vianoce! — Fröhliche Weihnachten — Gleðileg jól

Weihnachtsbaum bei Nacht 1981
Quelle: Wikimedia
© Karl Brodowsky 1981-2013
Carry Bit: How does it work?
Most of us know from elementary school how to add multi-digit numbers on paper. Usage of the carry bit is the same concept, but not for base 10, not even for base 2, but for base 256 (in the old 8-bit-days), base 65536 (in the almost as old 16-bit-days), base 4294967296 (32 bit) or base 18446744073709551616 (64 bit), whatever is the word width of the CPU. Always using powers of two is common today, but it is quite possible that this will change from bits to trits (having three possible values, -1, 0 and 1) in the far future.
I do not think that application development should be dealing with low level stuff like bits and bytes, but currently common programming languages like Java, C, C++, C# and more would not let you get away with that, you have to be aware of the bits underlying their numeric types to some extent. So it is a good idea to spend some effort on understanding this. Unfortunately all of these languages are lacking the carry bit, but it is anyway useful to understand the concept.
I have been writing software since the beginning of the 80es. The computers available to me at that time where 8-bit with a 6502- or 6510-CPU and 1 MHz clock speed. Yes, it was 1 MHz, not 1 GHz. It was possible to program them in some BASIC-dialect, but that was quite useless for many purposes because it was simply too slow. Compiled languages existed, but were too clumsy and too big to be handled properly on those computers, at least the ones that I have seen. So assembly language was the way to go. In later years I have also learned to use the 680×0 assembly language and the 80×86 assembly language, but after the mid 90es that has not happened any more. An 8-bit CPU can add two 8-bit numbers and yield an 8-bit result. For this two variants need to be distinguished, namely signed and unsigned integers. For signed numbers it is common to use 2’s complement. That means that the highest bit encodes the sign. So all numbers from 0 to 127 are positive integers as expected. 127 has the 8-bit representation 01111111. Now it would be tempting to assume that 10000000 stands for the next number, which would be +128, but it does not. Having the highest bit 1 makes this a negative number, so this is -128. Those who are familiar with modular arithmetic should find this easily understandable, it is just a matter of choosing the representatives for the residue classes. But this should not disturb you, if you have no recent experience with modular arithmetic, just accept the fact that 10000000 stands for -128. Further increments of this number make it less negative, so 10000001 stands for -127 and 11111111 for -1. For unsigned numbers, the 8 bits are used to express any number from 0 to 255.
For introducing the carry bit let us start with unsigned integral numbers. The possible values of a word are to
where
is the word width in bits, which would be 8 in our example. Current CPUs have off course 64-bit word width, but that does not change the principle, so we stick with 8-bit to make it more readable. Just use your imagination for getting this to 32, 64, 96 or 128 bits.
So now the bit sequence 11111111 stands for 255. Using an assembly language command that is often called ADD or something similar, it is possible to add two such numbers. This addition can typically be performed by the CPU within one or two clock cycles. The sum of two 8-bit numbers is in the range from 0 through 510 (111111110 in binary), which is a little bit too much for one byte. One bit more would be sufficient to express this result. The workaround is to accept the lower 8 bits as the result, but to retain the upper ninth bit, which can be 0 or 1, in the so called carry bit or carry flag. It is possible to query it and use a different program flow depending on it, for example for handling overflows, in case successive operation cannot handle more than 8 bit. But there is also an elegant solution for adding numbers that are several bytes (or several machine words) long. From the second addition onwards a so called ADC („add with carry“) is used. The carry bit is included as third summand. This can create results from 0 to 511 (111111111 in binary). Again we are getting a carry bit. This can be continued until all bytes from both summands have been processed, just using 0 if one summand is shorter than the other one. If the carry bit is not 0, one more addition with both summand 0 and the carry bit has to be performed, yielding a result that is longer than the longer summand. This can off course also be achieved by just assuming 1, but this is really an implementation detail.
So it is possible to write a simple long integer addition in assembly language. One of the most painful design mistakes of current programming languages, especially of C is not providing convenient facilities to access the carry bit, so a lot of weird coding is used to work around this when writing a long integer arithmetic. Usually 64-bit arithemetic is used to do 32-bit calculations and the upper 32 bits are used for the carry bit. Actually, it is not that hard to recover the carry bit, but it is anyway a bit annoying.
Subtraction of long integers can be done in a quite similar way, using something like SBC („subtract with carry“) or SBB („subtract with borrow“), depending on how the carry bit is interpreted when subtracting.
For signed integer special care has to be taken for the highest bit of the highest word of each summand, which is the sign. Often a so called overflow but comes in handy, which allows to recognize if an additional machine word is needed for the result.
Within the CPU of current 64 bit hardware it could theoretically be possible to do the 64-bit addition internally bit-wise or byte-wise one step after the other. I do not really know the implementation details of ARM, Intel and AMD, but I assume that much more parallelism is used for performing such operation within one CPU cycle for all 64 bits. It is possible to use algorithms for long integer addition that make use of parallel computations and that can run much faster than what has been described here. They work for the bits and bytes within the CPU, but they can also be used for very long numbers when having a large number of CPUs, most typically in a SIMD fashion that is available on graphics devices misused for doing calculations. I might be willing to write about this, if interest is indicated by readers.
It is quite interesting to look how multiplication, division, square roots, cube roots and more are calculated (or approximated). I have a lot of experience with that so it would be possible to write about hat. In short these operations can be done quite easily on modern CPUs, because they have already quite sophisticated multiplication and division functions in the assembly language level, but I have off course been able to write such operations even for 8-bit CPUs lacking multiplication and division commands. Even that I could cover, but that would be more for nostalgic reasons. Again there are much better algorithms than the naïve ones for multiplication of very long integers.