RegEx-Performance in SSIS Datenflüssen

Im letzten Beitrag hatten wir empfohlen, reguläre Ausdrücke zu verwenden, wenn Muster in Texten gesucht werden sollen. Dabei hatten wir neben einer kleinen Syntax-Einführung auf eine MSDN-Seite mit Performance-Tipps verwiesen. In diesem zweiten Teil der RegEx-Beiträge möchten wir diese Tipps konkret in den SSIS-Kontext übertragen. Dafür wollen wir zunächst testen, wie hoch der Overhead für die Instanziierung eines RegEx-Objekts im Vergleich zum Aufruf statischer RegEx-Methoden in C# ist. Hinterher möchten wir beide Methoden in einem einfachen SSIS-Datenfluss vergleichen.

Performance-Vergleich

Um zu bemessen, wie hoch der Overhead für die Erzeugung eines RegEx-Objekts ist, möchten wir ein einfaches C#-Programm verwenden. Diesem übergeben wir die Expression und den String in dem gesucht werden soll als Argumente aus der Kommandozeile:

String sRegex = args[0];

String sTestString = args[1];

Console.WriteLine(„Testing {0} for expression {1}“, sTestString, sRegex);

 

Zum Messen der Laufzeit verwenden wir die Stopwatch-Klasse:

Stopwatch sw = Stopwatch.StartNew();

Zunächst erzeugen wir ein RegEx-Objekt, mit dem wir den String parsen:

Regex myExpression = new
Regex(sRegex);

MatchCollection matches = myExpression.Matches(sTestString);

 

Danach stoppen wir die Zeitmessung und geben die vergangene Zeit aus. Die Zeit wird dabei in Ticks gemessen und dann mittels der Frequency-Eigenschaft der Stopwatch in Sekunden umgewandelt:

sw.Stop();

Console.WriteLine(„Tested with new object {0} ticks, that is {1:F6}s“, sw.ElapsedTicks, (double)sw.ElapsedTicks/(double)Stopwatch.Frequency);

 

Dasselbe Vorgehen wählen wir auch für die Verwendung der statischen RegEx-Methoden:

sw = Stopwatch.StartNew();

Regex.Matches(sTestString, sRegex);

sw.Stop();

Console.WriteLine(„Tested with static member call in {0} ticks, that is {1:F6}s“, sw.ElapsedTicks, (double)sw.ElapsedTicks/(double)Stopwatch.Frequency);

 

Das Resultat der Ausführung ist wie folgt:

Testing 2D7F3D0D-2C6A-4079-8EE9-F5C9F6D2F00C for expression (?<=-)(\d{4})(?=-)

Tested with new object 1222 ticks, that is 0,000483s

Tested with static member call in 115 ticks, that is 0,000045s

 

Wir sehen, dass beide Arten der RegEx-Ausführung für diesen einfachen Testfall nicht sehr teuer sind. Da die Ausführung bei Instanziierung eines RegEx-Objekts aber um eine Größenordnung teurer ist, als die Verwendung statischer Methoden, könnte die kumulierte Differenz beider Arten des Parsings bei häufiger Anwendung beispielsweise in einem SSIS Datenfluss durchaus signifikante Auswirkungen auf die Laufzeit haben.

RegEx in SSIS Teil 1: Vorbereitung der Datenbank

In einer Testdatenbank wollen wir uns nun Tabellen anlegen, um die verschiedenen Ausführungsarten von regulären Ausdrücken in einem SSIS Datenfluss zu vergleichen. Dafür legen wir uns zunächst eine Quelltabelle an, in der die Daten liegen werden, die wir abrufen möchten:

if object_id(‚dbo.RegexTestDataSrc‘) is not null drop table dbo.SSISTestDataSrc;
create table dbo.RegexTestDataSrc
(
    Id int not null identity,
    TextCol varchar(150) null,
)

Dann legen wir uns eine Zieltabelle an, diese wird neben der ursprünglichen Textspalte noch eine Spalte enthalten, die unseren RegEx-Treffer enthält sowie eine Spalte, die die für das Parsing benötigte Zeit hält:

if object_id(‚dbo.RegexTestDataTgt‘) is not null drop table dbo.SSISTestDataTgt;
create table dbo.RegexTestDataTgt
(
    Id int not null identity,
    TextCol varchar(150) null,
    Match varchar(4) null,
    SeekTime bigint null

)

Die Quelltabelle füllen wir nun mit 10.000 Zufalls-Strings:

declare @rc int
set @rc = 0
while @rc < 10000
begin
    insert into dbo.RegexTestDataSrc (TextCol) values (NewId())
    select @rc =count(*) from dbo.RegexTestDataSrc
end

Der Funktionsaufruf NewId() liefert dabei eine GUID, also 32 Hexadezimalzeichen zurück, wobei diese im Format 8-4-4-4-12 Zeichen gruppiert sind (zum Beispiel BED9ACF1-FAE5-43C6-82D5-AF9D21552CBC).

