Systemprogrammierung

So etwas muss man ja heute selten machen, Applikationsentwicklung ist eher die Tätigkeit, mit der man sich herumschlägt, wenn man kundenspezifische „Business-Software“ entwickelt oder anpasst. Bei der Systemprogrammierung schreibt man Software, die direkt auf Betriebssystemfunktionen und Hardware zugreift oder Teile von Betriebssystemen im weiteren Sinn, also nicht unbedingt nur Teile des Linux-Kernels oder Kernelmodule, sondern auch so etwas wie ls, mv oder auch Datenbanken und Webserver.

Aber gelegentlich kommt es doch in Projekten vor, dass man solche Kenntnisse einsetzen muss und dann ist es auch gut, sie zu haben. Für ein Projekt in der Vergangenheit, als es darum ging, Serversoftware für Billingsysteme zu entwickeln, die in C für Solaris geschrieben wurde, mit anderen Komponenten kommunizieren sollte und dann auch noch Performance bringen sollte, die ausreicht, um wenigstens jeweils in 24 Stunden mindestens die Daten zu verarbeiten, die durch die Telefonate innerhalb von 24 Stunden angesammelt wurden, was sich noch alle paar Monate verdoppelte.

Auch um bei Fahrkartenautomaten einer großen Bahngesellschaft die betriebssystemnahe Funktionalität zum Betrieb und zur Wartung von Fahrkartenautomaten zur Verfügung zu stellen, war es erforderlich, sich in diesem Bereich bewegen zu können.

Zur Zeit halte ich eine Vorlesung an einer Fachhochschule (ZHAW) in Zürich über Systemprogrammierung. Deshalb wird hier vielleicht auch gelegentlich einmal der eine oder andere Artikel zu Themen aus dem Gebiet auftauchen.

Die Beispielprogramme, die ich zu dem Thema erstelle, sind als Open-Source-Software in github und unter den Bedingungen der GPL v2 für jeden Interessenten verfügbar.

Share Button

Rounding of Money Amounts

Deutsch

Many numerical calculations deal with amounts of money. It is always nice if these calculations are somewhat correct or if we can at least rest assured that we do not loose money due to such inaccuracies. Off course there are calculations that may legitimately deal with approximations, for example when calculating profits as percentages of the investment (return on investment). For this kind of calculations floating point numbers (double, Float or something like that) come in handy, but off course dealing with the numeric subtleties can be quite a challenge and rounding errors can grow huge if not dealt with properly.

It is often necessary to do calculations with exact amounts. It is quite easy to write something like 3.23 as floating point number, but unfortunately these are internally expressed in binary format (base 2), so the fraction with a power of 10 in the denominator needs to be expressed as a fraction with a power of two in the denominator. We can just give it a try and divide x=323_{10}=101000011_{2} by y=100_{10}=1100100_2, doing the math in base 2 arithmetic. We get something like z=x/y=11.0011101011100001010001111010111000010100011110101\ldots_{2}, more precisely z=11.00\overline{11101011100001010001} or as fraction z=11_2+\frac{11101011100001010001_2}{100_2*(100000000000000000000_2-1_2)} or in base 10 z=3_{10}+\frac{964689_{10}}{4_{10}\cdot1048575_{10}}.

It can be seen that such a harmless number with only two digits after the decimal point ends up having an infinite number of digits after the decimal point. Our usual Double and Float types are usually limited to 64 Bits, so some digits have to be discarded, causing a rounding error. Fortunately this usually works well and the stored number is still shown as 3.23. But working a little bit with these floating point numbers sooner or later results like 3.299999999999 or 3.2300000001 will appear instead of 3.23, even when doing only addition and subtraction. When doing larger sums it might even end up as 3.22 or 3.24. For many applications that is unacceptable.

A simple and often useful solution is to use integral numbers and storing the amounts in cents instead of dollars. Then the rounding problem of pure additions and subtractions is under control and for other operations it might at least become easier to deal with the issue. A common mistake that absolutely needs to be avoided is mixing amountInDollar and amountInCent. In Scala it would be a good idea to use different types to make this distinction, so that such errors can be avoided at compile time. In any case it is very important to avoid such numeric types as int of Java that have a weird and hardly controllable overflow behavior where adding positive numbers can end up negative. How absurd, modular arithmetic is for sure not what we want for our money, it is a little bit too socialistic. 😉 Estimations about the upper bound of money amounts of persons and companies and other organizations are problematic, because there can be inflation and there can be some accumulation of money in somebody’s accounts… Even though databases tend to force us to such assumption, but we can make them really huge. So some languages might end up using something like BigInteger or BigNum or BigInt. Unfortunately Java shows one of its insufficiencies here, which makes quite ugly to use for financial applications, because calculations like a = b + c * d for BigInteger appear like this: a = b.{\rm add}(c.{\rm multiply}(d)). The disadvantage is that the formula cannot be seen at one glance, which leads to errors. In principal this problem can be solved using a preprocessor for Java. Maybe a library doing some kind of RPN-notation would possible, writing code like this:

Calculation calc = new Calculation();
calc.push(b)
calc.push(c)
calc.push(d)
calc.add()
calc.multiply()
a = calc.top()

Those who still know old HP calculators (like HP 25 and HP 67 😉 in the good old days) or Forth might like this, but for most of us this is not really cutting it.

Common and useful is actually the usage of some decimal fixed point type. In Java this is BigDecimal, in Ruby it is LongDecimal.
And example in Ruby:

> sudo gem install long-decimal
Successfully installed long-decimal-1.00.01
1 gem installed
....
> irb
irb(main):001:0> require "long-decimal"
=> true
irb(main):002:0> x = LongDecimal("3.23")
=> LongDecimal(323, 2)
irb(main):003:0> y = LongDecimal("7.68")
=> LongDecimal(768, 2)
irb(main):004:0> z = LongDecimal("3.9291")
=> LongDecimal(39291, 4)
irb(main):005:0> x+y
=> LongDecimal(1091, 2)
irb(main):006:0> (x+y).to_s
=> "10.91"
irb(main):007:0> x+y*z
=> LongDecimal(33405488, 6)
irb(main):008:0> (x+y*z).to_s
=> "33.405488"
irb(main):009:0> 

It is interesting to see that the number of digits remains the same under addition and subtraction if both sides have the same number of digits. But the longer number of digits wins otherwise. During multiplication, division and off course during more complex operations many decimal places can become necessary. It becomes important to do rounding and to do it right and in a controlled way. LongDecimal supports the following rounding modes:

ROUND_UP
Round away from 0.
ROUND_DOWN
Round towards 0.
ROUND_CEILING
Round to more positive, less negative numbers.
ROUND_FLOOR
Round to more negative, less positive numbers.
ROUND_HALF_UP
Round the middle and from above up (away from 0), everything below down (towards 0).
ROUND_HALF_DOWN
Round the middle and from above down (towards 0), everything below up (away from 0).
ROUND_HALF_CEILING
Round from the middle onward towards infinity, otherwise towards negative infinity.
ROUND_HALF_FLOOR
Round up to and including the middle towards negative infinity, otherwise towards infinity.
ROUND_HALF_EVEN
Round the middle in such a way that the last digit becomes even.
ROUND_HALF_ODD
Round the middle in such a way that the last digit becomes odd (will be added to long-decimal in next release).
ROUND_UNNECESSARY
Do not round, just discard trailing 0. If that does not work, raise an exception.

Which of these to use should be decided with domain knowledge in mind. The above example could be continued as follows:

irb(main):035:0> t=(x+y*z)
=> LongDecimal(33405488, 6)
irb(main):036:0> 
irb(main):037:0* t.round_to_scale(2, LongDecimal::ROUND_UP).to_s
=> "33.41"
irb(main):038:0> t.round_to_scale(2, LongDecimal::ROUND_DOWN).to_s
=> "33.40"
irb(main):039:0> t.round_to_scale(2, LongDecimal::ROUND_CEILING).to_s
=> "33.41"
irb(main):040:0> t.round_to_scale(2, LongDecimal::ROUND_FLOOR).to_s
=> "33.40"
irb(main):041:0> t.round_to_scale(2, LongDecimal::ROUND_HALF_UP).to_s
=> "33.41"
irb(main):042:0> t.round_to_scale(2, LongDecimal::ROUND_HALF_DOWN).to_s
=> "33.41"
irb(main):043:0> t.round_to_scale(2, LongDecimal::ROUND_HALF_CEILING).to_s
=> "33.41"
irb(main):044:0> t.round_to_scale(2, LongDecimal::ROUND_HALF_FLOOR).to_s
=> "33.41"
irb(main):045:0> t.round_to_scale(2, LongDecimal::ROUND_HALF_EVEN).to_s
=> "33.41"
irb(main):046:0> t.round_to_scale(2, LongDecimal::ROUND_UNNECESSARY).to_s
ArgumentError: mode ROUND_UNNECESSARY not applicable, remainder 5488 is not zero
        from /usr/local/lib/ruby/gems/1.9.1/gems/long-decimal-1.00.01/lib/long-decimal.rb:507:in `round_to_scale_helper'
        from /usr/local/lib/ruby/gems/1.9.1/gems/long-decimal-1.00.01/lib/long-decimal.rb:858:in `round_to_scale'
        from (irb):46
        from /usr/local/bin/irb:12:in `
' irb(main):047:0>

A specialty is that some countries do not use the coins with one of the smallest unit, like 1 cent in the US. In Switzerland the smallest common coin is 5 Rp (5 cent=0.05 CHF). It might be possible to make the bank do a transfer for amounts not ending in 0 or 5, but usually invoices apply this kind of rounding and avoid such exact amounts that could not possibly be paid in cash. This can be dealt with by multiplying the amount by 20, rounding it to 0 digits after the decimal point and divide it by 20 and round the result to 2 digits after the point. In Ruby there is a better way using an advanced feature of LongDecimal called remainder rounding (using the method round_to_allowed_remainders(…) ). Assuming we want to round a number x with n decimal places in such a way that, 10^n\cdot x belongs to one of the residue classes \overline{0} \mod 10 or \overline{5} \mod 10. In this case we are just talking about the last digit, but the mechanism has been implemented in a more general way allowing any set of allowed residues and even a base other than 10. If 0 is not in the set of allowed residues, it may be unclear how 0 should be rounded and this needs to be actually defined with a parameter. For common practical uses the last digit of 0 is allowed, so things work out of the box:


irb(main):003:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_UP).to_s
=> "33.45"
irb(main):005:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_DOWN).to_s
=> "33.40"
irb(main):006:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_CEILING).to_s
=> "33.45"
irb(main):007:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_FLOOR).to_s
=> "33.40"
irb(main):008:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_HALF_UP).to_s
=> "33.40"
irb(main):009:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_HALF_DOWN).to_s
=> "33.40"
irb(main):010:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_HALF_CEILING).to_s
=> "33.40"
irb(main):011:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_HALF_FLOOR).to_s
=> "33.40"

In an finance application the rounding methods should probably be defined for each currency, possible using one formula and some currency specific parameters.

LongDecimal can do even more than that. It is possible to calculate logarithms, exponential functions, square root, cube root all to a desired number of decimal places. The algorithms have been tuned for speed without sacrificing precision.

Share Button

Rundung bei Geldbeträgen

English

Ein Teil der numerischen Berechnungen bezieht sich auf Geldbeträge. Es ist immer ganz schön, wenn diese Berechnungen stimmen oder wenn man zumindest durch solche Ungenauigkeiten kein Geld verliert. Natürlich gibt es Berechnungen, die von ungefähren Zahlwerten ausgehen dürfen, wenn es etwa um Berechnungen von prozentualen Renditen geht. Da können dann auch diese Fließkomma-Zahlen (double, Float oder wie sie auch heißen) zum Einsatz kommen, es muss aber natürlich auf numerische Probleme Rücksicht genommen werden, sonst können sich Rundungsfehler massiv hochschaukeln.

Oft ist es aber notwendig, mit den genauen Beträgen zu rechen. Nun kann man so etwas wie 3.23 sehr schön als Fließkommazahl schreiben, aber leider arbeiten diese Fließkommazahlen intern im Dualsystem und drücken also die Zehntel und Hundertstel in Brüchen mit Zweierpotenzen im Nenner aus. Man kann einmal den Versuch machen x=323_{10}=101000011_{2} durch y=100_{10}=1100100_2 zu teilen, im Dualsystem. Da bekommt man dann z=x/y=11.0011101011100001010001111010111000010100011110101\ldots_{2}, genauer z=11.00\overline{11101011100001010001} oder als Bruch daraus z=11_2+\frac{11101011100001010001_2}{100_2*(100000000000000000000_2-1_2)} oder im Zehnersystem z=3_{10}+\frac{964689_{10}}{4_{10}\cdot1048575_{10}}.

Man sieht also, dass diese unscheinbare Zahl mit nur zwei Stellen nach dem Komma im Dualsystem unendlich viele Stellen nach dem Komma bräuchte. In den üblichen Double oder Float-Typen sind aber nur 64 Bit insgesamt vorgesehen und es werden einige Stellen weggeworfen. Zum Glück funktioniert es meistens und die gespeicherte Zahl wird immer noch als 3.23 angezeigt, aber jeder, der ein bisschen mit diesen Fließkommazahlen hantiert hat, weiß, dass irgendwann einmal etwas in der Art von 3.299999999999 oder 3.2300000001 statt 3.23 herauskommt und dass bei typischen Additions- und Subtraktionsoperationen. Man kann sogar auf 3.22 oder 3.24 kommen, wenn man lange genug herumrechnet. Für viele Applikationen ist das unakzeptabel.

Eine einfache und oft sinnvolle Lösung ist es, mit Ganzzahlen zu rechnen und die Beträge in Cent, Rappen, Pfennigen u.s.w. auszudrücken. Dann ist das Rundungsproblem für reine Additionen und Subtraktionen vollständig gelöst oder zumindest für alle anderen Operationen etwas einfacher handhabbar. Man muss nur in der Software darauf achten, dass man die „Beträge in Cent“ und die „Beträge in Euro“ niemals, wirklich niemals durcheinanderbringt. In Scala würde man verschiedene Typen verwenden, so dass zur compile-Zeit diese Durchmischung unterbunden wird. Wichtig ist natürlich in jedem Fall, dass man nicht so etwas wie int von Java verwendet, wo der Überlauf zu unüberschaubaren Katastrophen führen kann. Und Abschätzungen über die oberen Grenzen des monetären Reichtums von Firmen und Einzelpersonen gezählt in einer potentiell inflationsgefährdeten Währung sind fast immer falsch. Hier zeigt sich, dass Java für Finanzapplikationen schlecht geeignet ist, weil man dann statt a = b + c * d für BigInteger so etwas wie a = b.{\rm add}(c.{\rm multiply}(d)) schreiben muss, was dazu führt, dass man die Formel nicht mehr „sieht“ und deshalb sehr viel mehr Fehler produziert. Eventuell kann man das Problem mit einem Präprozessor lösen oder es mit einer Library, die wenigstens so etwas wie UPN-Notation für die Berechnung ermöglicht, etwas entschärfen. Dann schreibt man etwa so etwas

Calculation calc = new Calculation();
calc.push(b)
calc.push(c)
calc.push(d)
calc.add()
calc.multiply()
a = calc.top()

Wer noch alte HP-Taschenrechner oder Forth kennt, wird das vielleicht mögen, aber für die meisten von uns ist auch das nicht wirklich die Lösung des Problems.

Üblich und sinnvoll ist aber in den meisten Fällen, so ein dezimaler Festkomma-Typ für diesen Anwendungsfall. In Java ist das BigDecimal, in Ruby LongDecimal. Ein Beispiel in Ruby

> sudo gem install long-decimal
Successfully installed long-decimal-1.00.01
1 gem installed
....
> irb
irb(main):001:0> require "long-decimal"
=> true
irb(main):002:0> x = LongDecimal("3.23")
=> LongDecimal(323, 2)
irb(main):003:0> y = LongDecimal("7.68")
=> LongDecimal(768, 2)
irb(main):004:0> z = LongDecimal("3.9291")
=> LongDecimal(39291, 4)
irb(main):005:0> x+y
=> LongDecimal(1091, 2)
irb(main):006:0> (x+y).to_s
=> "10.91"
irb(main):007:0> x+y*z
=> LongDecimal(33405488, 6)
irb(main):008:0> (x+y*z).to_s
=> "33.405488"
irb(main):009:0> 

Interessant ist, dass bei Addition und Subtraktion die Anzahl der benötigten Nachkommastellen gleich bleibt bzw. sich die größere Anzahl von Nachkommastellen durchsetzt. Bei Multiplikation und Division und sowieso bei komplexeren Operationen können aber viele Nachkommastellen entstehen. Da ist es entscheidend, richtig zu runden. LongDecimal kennt die folgenden Rundungsmethoden:

ROUND_UP
Rundet von 0 weg, für positive Zahlen gleich wie ROUND_CEILING
ROUND_DOWN

Rundet zu 0 hin, für positive Zahlen gleich wie ROUND_FLOOR

ROUND_CEILING
Rundet auf, also hin zu größeren, positiveren, weniger negativen Zahlen
ROUND_FLOOR
Rundet ab, also hin zu kleineren, negativeren, weniger positiven Zahlen
ROUND_HALF_UP
Rundet ab der Mitte weg von 0, unterhalb der Mitte zu 0 hin
ROUND_HALF_DOWN
Rundet bis einschließlich der Mitte zur 0 hin, oberhalb der Mitte von der 0 weg.
ROUND_HALF_CEILING
Rundet bis einschließlich der Mitte auf (Richtung unendlich) hin, sonst ab.
ROUND_HALF_FLOOR
Rundet ab der Mitte ab, sonst auf.
ROUND_HALF_EVEN
Rundet die Mitte so, dass dabei die letzte Stelle gerade wird.
ROUND_HALF_ODD
Rundet die Mitte so, dass dabei die letzte Stelle ungerade wird.
ROUND_UNNECESSARY
Rundet gar nicht und wirft eine Exception, wenn die weggerundeten Stellen nicht alles Nullen sind.

Welche man davon anwendet sollte man unbedingt mit den entsprechenden Spezialisten von der „Fachseite“ besprechen oder für den jeweiligen Anwendungsfall herausfinden. Als Fortsetzung vom obigen Beispiel sähe das etwa so aus:

irb(main):035:0> t=(x+y*z)
=> LongDecimal(33405488, 6)
irb(main):036:0> 
irb(main):037:0* t.round_to_scale(2, LongDecimal::ROUND_UP).to_s
=> "33.41"
irb(main):038:0> t.round_to_scale(2, LongDecimal::ROUND_DOWN).to_s
=> "33.40"
irb(main):039:0> t.round_to_scale(2, LongDecimal::ROUND_CEILING).to_s
=> "33.41"
irb(main):040:0> t.round_to_scale(2, LongDecimal::ROUND_FLOOR).to_s
=> "33.40"
irb(main):041:0> t.round_to_scale(2, LongDecimal::ROUND_HALF_UP).to_s
=> "33.41"
irb(main):042:0> t.round_to_scale(2, LongDecimal::ROUND_HALF_DOWN).to_s
=> "33.41"
irb(main):043:0> t.round_to_scale(2, LongDecimal::ROUND_HALF_CEILING).to_s
=> "33.41"
irb(main):044:0> t.round_to_scale(2, LongDecimal::ROUND_HALF_FLOOR).to_s
=> "33.41"
irb(main):045:0> t.round_to_scale(2, LongDecimal::ROUND_HALF_EVEN).to_s
=> "33.41"
irb(main):046:0> t.round_to_scale(2, LongDecimal::ROUND_UNNECESSARY).to_s
ArgumentError: mode ROUND_UNNECESSARY not applicable, remainder 5488 is not zero
        from /usr/local/lib/ruby/gems/1.9.1/gems/long-decimal-1.00.01/lib/long-decimal.rb:507:in `round_to_scale_helper'
        from /usr/local/lib/ruby/gems/1.9.1/gems/long-decimal-1.00.01/lib/long-decimal.rb:858:in `round_to_scale'
        from (irb):46
        from /usr/local/bin/irb:12:in `
