Einleitung: Der Mythos der Geschwindigkeit regulärer Ausdrücke
\n\nWenn Sie schon einmal in C# programmiert und reguläre Ausdrücke verwendet haben, sind Sie sicherlich auf das Dilemma gestoßen: Soll man \\d oder [0-9] zum Suchen von Ziffern verwenden? Auf den ersten Blick gibt es keinen Unterschied – beide finden Ziffern. Aber erfahrene Entwickler, die die Leistung bis zur Perfektion polieren, wissen: \\d kann deutlich langsamer sein als [0-9].
Auf Stack Overflow hat diese Frage tausende Aufrufe und Dutzende Kommentare gesammelt. Heute werden wir die Antwort nicht nur nacherzählen, sondern einen Blick unter die Haube der .NET Regex-Engine werfen und verstehen, warum das passiert und wie es Ihren Code beeinflusst. Wir schreiben echte Benchmarks in C#, schauen uns den IL-Code an und erfahren, wann wir Alarm schlagen sollten und wann wir entspannt sein können.
\n\nBereit? Los geht's!
\n\nWas sind \\d und [0-9] in C#?
\n\nZunächst die Grundlagen. In den meisten Implementierungen regulärer Ausdrücke (einschließlich C#) ist \\d eine Abkürzung für "jede Ziffer". Und [0-9] ist eine Zeichenklasse, die explizit den Bereich von '0' bis '9' auflistet.
Es scheint dasselbe zu sein. Aber der Teufel steckt, wie immer, im Detail.
\n\nUnicode vs ASCII
\n\nIn .NET (ab .NET Framework 4.5 und höher) entspricht \\d standardmäßig jedem Zeichen aus der Unicode-Kategorie Nd (Number, Decimal Digit). Dies sind nicht nur die arabischen Ziffern 0-9, sondern auch:
- \n
- Devanagari-Ziffern (०१२३...) \n
- Tamilische Ziffern \n
- Ostarabische Ziffern \n
- Und Dutzende anderer Zahlensysteme \n
Und [0-9] ist ein fest definierter Bereich von ASCII-Codes (48-57). Nur 0,1,2,3,4,5,6,7,8,9. Kein Unicode.
Warum ist die Geschwindigkeit unterschiedlich? Ein Blick unter die Haube
\n\nJetzt zum Hauptpunkt: Warum ist \\d langsamer? Lassen Sie uns das auf Engine-Ebene analysieren.
1. Anzahl der Prüfungen
\n\nWenn die Engine auf [0-9] trifft, macht sie eines: Sie prüft, ob der Zeichencode im Bereich 48-57 liegt. Das ist ein Vergleich (oder zwei, wenn man die Grenzen berücksichtigt).
Wenn die Engine auf \\d trifft, muss sie:
- \n
- Die Unicode-Kategorie des Zeichens abrufen (das ist bereits teurer als nur ein int). \n
- Sie mit der Kategorie
Ndvergleichen. \n
Die Unicode-Kategorie ist nicht nur eine Zahl, sondern ein ganzes Flag, das über interne Tabellen berechnet wird. Für lateinische Ziffern ist das Ergebnis gleich, aber der Weg dorthin ist länger.
\n\n2. Caching und JIT
\n\nIn .NET wird Regex in Zwischencode (IL) oder sogar in nativen Code kompiliert (wenn RegexOptions.Compiled verwendet wird). Für [0-9] kann der JIT eine sehr einfache Sequenz von Anweisungen generieren: ein paar Vergleiche und eine bedingte Verzweigung. Für \\d – einen Aufruf der Methode CharUnicodeInfo.GetUnicodeCategory und einen anschließenden Vergleich.
Ein Methodenaufruf verursacht Overhead: Argumente pushen, call, Stack-Prüfung. Selbst wenn die Methode klein ist, ist sie immer noch langsamer als ein eingebauter Vergleich.
\n\n3. Beispiel aus einem echten Benchmark
\n\nSchreiben wir einen einfachen Test in C# mit BenchmarkDotNet (installieren Sie es unbedingt über NuGet, wenn Sie es nachvollziehen möchten):
\n\nusing BenchmarkDotNet.Attributes;\nusing BenchmarkDotNet.Running;\nusing System.Text.RegularExpressions;\n\npublic class RegexBenchmark\n{\n private static readonly Regex _digitPattern = new Regex(@"\\d", RegexOptions.Compiled);\n private static readonly Regex _rangePattern = new Regex(@"[0-9]", RegexOptions.Compiled);\n \n private const string TestString = "Hello123World456!";\n\n [Benchmark]\n public bool DigitPattern() => _digitPattern.IsMatch(TestString);\n\n [Benchmark]\n public bool RangePattern() => _rangePattern.IsMatch(TestString);\n}\n\nclass Program\n{\n static void Main() => BenchmarkRunner.Run<RegexBenchmark>();\n}\n\nErgebnisse (auf .NET