(0) exportieren Drucken
Alle erweitern
Erweitern Minimieren

Verbessern der Zeichenfolgenverarbeitung in .NET Framework-Anwendungen

Veröffentlicht: 31. Jul 2003 | Aktualisiert: 28. Jun 2004
Von James Musson

Auf dieser Seite

Einführung
Zeichenfolgenverkettung
Was ist ein "StringBuilder"?
Erstellen des Testcodes
Testen
Ergebnisse
Schlussfolgerung

Einführung

Beim Schreiben von .NET Framework-Anwendungen muss der Entwickler früher oder später unweigerlich die Zeichenfolgendarstellung von Daten durch Verketten anderer Zeichenfolgendaten erstellen. In der Regel verwendet er dazu wiederholt einen der Verkettungsoperatoren (entweder '&' oder '+'). In der Vergangenheit wurde bei der Untersuchung der Leistungs- und Skalierbarkeitsmerkmale einer Vielzahl von Anwendungen festgestellt, dass sich diese beiden Faktoren durch einen geringen zusätzlichen Entwicklungsaufwand erheblich verbessern lassen.

Zeichenfolgenverkettung

Sehen Sie sich das folgende Codefragment aus einer Visual Basic .NET-Klasse an. Die BuildXml1-Funktion verwendet einfach mehrere Iterationen (Reps) und erstellt mit Hilfe der Standardzeichenfolgenverkettung eine XML-Zeichenfolge mit der erforderlichen Anzahl von Bestellelementen.