' irb(main):047:0>

Eine Besonderheit ist, dass in manchen Ländern die kleinste Münze nicht mehr gebräuchlich ist. In der Schweiz ist zum Beispiel der kleinstmögliche Betrag 5 Rappen (0.05 CHF), nicht 1 Rappen. Nun kann man zwar die Bank dazu bringen, Überweisungen zu machen, deren Endziffer nicht 0 oder 5 ist, aber es ist doch verbreitet, Rechungen auf Vielfache von 5 Rappen zu runden. Man kann das Lösen, indem man den Betrag mit 20 multipliziert, dann auf die gewünschte Art rundet und zum Schluss wieder durch 20 dividiert und das Ergebnis auf 2 Nachkommastellen rundet. In Ruby geht das aber einfacher, weil LongDecimal eine „Restklassenrundung“ kennt. Man kann also sagen, man möchte eine Zahl x mit n dezimalen Nachkommastellen so runden, dass die mit 10^n\cdot x einer der Restklassen \overline{0} \mod 10 oder \overline{5} \mod 10 entspricht. In dem Fall ist es einfach die Endziffer. Man kann eine beliebige Menge von Endziffern vorgeben und sogar die 0 verbieten, wobei dann noch festgelegt werden muss, wie die 0 gerundet werden soll, was die üblichen Rundungsmodi nicht vollständig erklären können. Bei Interesse kann ich darauf noch näher eingehen, es ist aber für diesen praktischen Fall nicht relevant, weil die Endziffer 0 natürlich erlaubt ist. Für diesen praktischen Anwendungsfall funktioniert es also so:


irb(main):003:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_UP).to_s
=> "33.45"
irb(main):005:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_DOWN).to_s
=> "33.40"
irb(main):006:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_CEILING).to_s
=> "33.45"
irb(main):007:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_FLOOR).to_s
=> "33.40"
irb(main):008:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_HALF_UP).to_s
=> "33.40"
irb(main):009:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_HALF_DOWN).to_s
=> "33.40"
irb(main):010:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_HALF_CEILING).to_s
=> "33.40"
irb(main):011:0> t.round_to_allowed_remainders(2, [0, 5], 10, LongDecimal::ROUND_HALF_FLOOR).to_s
=> "33.40"

Man kann nun also in der Finanzapplikation für jede Währung Rundungsmethoden definieren.

LongDecimal kann noch etwas mehr. So ist es möglich, Logarithmen, e-Funktion, Quadratwurzel und Kubikwurzel auf eine gewünschte Anzahl von Nachkommastellen genau zu berechnen und die entsprechenden Algorithmen sind daraufhin optimiert, das Ergebnis möglichst schnell, aber natürlich mit der erforderlichen Genauigkeit zu berechnen.

Share Button

Testbarkeit von Software

Viele haben inzwischen gelernt, dass man Software in erster Linie für den Anwender schreibt und nicht für die Verwendung möglichst cooler Technologie. Das vertrete ich hier auch immer wieder, aber nun kommt doch ein kleiner Gedanke in die Gegenrichtung. Betrachten wir einmal nicht nur die eigentlichen Softwareentwickler, sondern auch Softwaretester, -architekten, diejenigen die die Software später installieren und betreiben, DBAs u.s.w. als das Team das am Ende die Software dem Endanwender zur Verfügung stellt. Ich habe gute Erfahrungen mit dieser Sichtweise gemacht. Es geht jetzt also darum, Software im Interesse der Effizienz dieses Teams, speziell der Softwaretester zu optimieren.

