Laden...

Gültiges Datum prüfen: 1 Mio Funktionsaufrufe in 23 Millisekunden

Erstellt von DERKLAUS vor 10 Jahren Letzter Beitrag vor 10 Jahren 19.469 Views
D
DERKLAUS Themenstarter:in
16 Beiträge seit 2014
vor 10 Jahren
Gültiges Datum prüfen: 1 Mio Funktionsaufrufe in 23 Millisekunden

Beschreibung:

Wie prüft man ein Datum auf Gültigkeit? Und wie prüft man z. B. eine importierte Datei mit einigen tausend Datensätzen, ob die Datumsangaben gültig sind? Möglicherweise bietet C# bzw. .NETdafür Funktionen an, die ich noch nicht kenne. Ich habe einige getestet und folgendes festgestellt:

Kommt das Datum als String daher, ist die Performance sowieso schon mal im Keller. Es empfiehlt sich unbedingt, das Datum für die interne Verarbeitung als int32 Jahr, int32 Monat, int 32 Tag vorzuhalten und nur zu Anzeigezwecken in einem String zu verwandeln. Ein Aufruf von dateTime(int, int, int) mit new und die Prüfung try/catch ist aber trotzdem unendlich langsam, ich vermute wegen dem Konstruktoraufruf. Unendlich heißt, um den Faktor einige 1000 mal langsamer.

Daher habe ich eine Methode geschrieben, die das gültige Datum überprüft, und das Ergebnis ist, daß sie auf meinem Notebook für 1 Mio. Funktionsaufrufe 23 Millisekunden benötigt. Das ist unendlich viel schneller als das, was ich sonst noch getestet habe. Dieselbe Methode mit String-Parameter ist je nach Programmier-Logik mindestens 14 mal mal langsamer.

Es heißt zwar (mit Recht), man soll nicht nachprogrammieren, was sich schon in der Bibliothek findet, aber Performance ist nun mal eine Sache, die für sich selbst spricht.

Die Philosophie ist, daß höchstwahrscheinlich 99,9 Prozent der Daten gültig sind. Daher werden nicht alle möglichen Fehler vorweg geprüft, so daß wir nach 20 Programmzeilen allmählich die faulen Eier draußen haben, sondern es wird versucht, die Masse der gültigen Eingaben mit möglichst wenig Programmzeilen abzufangen, um die Methode möglichst schnell zu beenden. Im Prinzip werden nicht gültige Eingaben gar nicht behandelt, sondern fallen mit der Generalanweisung return gueltig=false sozusagen ungeprüft unter den Tisch.

Weil die Methode wirklich extrem schnell ist, will ich sie hier mal vorstellen.

Es werden erst die Monate mit 31 Tagen geprüft (7 von 12 erledigt), dann die Monate mit 30 Tagen (damit sind dann 11 von 12 möglichen schon erledigt). Erst danach kommt mit einer Chance von 1:12 der Februar, der erforderlich macht, das Schaltjahr zu prüfen. Prüfung für das Schaltjahr erfolgt aber nur für einen einzigen Tag, den 29., also in 366 Aufrufen ist nur einer dabei, der diese Prüfung nötig macht. Die steht dann ganz am Schluß und wird praktisch niemals aufgerufen.

Schaltjahr ist 1.: Was durch 4 teilbar ist aber nicht durch 100, das sind auf die nächsten 10 Generationen 100 Prozent der Fälle, denn die zweite Bedingung, Schaltjahr ist, was durch 400 teilbar ist, wird erst im Jahre 2400 wieder eintreten.

Das schöne an der Methode ist, daß sie auch über das Datumsformat keine Vorgaben macht, nicht an länderspezifische Datumsformate gebunden ist etc. etc., eben dadurch, daß man alles mit Int Werten erledigt.