' build an Xml string using standard concatenation 
Public Shared Function BuildXml1(ByVal Reps As Int32) As String 
   Dim nRep As Int32 
   Dim sXml As String 
   For nRep = 1 To Reps 
   sXml &= "<Order orderId=""" _ 
   & nRep _ 
   & """ orderDate=""" _ 
   & DateTime.Now.ToString() _ 
   & """ customerId=""" _ 
   & nRep _ 
   & """ productId=""" _ 
   & nRep _ 
   & """ productDescription=""" _ 
   & "This is the product with the Id: " _ 
   & nRep _ 
   & """ quantity=""" _ 
   & nRep _ 
   & """/>" 
   Next nRep 
   sXml = "<Orders method=""1"">" & sXml & "</Orders>" 
   Return sXml 
End Function

Der entsprechende Visual C#-Code wird nachfolgend dargestellt.

// build an Xml string using standard concatenation 
public static String BuildXml1(Int32 Reps) 
{ 
   String sXml = ""; 
   for( Int32 nRep = 1; nRep<=Reps; nRep++ ) 
   { 
   sXml += "<Order orderId=\""  
   + nRep  
   + "\" orderDate=\""  
   + DateTime.Now.ToString()  
   + "\" customerId=\""  
   + nRep  
   + "\" productId=\""  
   + nRep  
   + "\" productDescription=\""  
   + "This is the product with the Id: "  
   + nRep  
   + "\" quantity=\""  
   + nRep  
   + "\"/>"; 
   } 
   sXml = "<Orders method=\"1\">" + sXml + "</Orders>"; 
   return sXml; 
}

Diese Methode wird recht häufig zur Erstellung umfangreicher Zeichenfolgendaten in .NET Framework-Anwendungen und Anwendungen, die in anderen Umgebungen geschrieben wurden, genutzt. XML-Daten werden hier nur als Beispiel verwendet. Es gibt andere und bessere Methoden in .NET Framework zur Erstellung von XML-Zeichenfolgen, z.B. die System.Xml.XmlTextWriter-Klasse. Problematisch am BuildXml1-Code ist, dass der in .NET Framework enthaltene System.String-Datentyp eine unveränderliche Zeichenfolge darstellt. Das heißt, dass die ursprüngliche Darstellung der gespeicherten Zeichenfolge bei jeder Änderung der Zeichenfolgendaten zerstört und eine neue Zeichenfolge erstellt wird. Diese enthält die neuen Zeichenfolgendaten, was eine Speicherzuweisung und eine Speicherfreigabe zur Folge hat. Dies wird natürlich im Hintergrund durchgeführt, sodass die echten Kosten nicht sofort deutlich sind. Die Zuweisung und Freigabe haben eine erhöhte Aktivität bei der Speicherverwaltung und Garbage Collection in der Common Language Runtime (CLR) zur Folge und können hohe Kosten verursachen. Dies wird besonders deutlich bei großen Zeichenfolgen und wenn große Speicherblöcke in schneller Folge zugewiesen und freigegeben werden, wie es bei verstärkter Zeichenfolgenverkettung der Fall ist. Während dies in einer Einzelbenutzerumgebung keine größeren Probleme verursacht, können Leistung und Skalierbarkeit in einer Serverumgebung erheblich beeinträchtigt werden, z.B. in einer ASP.NET®-Awendung auf einem Webserver.

Zurück zu dem oben gezeigten Codefragment: Wie viele Zeichenfolgenzuweisungen werden hier ausgeführt? Die Antwort lautet "14". In diesem Fall wird die von der sXml-Variablen referenzierte Zeichenfolge bei jeder Anwendung des '&' (oder '+')-Operators zerstört und neu erstellt. Wie bereits erwähnt, verursacht die Zeichenfolgenzuweisung hohe Kosten, die sich mit zunehmender Größe der Zeichenfolge erhöhen. Aus diesem Grund steht im .NET Framework die StringBuilder-Klasse zur Verfügung.

Was ist ein "StringBuilder"?

Die Idee hinter der StringBuilder-Klasse ist nicht neu. In meinem früheren Artikel Improving String Handling Performance in ASP Applications (in Englisch) wird gezeigt, wie Sie einen StringBuilder mit Visual Basic 6 schreiben. Der Grundgedanke dabei ist, dass der StringBuilder seinen eigenen Zeichenfolgenpuffer verwaltet. Sobald ein Vorgang im StringBuilder ausgeführt wird, der unter Umständen die Länge der Zeichenfolgendaten verändert, prüft der StringBuilder zunächst, ob der Puffer groß genug ist, um die neuen Zeichenfolgendaten aufzunehmen. Ist dies nicht der Fall, wird der Puffer um einen vorbestimmten Wert vergrößert. Die StringBuilder-Klasse in .NET Framework bietet auch eine effiziente Replace-Methode an, die an Stelle von String.Replace verwendet werden kann.

Abbildung 1 zeigt einen Vergleich der Speichernutzungsmuster von Standardverkettung und StringBuilder-Verkettung. Bei der Standardverkettung wird für jeden Verkettungsvorgang eine neue Zeichenfolge erstellt, während der StringBuilder immer denselben Zeichenfolgenpuffer verwendet.

vbnstrcatn01.gif

Abbildung 1 Vergleich des Speichernutzungsmusters von Standard- und "StringBuilder"-Verkettung

Der Code zur Erstellung der XML-Zeichenfolgendaten mit der StringBuilder-Klasse wird nachfolgend in BuildXml2 gezeigt.

' build an Xml string using the StringBuilder 
Public Shared Function BuildXml2(ByVal Reps As Int32) As String 
   Dim nRep As Int32 
   Dim oSB As StringBuilder 
   ' make sure that the StringBuilder capacity is 
   ' large enough for the resulting text 
   oSB = New StringBuilder(Reps * 165) 
   oSB.Append("<Orders method=""2"">") 
   For nRep = 1 To Reps 
   oSB.Append("<Order orderId=""") 
   oSB.Append(nRep) 
   oSB.Append(""" orderDate=""") 
   oSB.Append(DateTime.Now.ToString()) 
   oSB.Append(""" customerId=""") 
   oSB.Append(nRep) 
   oSB.Append(""" productId=""") 
   oSB.Append(nRep) 
   oSB.Append(""" productDescription=""") 
   oSB.Append("This is the product with the Id: ") 
   oSB.Append(nRep) 
   oSB.Append(""" quantity=""") 
   oSB.Append(nRep) 
   oSB.Append("""/>") 
   Next nRep 
   oSB.Append("</Orders>") 
   Return oSB.ToString() 
End Function

Der entsprechende Visual C#-Code wird nachfolgend dargestellt.

// build an Xml string using the StringBuilder 
public static String BuildXml2(Int32 Reps) 
{ 
   // make sure that the StringBuilder capacity is 
   // large enough for the resulting text 
   StringBuilder oSB = new StringBuilder(Reps * 165); 
   oSB.Append("<Orders method=\"2\">"); 
   for( Int32 nRep = 1; nRep<=Reps; nRep++ ) 
   { 
   oSB.Append("<Order orderId=\""); 
   oSB.Append(nRep); 
   oSB.Append("\" orderDate=\""); 
   oSB.Append(DateTime.Now.ToString()); 
   oSB.Append("\" customerId=\""); 
   oSB.Append(nRep); 
   oSB.Append("\" productId=\""); 
   oSB.Append(nRep); 
   oSB.Append("\" productDescription=\""); 
   oSB.Append("This is the product with the Id: "); 
   oSB.Append(nRep); 
   oSB.Append("\" quantity=\""); 
   oSB.Append(nRep); 
   oSB.Append("\"/>"); 
   } 
   oSB.Append("</Orders>"); 
   return oSB.ToString(); 
}

Die Leistung der StringBuilder-Methode im Vergleich zur Standardverkettungsmethode hängt von mehreren Faktoren ab, darunter die Anzahl der Verkettungen, die Größe der zu erstellenden Zeichenfolge und wie gut die Initialisierungsparameter für den StringBuilder-Puffer ausgewählt werden. In den meisten Fällen ist es besser, den erforderlichen Speicherplatz im Puffer höher anzusetzen, als ihn ständig erweitern zu müssen.

Erstellen des Testcodes

Da ich die beiden Methoden der Zeichenfolgenverkettung mit Application Center Test® (ACT) testen wollte, mussten sie von einer ASP.NET-Webanwendung offen gelegt werden. Weil bei der Verarbeitung nicht für jede Anforderung eine ASP.NET-Seite in meinen Ergebnissen erstellt werden sollte, habe ich einen HttpHandler erstellt und registriert, der Anforderungen nach meinem logischen URL StringBuilderTest.jemx akzeptiert und die entsprechende BuildXml-Funktion aufruft. Zwar sprengt eine ausführliche Erläuterung von HttpHandlers den Rahmen dieses Artikels, dennoch ist mein Testcode nachfolgend dargestellt.

Public Class StringBuilderTestHandler 
   Implements IHttpHandler 
   Public Sub ProcessRequest(ByVal context As HttpContext) _ 
   Implements IHttpHandler.ProcessRequest 
   Dim nMethod As Int32 
   Dim nReps As Int32 
   ' retrieve test params from the querystring 
   If Not context.Request.QueryString("method") Is Nothing Then 
   nMethod = Int32.Parse( _ 
   context.Request.QueryString("method").ToString()) 
   Else 
   nMethod = 0 
   End If 
   If Not context.Request.QueryString("reps") Is Nothing Then 
   nReps = Int32.Parse( _ 
   context.Request.QueryString("reps").ToString()) 
   Else 
   nReps = 0 
   End If 
   context.Response.ContentType = "text/xml" 
   context.Response.Write( _ 
   "<?xml version=""1.0"" encoding=""utf-8"" ?>") 
   ' write the Xml to the response stream 
   Select Case nMethod 
   Case 1 
   context.Response.Write( _ 
   StringBuilderTest.BuildXml1(nReps)) 
   Case 2 
   context.Response.Write( _ 
   StringBuilderTest.BuildXml2(nReps)) 
   End Select 
   End Sub 
   Public ReadOnly Property IsReusable() As Boolean _ 
   Implements IHttpHandler.IsReusable 
   Get 
   Return True 
   End Get 
   End Property 
End Class

Der entsprechende Visual C#-Code wird nachfolgend dargestellt.

public class StringBuilderTestHandler : IHttpHandler 
{ 
   public void ProcessRequest(HttpContext context)  
   { 
   Int32 nMethod = 0; 
   Int32 nReps = 0; 
   // retrieve test params from the querystring 
   if( context.Request.QueryString["method"]!=null ) 
   nMethod = Int32.Parse( 
   context.Request.QueryString["method"].ToString()); 
   if( context.Request.QueryString["reps"]!=null ) 
   nReps = Int32.Parse( 
   context.Request.QueryString["reps"].ToString()); 
   // write the Xml to the response stream 
   context.Response.ContentType = "text/xml"; 
   context.Response.Write( 
   "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"); 
   switch( nMethod ) 
   { 
   case 1 : 
   context.Response.Write( 
   StringBuilderTest.BuildXml1(nReps)); 
   break; 
   case 2 : 
   context.Response.Write( 
   StringBuilderTest.BuildXml2(nReps)); 
   break; 
   } 
   } 
   public Boolean IsReusable { get{ return true; } } 
}

Die ASP.NET-HttpPipeline erstellt eine Instanz von StringBuilderTestHandler und ruft die ProcessRequest-Methode für jede HTTP-Anforderung von StringBuilderTest.jemx auf. ProcessRequest extrahiert mehrere Parameter aus der Anforderungszeichenfolge und ruft die richtige BuildXml-Funktion auf. Nachdem einige Headerinformationen erstellt wurden, wird der Rückgabewert der BuildXml-Funktion an den Response-Strom zurückgegeben.

Weitere Informationen zu HttpHandlers finden Sie in der IhttpHandler-Dokumentation.

Testen

Die Tests wurden unter Verwendung von ACT auf einem einzelnen Client (Windows® XP Professional, PIII-850 MHz, 512 MB RAM) gegen einen einzelnen Server (Windows Server 2003 Enterprise Edition, Dual PIII-1000 MHz, 512 MB RAM) in einem 100 Mbit/s-Netz ausgeführt. ACT war zur Verwendung von 5 Threads konfiguriert, sodass fünf Benutzer simuliert wurden, die sich bei der Website anmelden. Jeder Test bestand aus einer 10-sekündigen Anlaufzeit, gefolgt von einer 50-sekündigen Ladezeit, in der so viele Anforderungen wie möglich ausgeführt wurden.

Die Tests wurden für eine unterschiedliche Anzahl von Verkettungsvorgängen wiederholt. Dabei variierte die Anzahl der Iterationen in der Hauptschleife, wie Sie in den Codefragmenten für die BuildXmlFunktionen sehen.

Ergebnisse

Nachfolgend werden in mehreren Diagrammen die Auswirkung jeder Methode auf den Durchsatz der Anwendung sowie die Reaktionszeit dargestellt, die der XML-Datenstrom zur Rückleitung an den Client benötigte. Dadurch wird ansatzweise erkennbar, wie viele Ergebnisse die Anwendung verarbeiten kann und wie lange die Benutzer bzw. Clientanwendungen auf den Empfang der Daten warten.

Tabelle 1 Schlüssel zu den verwendeten Abkürzungen der Verkettungsmethode

Methodenabkürzung

Beschreibung

CAT

Standardzeichenfolgenverkettungsmethode (BuildXml1)

BLDR

StringBuilder-Methode (BuildXml2)

Obwohl dieser Test, was die Simulation der Arbeitslast einer gängigen Anwendung betrifft, nicht sehr realistisch ist, können Sie anhand der Tabelle 2 sehen, dass die XML-Datenzeichenfolge selbst bei 425 Wiederholungen nicht sonderlich groß ist. Bei vielen Anwendungen entspricht die durchschnittliche Größe der Datenübertragungen diesen Werten oder liegt darüber.

Tabelle 2 XML-Zeichenfolgengrößen und Anzahl der Verkettungen für Testbeispiele

Anz. der Iterationen

Anz. der Verkettungen

XML-Zeichenfolgengröße (Byte)

25

350

3.897

75

1.050

11.647

125

1.750

19.527

175

2.450

27.527

225

3.150

35.527

275

3.850

43.527

325

4.550

51.527

375

5.250

59.527

425

5.950

67.527

vbnstrcatn02.gif

Abbildung 2 Diagramm mit Durchsatzergebnissen

vbnstrcatn03.gif

Abbildung 3 Diagramm mit Reaktionszeiten

Wie deutlich in den Abbildungen 2 und 3 erkennbar ist, übertrifft die StringBuilder-Methode (BLDR) die Standardverkettung (CAT), sowohl was die Anzahl der verarbeiteten Anforderungen als auch die verstrichene Zeit betrifft, die zur Generierung einer Rückantwort an den Client erforderlich ist (siehe Grafik unter Time-To-First-Byte bzw. TTFB). Im Gegensatz zur Standardverkettung verarbeitet die StringBuilder-Methode bei 425 Iterationen das 17fache an Anforderungen und benötigt für jede Anforderung lediglich 3 % der verstrichenen Zeit.

vbnstrcatn04.gif

Abbildung 4 Diagramm zum Gesamtzustand des Systems während der Tests

Abbildung 4 zeigt Ihnen die Serverlast während der Tests. Es ist interessant zu sehen, dass die StringBuilder-Methode (BLDR) die Standardverkettung (CAT) nicht nur in jedem Schritt übertrifft, sondern auch eine weitaus geringere CPU-Nutzung und eine kürzere Garbage Collection verzeichnen kann. Zwar beweist dies nicht, dass die Ressourcen auf dem Server während der StringBuilder-Vorgänge effektiver genutzt wurden, legt aber diese Vermutung mit Sicherheit nahe.

Schlussfolgerung

Die Schlussfolgerung aus diesen Testergebnissen liegt auf der Hand: Verwenden Sie die StringBuilder-Klasse für die einfachsten Zeichenfolgenverkettungen (oder Replace-Vorgänge). Der zusätzliche Aufwand zur Verwendung der StringBuilder-Klasse ist geringfügig und wird durch die damit erzielte bessere Leistung und Skalierbarkeit bei weitem wieder wettgemacht.

Links zu verwandten Themen

Anzeigen:
© 2014 Microsoft