Es gibt es oft die Situation, dass sich reale Szenarien während des Software-Tests nicht beliebig gut nachstellen lassen, weil die umgebende Infrastruktur irgendwo aufhören muss und durch Simulationen ersetzt wird. Die Frage der Testdaten ist auch immer wichtig und oft wird aus Datenschutzgründen der Zugang zu den realen Daten erschwert und es wird mit mehr oder weniger guten synthetischen Daten getestet. Besonders interessant wird es bei Software, die ein Zeitverhalten aufweisen soll, z.B. einen Fahrplanwechsel zu einem bestimmten Zeitpunkt berücksichtigen. Man kann ein eigenes Testnetzwerk mit eigenem Timeserver aufbauen und dann Datum und Uhrzeit manipulieren. Weil so ein Testnetzwerk aufwendig ist, sieht man es nicht überall und wenn man es hat, wird es von vielen Anwendern gleichzeitig verwendet, so dass das Stellen der Uhr koordiniert ablaufen muss und nur wenige Tests möglich sind. Die Uhr bei einzelnen Rechnern zu verstellen ist verlockend, aber doch oft problematisch, weil die Applikation auf verschiedenen Servern läuft und es interessante Effekte gibt, wenn die Uhr nicht überall auf dieselbe Art verstellt wird. Der Timeserver ist ja oft so wichtig, damit alle Server dieselbe Uhrzeit haben und nicht einmal in erster Linie damit die Zeit richtig ist. Man hört, dass Google sogar in den größeren Rechenzentren jeweils eine Atomuhr aufstellt, um die Zeit synchron zu halten, was zwischen verschiedenen Rechenzentren wegen der Latenz der Netzwerkverbindungen gar nicht so einfach ist. Die Zeitstempel in übertragenen, gespeicherten und verarbeiteten Daten sind häufig sehr relevant, um zu entscheiden, in welcher Reihenfolge gewisse Ereignisse erfolgt sind. Für verteilte Applikationen und insbesondere für verteilte Datenbanken ist das sehr wichtig.

Dieses Beispiel zeigt es, dass es sich lohnen kann, bei der Softwareentwicklung die Testbarkeit zu berücksichtigen. Man sollte den Testern halbwegs effiziente und machbare Möglichkeiten bieten, ihre Arbeit zu machen. Software könnte also Wege anbieten, speziell mit der Zeit umzugehen, Zwischenergebnisse abzugreifen oder zu ändern, die im Produktiveinsatz eigentlich gar nicht relevant wären. So kann man die Zeit des Testens nutzbringender einsetzen, mehr Tests machen und mehr Fehler finden. Dass sich diese extrem aufwendigen Tests in einer idealen Testumgebung am Schluss auch noch lohnen, ist unbestritten, aber man bremst die ganze Entwicklung und damit den Teamerfolg aus, wenn man die Hürden für alle Tests zu hoch ansetzt.

Share Button

Datenbankmigrationen

Fast jede größere Software, mindestens serverseitige Software, benutzt Datenbanken. Früher war dies „immer“ Oracle, außer man war in einer sehr Mainframe-lastigen Umgebung, dann war es halt DB2, oder in einer sehr Microsoft-lastigen Umgebung, dann war es MS-SQL-Server.

Das scheint sich jetzt ein bißchen zu ändern. Oracle scheint viele Kunden mit seiner Lizenzpolitik verärgert zu haben und andererseits sind die Alternativen PostgreSQL und verschiedene mySQL-Derivate (z.B. MariaDB) inzwischen auf einem guten Stand, der es erlaubt, viele Aufgaben, für die man früher selbstverständlich auf Oracle-Datenbanken gesetzt hat, damit zu lösen, auch für größere Datenbanken. Mir scheint es immer noch so zu sein, dass man mit mySQL oder MariaDB an Grenzen stößt, und für viele Zwecke PostgreSQL die bessere Wahl ist.

Andererseits kommen auch vermehrt die sogenannten NoSQL-Datenbanken auf, z.B. Riak, Neo4J, MongoDB, Redis und etliche mehr.

Mit der Weiterentwicklung der Software gehen irgendwann auch Änderungen des Datenmodells einher. In der klassischen Welt der SQL-Datenbanken führt man dann eine Migration durch, wo neue Tabellen hinzugefügt werden, vorhandene geändert werden und Daten transformiert werden. Theoretisch geht das zumindest bei kleineren Änderungen, ohne die Applikation abzuschalten, aber für größere Änderungen fährt man doch gerne sicherheitshalber die Applikation so lange herunter, wenn man die Möglichkeit hat, und sei es nur, damit die Migration schneller fertig wird. Idealerweise wird die Software dann natürlich im gleichen Atemzug auch umgestellt und wenn danach alles gut läuft, ist es gut. Wenn man es merkt, dass etwas nicht stimmt, bevor die neue Software Daten angelegt hat, kann man einfach das Backup einspielen und es in der nächsten Nacht noch einmal probieren. Schlimmer ist der Fall, wenn man es erst nach einer Weile merkt, dass die Applikation nicht mehr rund läuft und wenn man dann schon Daten verloren hat oder zumindest in einem Zustand hat, der es nur mit großer Mühe erlaubt, das zu korrigieren. Deshalb empfielt es sich, so eine Migration gut zu testen und zwar nicht nur mit leeren Datenbanken, sondern mit richtigen Daten. Sinnvoll sind anonymisierte Daten aus der Produktivdatenbank.