Sobald man sich mit DateTime beschäftigt, sind wegen der Länderspez. Einstellungen und der möglichen Formate böse Fehler möglich, hier sind keine Fehler möglich, weil es sich nur um 3 Int32 Zahlen handelt:


  private bool pruefe_datum(int jahr, int monat, int tag)
        {
            bool gueltig = false;
            if (jahr < 1600 || jahr > 2999) return false;
            switch (monat)
            {
                case 1:
                case 3:
                case 5:
                case 7:
                case 8:
                case 10:
                case 12: if (tag > 0 && tag < 32) gueltig = true;
                         break;
                case 4: 
                case 6: 
                case 9:
                case 11: if (tag > 0 && tag < 31) gueltig = true;
                         break;
                case 2: if (tag > 0 && tag < 29) gueltig=true; 
                        else if (tag == 29)
                        {
                            if (jahr % 4 == 0 && jahr % 100 != 0) gueltig = true;
                            else if (jahr % 400 == 0) gueltig = true;
                        }
                         break;
            }  // switch
            return gueltig;
        } // fu


So wurde die Zeit gemessen:


      static void Main(string[] args)
        {
            Program hprog = new Program();
            long l1;long max=1000*1000;
            Stopwatch uhr1 = new Stopwatch();
            long lzeit1;
            long lzeit2;

            bool korrekt = false;

            uhr1.Reset();
            uhr1.Start();

            for (l1 = 0; l1 < max; l1++)
                korrekt = hprog.pruefe_datum(2005, 31, 1);
            uhr1.Stop();
            lzeit2 = uhr1.ElapsedMilliseconds;
            Console.WriteLine("Millisekunden für eigene Funktion {0}",lzeit2.ToString());

            Console.ReadKey();
        }
  


Datum intern immer mit int32 verwalten
Datum auf Gültigkeit überprüfen
Sehr schnelle Methode zur Datumsprüfung

D
DERKLAUS Themenstarter:in
16 Beiträge seit 2014
vor 10 Jahren

Indexer:

Dazu paßt noch ein sauschneller Indexer.

Damit lassen sich Datumsangaben auf ≤ oder => vergleichen.



            // Es erfolgt keine Prüfung auf Gültigkeit
            // um die Funktion aufzurufen, muß das Datum zuvor geprüft worden sein


       public long datindex(int jahr,int monat, int tag)
        {
            return (long) jahr * 12 * 31 + monat * 31 + tag;
        }


Um die Tage sicher zu unterscheiden, wird das Jahr mit 12*31=372 Tagen gezählt. Dadurch ist der Indexer "sicher".

Addieren und subtrahieren kann man nicht.

Dafür kann man aber ja .NET benutzen.

3.825 Beiträge seit 2006
vor 10 Jahren

Sobald man sich mit DateTime beschäftigt, sind wegen der Länderspez. Einstellungen und der möglichen Formate böse Fehler möglich...

Solange man sich mit DateTime beschäftigt stören einen die verschiedenen Länder nicht, erst wenn man in oder aus String umwandelt.

Der DateTime-Datentyp funktioniert in jedem Land.

Grüße Bernd

Workshop : Datenbanken mit ADO.NET
Xamarin Mobile App : Finderwille Einsatz App
Unternehmenssoftware : Quasar-3

F
10.010 Beiträge seit 2004
vor 10 Jahren

Es heißt zwar (mit Recht), man soll nicht nachprogrammieren, was sich schon in der Bibliothek findet, aber Performance ist nun mal eine Sache, die für sich selbst spricht.

Das ist eine falsche Zusammenfassung der allgemeinen Herangehensweise.

Man soll solange man mit den Vorhandenen Funktionen auskommt nicht wegen microoptimierungen vorschnell Zeit investieren um einmalig evtl 10ms herauszuholen.

Wenn man weis das man Millionen von durchläufen hat, kann man natürlich optimieren.
Aber deine Switche ( und vorallem deine Formatierung ) sind alles andere als glücklich gewählt.

Tag sollte immer über 0 sein und das max lässt sich auch viel einfacher über einen ArrayIndex machen.

D
DERKLAUS Themenstarter:in
16 Beiträge seit 2014
vor 10 Jahren

@BerndFfm

Der string ist fehleranfällig bei der Auswertung (nicht nur Länder, sondern auch die verschiedenen Formate sind Fehlerquellen). Das fängt ja schon an mit Punkt und Komma länderspezifisch.