RegEx in SSIS Teil 2: Performace-Analyse

Nach Ausführen dieser SQL-Befehle sind in der Quelltabelle 10.000 GUIDs hinterlegt. Diese möchten wir nun in einem SSIS-Datenfluss in die Zieltabelle überführen und dabei in den Vierergruppen der GUID nach Quadrupeln suchen, die aus 4 Ziffern bestehen. Der reguläre Ausdruck, um solche Quadrupel zu finden ist der bereits oben verwendete (?<=-)(\d{4})(?=-). Dafür erzeugen wir uns ein einfaches Paket mit einem Datenfluss. Dieser liest aus unserer RegexTestDataSrc-Quelltabelle und schreibt in die RegexTestDataTgt-Zieltabelle. Im Datenfluss befindet sich nur eine synchrone Skriptkomponente, in der die Text-Spalten mittels regulärer Ausdrücke durchsucht werden (vgl. Abbildung 1: Datenfluss zum Bewerten der verschiedenen Vorgehensweisen). Die Ausführungszeit der RegEx sowie der erste Treffer werden in die Ziel-Spalten Match respektive SeekTime geschrieben.

Abbildung 1: Datenfluss zum Bewerten der verschiedenen Vorgehensweisen

Nun möchten wir verschiedene Vorgehensweisen bewerten:

  • Es wird in der ProcessInputRow-Methode für jede Zeile ein RegEx-Objekt erzeugt, das die Suche in der Textspalte ausführt, dabei wird wahlweise die Option „Compile“ der RegEx aktiviert bzw. deaktiviert.
  • Es wird in der PreExecute-Methode der Skriptkomponente einmalig das RegEx-Objekt erzeugt (wieder wahlweise mit und ohne die Compile-Option), dieses wird in ProcessInputRow immer wieder verwendet.
  • Es wird in ProcessInputRow die statische Matches-Methode der RegEx-Klasse mit und ohne die Compile-Option verwendet.

Am Ende fragen wir den durchschnittlichen „SeekTime“-Wert der Zieltabelle ab:

select avg(cast(seektime as decimal)) from dbo.RegexTestDataTgt

Die Resultate dieses Tests sind:

Tabelle 1: Ausführungszeiten der verschiedenen Ansätze

Methode Ø Seek time [Ticks] Ohne Option „Compile“ Ø Seek time [Ticks] Mit Option „Compile“
Objekt in ProcessInputRow 23.046 297.052
Ojbket in
Preexecute
3.784 4.197
Statischer Zugriff
5.239 8.840

Die Besonderheit, die allen Methoden gemein ist, ist, dass die erste Ausführung deutlich länger dauert, als die folgenden Ausführungen. Ein Grund dafür dürfte sein, dass bei der ersten Ausführung die gesamte RegEx-Engine von C# erstmalig in Betrieb genommen wird und die zugehörigen Caches initialisiert, was wieder Ausführungszeit in Anspruch nimmt.

Wie erwartet, verteuert die „Compiler“- Option, die ein neues Kompilieren der RegEx erzwingt, den Lauf erheblich. Konsequenterweise ist die Option, für jede Zeile ein Objekt zu erzeugen, die teuerste. Die Option, ein Objekt im PreExecute-Block der Skriptkomponente zu erzeugen und dieses immer wieder zu verwenden, schneidet im einfachen Vergleich deutlich am besten ab. Sie hat aber den Nachteil, dass sie nur bei immer gleich bleibenden Expressions verwendet werden kann, wird die Expression im Datenfluss dynamisch abhängig von den Eingabedaten erzeugt, so steht diese Option leider nicht zur Verfügung. In diesem Fall ist die beste Option der statische Zugriff, der zwar um einen Faktor ~1,4 teurer ist, dafür aber die volle Flexibilität beim Erzeugen der Expressions zulässt. Von der Option, ein RegEx-Objekt für jede Zeile neu zu erzeugen ist dringend abzuraten, sie ist im Fall einer konstanten Expression um einen Faktor ~6.0 teurer, als die schnellste Variante, im Fall dynamisch veränderbarer Expressions um einen Faktor ~4.4.

Fazit

Bei der Verwendung regulärer Ausdrücke in SSIS-Datenflüssen empfiehlt es sich, ein Augenmerk auf die Art des Aufrufes zu legen: bei einer Ausführungszeit von 0, 0005s für einen komplizierteren regulären Ausdruck kumuliert sich dieser bei 1.000.000 Ausführungen zu einer Gesamtdauer von 500 Sekunden, also 8,3 Minuten. Ein Faktor 6 bedeutet stattdessen eine reine Parsingzeit von 50 Minuten, die ohne weiteres eingespart werden kann.

Schreibe einen Kommentar