Ein völlig anderer Ansatz drängt sich bei schemalosen Datenbanken wie MongoDB auf. Man kann bei einer Änderung der Software einfach alle Daten so lassen, wie sie sind, vorausgesetzt, die Software ist in der Lage alte und neue Daten zu unterscheiden. Das kann sich am vorhandensein eines Attributs erkennen lassen, aber auch an einem Versionsattribut. Wenn nun ein Dokument, so heißen die „Datensätze“ in MongoDB, verwendet wird, kann die Software in diesem Moment die Migration mit geeigneten Defaultwerten oder berechneten Werten für neu hinzukommenden Felder für das eine Dokument durchführen. So kann die Migration unter Umständen Tage oder sogar Jahre dauern, aber die Applikation kann abgesehen von der Problematik des eigentlichen Software-Updates durchgängig laufen.

Grundsätzlich ließe sich dieser Ansatz auch für SQL-Datenbanken denken. Man kann zum Beispiel bei der Migration eine leere Tabelle anlegen, die die migrierten Daten einer vorhandenen Tabelle aufnehmen kann und die Daten so nach und nach durch die Applikation transformieren und in die neue Tabelle verschieben. In der Praxis wird das aber kompliziert, wenn man noch so Dinge wie referenzielle Integrität berücksichtigen will. So wird uns der bewährte klassische Ansatz für Datenbankmigrationen wohl noch lange erhalten bleiben.

Share Button

Unicode, UTF-8, UTF-16, ISO-8859-1: Why is it so difficult?

Deutsch

Since about 20 years we have been kept busy with the change to Unicode.

The good thing: We all know that Unicode and usually UTF-8 as representation is the way we should express textual data. The web is mostly UTF-8 today. But it has been a painful path and it still is sometimes.

Why is it so difficult?

The most important problem is that it is hard to tell how the content of a file is to be interpreted. We do have some hacks that often allow recognizing this:
The suffix is helpful for common and well defined file types, for example .jpg or .png. In other cases the content of the file is analyzed and something like the following is found in the beginning of the file:

#!/usr/bin/ruby

From this it can be deduced that the file should be executed with ruby, more precisely with the ruby implementation that is found under /usr/bin/ruby. If it should be the ruby that comes first in the path, something like

#!/usr/bin/env ruby

could be used instead. When using MS-Windows, this works as well, when using cygwin and cygwin’s Ruby, but not with native Win32-Ruby or Win64-Ruby.

The next thing is quite annoying. Which encoding is used for the file? It can be a useful agreement to assume UTF-8 or ISO-8859-1 or ISO-8859-15, but as soon as one team member forgets to configure the editor appropriately, a mess can be expected, because files appear that mix UTF-8 and ISO-8859-1 or other encodings, leading to obscure errors that are often hard to find and hard to fix.

Maybe it was a mistake when C and Unix and libc were defined and developed to understand files just as byte sequences without any meta information about the content. This more or less defined how current operating systems like Linux and MS-Windows deal with it.

They did not really have a choice, although they did both provide facilities that could theoretically be used to carry such information, but since almost no software uses them and there are no well known and established standards, this is not helpful today. In the internet mime headers have proved to be useful for email and web pages and some other content. This allows the recipient of the communication to know how to interpret the content.

It would have been good to have such meta-information also for files, allowing files to be renamed to anything with any suffix without loosing the readability. But in the seventies, when Unix and C and libc where initially created, such requirements were much less obvious and it was part of the beauty to have a very simple concept of an I/O-stream universally applicable to devices, files, keyboard input and some other ways of I/O. Also MS-Windows has probably been developed in C and has inherited this flaw. It has been tried to keep MS-Windows runnable on FAT-file-systems, which made it hard to benefit from the feature of NTFS of having multiple streams in a file, so the second stream could be used for the meta information. But as a matter of fact suffixes are still used and text files are analyzed for guessing the encoding and magic bytes in the beginning of binary files are used to assume a certain type.

Off course some text formats like XML have ways of writing the encoding within the content. That requires iterating through several assumptions in order to read up to that encoding information, which is not as bad as it sounds, because usually only a few encodings have to be tried in order to find that out. It is a little bit annoying to deal with this when reading XML from a network connection and not from a file, which requires some smart caching mechanism.