Daher sollte man meiner Meinung nach für das Datum drei Int32 verwenden, dann hat man diese ganzen Probleme gar nicht. Und kann das Datum dann als String darstellen wie man möchte, soll aber intern damit nicht arbeiten.

Außerdem ist int auf allen Systemen der schnellste Datentyp. String kommt da lange nicht hinterher. Benutzt man noch Bibliotheksfunktionen, um den String zu manipulieren, ist gegenüber int leicht der Faktor 1000 erreicht.

Das hab ich in diesem Zusammenhang gemessen daher der Beitrag.

1000 ist dabei noch sehr untertrieben.

@FZelle

Ich hab gegenüber Schnippseln aus dem Internet, nämlich das Datum zu prüfen, im Vergleich mit meiner Methode Performance-Unterschiede im Bereich einige 10.000 gemessen. Das betraf vor allem den Aufruf mit datetime(int, int, int) = new datetime und try mit catch.

Ob switch, wo man 5 cases durchfallen läßt, langsamer ist als eine if-Anweisung mit 5 || weiß ich nicht. Ich schätze, das dürfte sich nicht sehr viel tun, obwohl switch allgemein nicht als schnell gilt.

Die Vorgabe ist auf meinem Rechner 23 ms für 1.000.000 Funktionsaufrufe.

Jetzt können wir es vielleicht mal sportlich nehmen und du stellst eine Methode rein, die noch schneller läuft. Wäre ja allen damit geholfen.

C
1.214 Beiträge seit 2006
vor 10 Jahren

Daher sollte man meiner Meinung nach für das Datum drei Int32 verwenden, dann hat man diese ganzen Probleme gar nicht.

Intern könntest du das Datum z.B. auch als Unix Timestamp darstellen, dann brauchst nur einen long.

D
DERKLAUS Themenstarter:in
16 Beiträge seit 2014
vor 10 Jahren

Unter Unix Timestamp kann ich mir nichts vorstellen. Dazu ist C# für mich noch zu neu.

Aber mit dieser Methode, die ich oben noch angehängt habe:


    public long datindex(int jahr,int monat, int tag)
        {
            return (long) jahr * 12 * 31 + monat * 31 + tag;
        }

kann ich das Datum nicht nur als long-Index darstellen und damit arbeiten, sondern es auch exakt zurückverwandeln in die ursprünglichen drei Int-Werte. Nämlich indem man eine zweite Methode einsetzt, um das rückabzuwickeln. Obwohl das Jahr mit 31*12 Tagen gezählt wird, bekommt man immer exakt die Ausgangswerte. Was jetzt schneller ist, 3 int oder eine Long zurückzuverwandeln, wird sich nicht viel tun.

Mir geht es allerdings nicht um Mikrosekunden, sonder darum, um etwa eine Datei mit 3500 Datensätzen durchzuforsten, zu sortieren oder sonstwas, sitzt du vor dem Bildschirm und das eiert sekundenlang, paßt es nicht.

Als Anwender darf man keine merkliche Verzögerung bemerken.

Es muß alles flutschen. Darum geht es eigentlich. Verzögerungen im Sekundenbereich sind nicht tolerabel.

S
417 Beiträge seit 2008
vor 10 Jahren

Hallo,

ohne deine Lösung schlecht machen zu wollen, ist das Testszenario doch sehr praxisfremd.
Die 3 int-Werte müssen ja irgendwo herkommen, aus einem Textfile oder Datenbank etc., d.h. sie liegen nicht schon als int vor, es sei denn du speicherst sie in der DB als int und nicht als DateTime, was aber total praxisfremd wäre.

D.h. für einen fairen Vergleich, musst du davon ausgehen dass die Eingaben als String vorliegen. Diesen müsstest du dann erst in deine int-Komponenten zerlegen.

D.h. fair/realistisch wäre folgendes:

for (l1 = 0; l1 < max; l1++)
{
	var parts = "31.01.2005".Split('.').Select(f => int.Parse(f)).ToArray();
	korrekt = pruefe_datum(parts[2], parts[0], parts[1]);
}

Dann wirst du sehen, dass gegenüber der DateTime.TryParse-Methode keine zeitliche Verbesserung stattfindet.

