. Net: la teoria delle stringhe… più veloci

Una delle differenze tra il vecchio (e a me non troppo caro) VB6 ed il mondo  .NET sta nella gestione delle stringhe, in particolare nella concatenazione delle stringhe; se nel vecchio VB6 la concatenazione si esprimeva con la sintassi

stringaA = stringaB & stringaC

dove stringaA e stringaB potrebbero anche coincidere; in VB.NET si scrive

stringaA = stringaB + stringaC

(in realtà volendo è ancora possibile usare l’& per esprimere la concatenazione, ma personalmente preferisco usare il segno +, coerentemente con tutti gli altri tipi .NET).

Tuttavia questo modo di concatenare le stringhe non è molto performante, perlomeno quando si tratta di concatenare un numero elevato di stringhe: il Framework.NET infatti alloca un nuovo blocco di memoria ogni volta che la stringa viene modificata; per ovviare a questo problema abbiamo a disposizione nella namespace System.Text la classe StringBuilder da usare proprio per questi scopi. Per testare le differenze tra i vari metodi, creiamo un semplice progetto di tipo Console Application:

Imports System.Text
 Module Module1
 Sub Main()

Const tries As Integer = 1000
 Dim index As Integer
 Dim startTime As DateTime
 Dim diff As TimeSpan
 Debug.Write("Number of tries: ")
 Debug.WriteLine(tries.ToString)

' *** String
 startTime = Now
 Dim testString As String
 For index = 1 To tries
 testString = testString + "@"
 Next
 diff = Now.Subtract(startTime)
 Debug.Write("String: ")
 Debug.WriteLine(diff.ToString)

' *** IndexToString
 startTime = Now
 Dim testIndexToString As String
 For index = 1 To tries
 testIndexToString = testIndexToString + index.ToString
 Next
 diff = Now.Subtract(startTime)
 Debug.Write("IndexToString: ")
 Debug.WriteLine(diff.ToString)

' *** String.Format
 startTime = Now
 Dim testStringFormat As String
 For index = 1 To tries
 testString = testString + String.Format("{0}", index)
 Next
 diff = Now.Subtract(startTime)
 Debug.Write("String.Format: ")
 Debug.WriteLine(diff.ToString)

' *** StringBuilder.AppendString
 startTime = Now
 Dim testBuilderAppendString As New StringBuilder
 For index = 1 To tries
 testBuilderAppendString.Append("abcdef")
 Next
 diff = Now.Subtract(startTime)
 Debug.Write("StringBuilder.AppendString: ")
 Debug.WriteLine(diff.ToString)

' *** StringBuilder.Append
 startTime = Now
 Dim testBuilderAppendArgument As New StringBuilder
 For index = 1 To tries
 testBuilderAppendArgument.Append(index)
 Next
 diff = Now.Subtract(startTime)
 Debug.Write("StringBuilder.AppendArgument: ")
 Debug.WriteLine(diff.ToString)

' *** StringBuilder.Append
 startTime = Now
 Dim testBuilderAppendFormat As New StringBuilder
 For index = 1 To tries
 testBuilderAppendFormat.AppendFormat("{0}", index)
 Next
 diff = Now.Subtract(startTime)
 Debug.Write("StringBuilder.AppendFormat: ")
 Debug.WriteLine(diff.ToString)

End Sub

End Module

Questo semplice programmino di test produce sul mio computer di sviluppo (Pentium 4 2.8GHz, 2GB RAM) il seguente output:

Number of tries: 1000
String: 00:00:00
IndexToString: 00:00:00
String.Format: 00:00:00.0161025
StringBuilder.AppendString: 00:00:00
StringBuilder.AppendArgument: 00:00:00
StringBuilder.AppendFormat: 00:00:00

Incrementando il valore di tries prima a 10.000 e poi a 50.000, otterremmo i seguenti risultati:

Number of tries: 10000
String: 00:00:00.0805125
IndexToString: 00:00:00.3163560
String.Format: 00:00:00.6441000
StringBuilder.AppendString: 00:00:00
StringBuilder.AppendArgument: 00:00:00.0161025
StringBuilder.AppendFormat: 00:00:00.0161025

Number of tries: 50000
String: 00:00:02.7535275
IndexToString: 00:00:22.0289897
String.Format: 00:00:32.7846900
StringBuilder.AppendString: 00:00:00
StringBuilder.AppendArgument: 00:00:00.0322050
StringBuilder.AppendFormat: 00:00:00.0483075

Devo premettere che questo test non ha un rigore scientifico assoluto: ripetendo più volte gli stessi test con lo stesso numero di cicli è possibile ottenere valori leggermente diversi; quello che però non cambia tra un’esecuzione e l’altra è la “classifica” se così si può dire in termini di velocità di esecuzione tra le varie soluzioni possibili.

Come possiamo notare, per un numero sufficientemente piccolo di iterazioni le differenze non sono poi così rilevanti; ma se il numero di cicli cresce, crescono anche le differenze soprattutto tra l’utilizzo della StringBuilder, sempre molto veloce, e l’uso della String, decisamente più lenta.

Il motivo di tale lentezza è dovuto al fatto che il Framework ad ogni ciclo alloca una nuova stringa nell’heap contenente il risultato della concatenazione e poi assegna il risultato alla stringa originale; la StringBuilder invece ha una gestione molto più efficiente della memoria.

Tra le varie proprietà della classe StringBuilder vi sono la Length, che specifica la dimensione attuale della stringa contenibile, e MaxCapacity, che specifica la dimensione massima allocabile.

La classe prevede diversi costruttori: quello senza parametri alloca una dimensione predefinita (Length) pari a 16 caratteri e una dimensione massima (MaxCapacity) pari a 2,147,483,647; nel caso dovessimo sforare la dimensione attuale, la classe allocherebbe nuova memoria, purché entro il limite pari alla dimensione massima.

La cosa migliore, quando è possibile determinarla, è usare il costruttore che richiede come parametro la dimensione reale (Length) così da evitare nuove allocazioni; le differenze prestazionali comunque non sono così penalizzanti nel caso non sia determinabile a priori la dimensioni massima.

String e StringBuilder sono due classi del Framework non interscambiabili tra loro: per utilizzare il risultato delle StringBuilder, dovremo usarne il metodo ToString:

Const tries As Integer = 1000
Dim index As Integer
Dim builder As New StringBuilder(tries)
Dim result As String
For index = 1 To tries
    builder.Append("@")
Next
result = builder.ToString

Un ultima cosa…

Un’ultima cosa: la maniera più efficiente per inizializzare una stringa vuota, consiste nello scrivere la riga:

Dim myString As String = String.Empty

Infatti scrivendo

Dim myString As String = ""

il Framework allocherebbe una variabile di tipo String cui assegnerebbe il valore String.Empty e che poi verrebbe assegnata a myString. Anche qui la differenza nel caso di una ricorrenza singola è risibile, mentre potrebbe essere più rilevante all’interno di loop sostanziosi.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.

*