Marco Russo

.NET, Business Intelligence e dintorni

Corsi

Miei blog in inglese

Soluzione alle ottimizzazioni non intuitive

Nel mio post precedente ho illustrato due varianti di codice C# chiedendo quale sia la più performante. Ho ricevuto diverse mail a riguardo e riassumo qui per tutti la soluzione.

  • Il codice più veloce è quello di CompareU2 rispetto a CompareU. In pratica, è più veloce fare ch = m1[i] in un ciclo dove i va da 0 a N, piuttosto che fare ch = *m1++ in un ciclo di N iterazioni.
  • Chi arriva dal C (o programma in assembler) avrebbe scommesso il contrario (e molti l'hanno fatto - io ero tra questi)
  • Normalmente l'accesso a un array attraverso un offset (m1[i]) è più lento dell'accesso attraverso un puntatore che viene spostato con l'aritmetica dei puntatori (*m1). In C# però il codice IL compilato nel primo caso lavora solo sullo stack, nel secondo deve aggiornare una variabile. Se il jitter usa solo registri di CPU per risolvere l'algebra di offset e va invece a scrivere in ram il valore del puntatore modificato... ecco una possibile spiegazione
  • Il codice esterno al loop for(...) non è molto rilevante ai fini dell'ottimizzazione (l'algoritmo confronta due stringhe usando ? come carattere "jolly", richiedendo l'uguaglianza per tutti gli altri caratteri). Il numero di ? in una stringa è generalmente una piccola frazione dell'intera stringa usata come pattern di ricerca.
  • In realtà non sono andato a vedere il codice macchina generato dal jitter, ma i numeri misurati sono stati inequivocabili. Con alcune stringhe di riferimento per i dati tipici dello scenario in cui sarà usata, la CompareU2 è più veloce di quasi il 25% rispetto alla CompareU. Non si tratta di dettagli per funzioni chiamate milioni di volte al minuto.
  • Il motivo per cui si usano i puntatori è che il codice "safe" che usa direttamente le stringhe (non riportato nel post, ma di facile costruzione) ha un costo di esecuzione a metà tra CompareU2 e CompareU. Il che porta a pensare che sia necessario usare algoritmi simili solo in condizioni "estreme".

La lezione è che nonostante le apparenze, rispetto al C++ è ben diversa la logica con cui pensare alle ottimizzazioni fatte sul codice C# da parte, rispettivamente, del compilatore e del jitter. Magari potremmo parlare di "mancate ottimizzazioni" del jitter, almeno in questo caso... ma questo è lo stato attuale (con compilatori e runtime di .NET 2.0). Prima di giungere a conclusioni, è sempre bene fare qualche misura - anche l'esperienza porta ad avere pregiudizi, talvolta non validi.