for (l1 = 0; l1 < max; l1++)
{
	DateTime dt;
	korrekt = DateTime.TryParse("31.01.2005", out dt);
}
D
DERKLAUS Themenstarter:in
16 Beiträge seit 2014
vor 10 Jahren

Wenn man den Anwender vor der Tastatur sitzen hat und der gibt irgendein Datum in Echtzeit ein, gibt es kein Problem. Egal welche Methode, Millisekunde.

Die Frage ist, wie man das Datum in der Datei speichert.

Wenn man das nur im string-Format speichert, ist es sicher, daß alle Funktionsaufrufe dieser Datei, sagen wir um das Datum zu prüfen oder die Datei nach Datum zu sortieren, unendlich viel langsamer sind als es sein müßte.

Abhilfe schafft hier, die Datumsangeben neben dem String als drei int32 Felder für Jahr, Monat, Tag vorzuhalten und die direkt zu belegen.

Wichtig ist, daß die Datumsangaben ohne Umwandlung als int direkt verfügbar sind.

Der Faktor ist 1000 oder auch mehr.

Int32 ist das Format.

Oder long, um den Index daraus zu bilden.

Alles was mit strings zusammenhängt ist extrem suboptimal.

S
417 Beiträge seit 2008
vor 10 Jahren

Int32 ist das Format.

Naja, wenn du deine Werte aus der Datei liest, liegen sie aber erstmal als string vor und nicht als int. Das musst du dann so oder so parsen, aber das beziehst du in deinem Szenario nicht mit ein, was natürlich das Ergebnis verfälscht.

D
DERKLAUS Themenstarter:in
16 Beiträge seit 2014
vor 10 Jahren

Die class bzw. die daraus abgeleitete Datei führt:

string datum;
int jahr;
int monat;
int tag;
long datindex; 

string datum wird für die interne Verarbeitung nicht benutzt. Interne Verarbeitung ist Vergleich, sortieren nach Datum, bzw. das Datum prüfen auf Gültigkeit. Bevor man nach Datum sortiert, sollte das Datum geprüft werden.

Ich sage es, weil ich es vorher auch getan habe. Mit string hat man ja die .Net Bibliothek zur Verfügung. Sehr bequem.

Nur im Vergleich zu int32 sind alle diese Funktionen unendlich langsam. Unendlich langsam, sobald mal einige tausend Datensätze am Stück anfallen.

Probier es, der Faktor ist 1000 oder mehr.

C
1.214 Beiträge seit 2006
vor 10 Jahren

So ganz klar ist mir auch nicht, was du machst. Wenn du die Daten eh binär in einer Datei speicherst, kannst du auch DateTime verwenden. Das ist intern auch nur ein long, der die Anzahl der Ticks ab einem bestimmten Datum speichert. Brauchst es ja nicht als String speichern.

49.485 Beiträge seit 2005
vor 10 Jahren

Hallo DERKLAUS,

Die Philosophie ist, daß höchstwahrscheinlich 99,9 Prozent der Daten gültig sind.

unter dieser Voraussetzung unterscheidet sich die Performance deiner Methode von new DateTime kaum. Bei meiner Messung war es weniger als Faktor 2. Ob nun 10 oder 20ms wird kein Mensch merken. Daher würde ich deine Methode in diesem Kontext unter "premature optimization is the root of all evil" einstufen.

sonder darum, um etwa eine Datei mit 3500 Datensätzen durchzuforsten

Selbst wenn alle diese Datumse fehlerhaft wären, würde die Überprüfung mittels new DateTime nur ca. 100ms benötigen.

Wenn du die Daten eh binär in einer Datei speicherst

Korrekt, wenn du eh binär arbeitest, kannst du die DateTimes einfach (binär) Serialisieren. Wenn dann nicht gerade jemand mit dem Hexeditor an der Datei herumpfuscht, werden 100% der Datumse korrekt sein, weil sie es beim Speichern gewesen sein müssen (sonst hätte man sie nicht in einen DateTime gekriegt).

herbivore

3.825 Beiträge seit 2006
vor 10 Jahren

@BerndFfm
Der string ist fehleranfällig bei der Auswertung (nicht nur Länder, sondern auch die verschiedenen Formate sind Fehlerquellen). Das fängt ja schon an mit Punkt und Komma länderspezifisch.

