Stringhe più performanti

pubblicato il 05/11/2006

Una delle differenze tra il vecchio (e a me non troppo caro) VB6 e VB.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 tutte le altre classi .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 leggeremente 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 attuale (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: 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.