This is most dangerous with UTF-8 and ISO-8859-x (x=1,2,3,….), which are easy to mix up. The lower 128 characters are the same and the vast majority of the content consists of these characters for many languages. So it is easy to combine two files with different encodings and not recognizing that until the file is already somewhat in use and has undergone several conversion attempts to „fix“ the problem. Eventually this can lead to byte sequences that are not allowed in the encoding. Since spoken languages are usually quite redundant, it usually is possible to really fix such mistakes, but it can become quite expensive for large amounts of text. For UTF-16 this is easier because files have to start with FFFE or FEFF (two bytes in hex-notation), so it is relatively reliable to tell that a file is utf-16 with a certain endianness. There is even such a magic sequence of three bytes to mark utf-8, but it is not known by many people, not supported by the majority of the software and not at all commonly used. Often we have to explicitly remove these three marker bytes in order to avoid confusing software processing the file.

In the MS-Windows-world things are even more annoying because the whole system is working with modern encodings, but this black CMD-windows is still using CP-850 or CP-437, which contain almost the same characters as ISO-8859-1, but in different positions. So an „ä“ might be displayed as a sigma-character, for example. This incompatibility within the same system does have its disadvantages. In theory there should be ways to fix this by changing some settings in the registry, but actually almost nobody has done that and messing with the registry is not exactly risk-less.

Links

Share Button

Plans of UK government

Aparently the UK government plans switch from Microsoft Office to open source:

Share Button

Widersprüchliche Dependencies

Wenn man größere Applikationen entwickelt kommt man irgendwann einmal an den Punkt, wo verschiedene Libraries, die man gerne verwenden möchte, ihrerseits von anderen Libraries abhängen. Da kommt dann auch schonmal dieselbe Library mehrfach vor, nur leider mit unterschiedlichen Versionen. Man probiert dann immer gerne, ob es nicht eine Version gibt, mit der beide funktionieren und manchmal hat man Glück, bis dann eine dritte kommt. Im Prinzip ist es möglich, auch so etwas zu lösen, aber es ist schon konzeptionell nicht so einfach und in der Praxis auch etwas mühsam, wenn es überhaupt gelingt. Zum Glück ist es in vielen Fällen möglich, einfach die neueste der verlangten Versionen zu nehmen, aber manchmal geht das nicht, sei es, weil die Version wirklich überprüft wird und eine bestimmte verlangt wird oder sei es, weil diese beiden Versionen wirklich an einer Stelle inkompatibel sind, die dummerweise noch verwendet wird.

In Java gibt es dafür einen Ansatz, eigene ClassLoader zu verwenden. Hier ein kleines Beispiel, in dem eine Klasse X drei Objekte aus verschiedene Klassen Y instanziiert. Diese Klassen Y sind alle im selben Package, aber in verschiedenen Jars.

1. X.java

import java.net.URL;
import java.net.URLClassLoader;

public class X {
    public static void main(String args[]) {
        try {
            ClassLoader c0 = Thread.currentThread().getContextClassLoader();

            URL cpa = new URL("file:///home/bk1/src/class-loader/A/A.jar");
            URL cpb = new URL("file:///home/bk1/src/class-loader/B/B.jar");
            URL cpc = new URL("file:///home/bk1/src/class-loader/C/C.jar");
        
            ClassLoader ca = new URLClassLoader(new URL[]{ cpa }, c0);
            ClassLoader cb = new URLClassLoader(new URL[]{ cpb }, c0);        
            ClassLoader cc = new URLClassLoader(new URL[]{ cpc }, c0);
            
            Object a = ca.loadClass("Y").newInstance();
            System.out.println("a=" + a + " a.class=" + a.getClass());
            Object b = cb.loadClass("Y").newInstance();
            System.out.println("b=" + b + " b.class=" + b.getClass());
            Object c = cc.loadClass("Y").newInstance();
            System.out.println("c=" + c + " c.class=" + c.getClass());
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }
}

2. A/Y.java

public class Y {
    public String toString() {
        return "Feuer";
    }
}

Dies landed kompiliert in A.jar

3. B/Y.java

public class Y {
    public String toString() {
        return "Wasser";
    }
}

Dies landed kompiliert in B.jar

4. C/Y.java

public class Y {
    public String toString() {
        return "Erde";
    }
}

Dies landed kompiliert in C.jar

Und das ist die Ausgabe:

a=Feuer a.class=class Y
b=Wasser b.class=class Y
c=Erde c.class=class Y

Natürlich sollte man dies bevörzugt Frameworks überlassen, statt es selber zu programmieren. Aber grundsätzlich ist das Problem durchaus lösbar.

Share Button

IT Sky Consulting GmbH web page in English

The web page of IT Sky Consulting GmbH is now also available English.

Share Button

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.

Share Button