Ich speichere das Datum als DateTime, das ist länder- und kulturunabhängig.
Nur bei Eingabe und Anzeige muss man mal wandeln.

Beschäftige Dich mal mit den DateTime-Formaten und -Funktionen, die können alles das was Du brauchst denke ich.

Grüße Bernd

Workshop : Datenbanken mit ADO.NET
Xamarin Mobile App : Finderwille Einsatz App
Unternehmenssoftware : Quasar-3

T
2.219 Beiträge seit 2008
vor 10 Jahren

Ich denke hier wäre es sinnvoller DateTime direkt mit dem entsprechenden Konstruktor zu verwenden.
Falls dein Datum nicht gültig ist, würde dieser eine Exception werfen.
Diese abzufangen und zum nächsten Eintrag zu springen, ist jetzt ein keine langsame Verarbeitung.
Alternativ, wenn du auch Strings hast und das Format fest ist, kannst du auch DateTime.TryParse verwenden.

Vom Stil her würde ich deinen Code auch nochmal überarbeiten.
Wenn man mit C# arbeitet, sollte man auch einen sauberen Code Stil verwenden.

Aus meiner Sicht hast du dir zwar eine "gute" Validierungsmethode gebaut, diese ist aber nicht wirklich sinnvoll.
Warum du die DateTime Klasse ignorierst, kann ich auch nicht ganz anchvollziehen.

Wenn es dir auch auf Performance im Millisekunden Bereich ankommt, bist du mit C# auch nicht gerade ideal versorgt.
Hier kann dir im schlimmsten Fall der GC auch wieder durch den Stopp des Programms einige Millisekunden klauen.

Dann macht es aber wieder Sinn direkt auf DateTime zu setzen.
Was herbivore vorschlägt ist auch das sinnvollste.

Martin

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

16.807 Beiträge seit 2008
vor 10 Jahren

Zudem gibts freie Projekte wie NodaTime, die deutlich flexibler sind, sicherer und wahrscheinlich auch schneller.

Ich denke also, dass man vor der Nutzung dieses Snippets eher abraten sollte.

H
523 Beiträge seit 2008
vor 10 Jahren

Ich denke hier wäre es sinnvoller DateTime direkt mit dem entsprechenden Konstruktor zu verwenden. Falls dein Datum nicht gültig ist, würde dieser eine Exception werfen.
Diese abzufangen und zum nächsten Eintrag zu springen, ist jetzt ein keine langsame Verarbeitung.

Das würde ich so nicht machen, denn Exceptions kosten [in diesem konkreten Fall] Performance. Und wenn von 1 mio Datensätzen 50.000 ein ungültiges Datum enthalten, wirst Du 50.000 Exceptions bekommen welche erheblich Zeit kosten. TryParse dürfte hier die bessere alternative sein.

Hinweis von herbivore vor 10 Jahren

Die Diskussion über die Performance von Exceptions im allgemeinen und die Frage, was denn nun genau (wie) teuer ist, das Erstellen, das Werfen oder das Fangen einer Exception, habe ich abgeteilt, denn solche allgemeinen Diskussionen gehören nicht in Snippets-Threads. Für diesen Thread ist sowieso nur die Aussage relevant, dass new DateTime (innerhalb eines try/catch) dann viel teuer ist, wenn falsche statt richtige Parameter übergeben wurden und diese Aussage ist wohl unbestritten, auch wenn die Angaben für den Faktor zwischen mehrere Hundert und mehrere Tausend schwanken.

Wenn sehr viele falsche Datumswerte geprüft werden, kann die Laufzeit dadurch tatsächlich unakzeptabel lang werden. Allerdings wäre es ungewöhnlich, sollten bei einer Massenverarbeitung viele falsche Datumswerte auftauchen. Typischerweise stammen Massendaten aus Datenbanken, Serialisierungsdateien oder automatisch generierten Quellen wie Logdateien und sind daher in der Regel alle korrekt. Einzelne falsche Datumswerte - z.B. aktuelle Benutzereingaben - zu prüfen, ist unproblematisch.