2009-12-21 12 views
18

Ho eseguito alcuni test delle prestazioni, principalmente per capire la differenza tra iteratori e loop semplici. Come parte di questo ho creato un semplice set di test e sono stato quindi totalmente sorpreso dai risultati. Per alcuni metodi, 64 bit era quasi 10 volte più veloce di 32 bit.Perché è più veloce a 64 bit rispetto a 32 bit?

Quello che sto cercando è una spiegazione del perché questo sta accadendo.

[La risposta seguente afferma che questo è dovuto all'aritmetica a 64 bit in un'app a 32 bit. La modifica dei long a ints comporta buone prestazioni sui sistemi a 32 e 64 bit.]

Ecco i 3 metodi in questione.

private static long ForSumArray(long[] array) 
{ 
    var result = 0L; 
    for (var i = 0L; i < array.LongLength; i++) 
    { 
     result += array[i]; 
    } 
    return result; 
} 

private static long ForSumArray2(long[] array) 
{ 
    var length = array.LongLength; 
    var result = 0L; 
    for (var i = 0L; i < length; i++) 
    { 
     result += array[i]; 
    } 
    return result; 
} 

private static long IterSumArray(long[] array) 
{ 
    var result = 0L; 
    foreach (var entry in array) 
    { 
     result += entry; 
    } 
    return result; 
} 

Ho un semplice di collaudo che verifica questo

var repeat = 10000; 

var arrayLength = 100000; 
var array = new long[arrayLength]; 
for (var i = 0; i < arrayLength; i++) 
{ 
    array[i] = i; 
} 

Console.WriteLine("For: {0}", AverageRunTime(repeat,() => ForSumArray(array))); 

repeat = 100000; 
Console.WriteLine("For2: {0}", AverageRunTime(repeat,() => ForSumArray2(array))); 
Console.WriteLine("Iter: {0}", AverageRunTime(repeat,() => IterSumArray(array))); 

private static TimeSpan AverageRunTime(int count, Action method) 
{ 
    var stopwatch = new Stopwatch(); 
    stopwatch.Start(); 
    for (var i = 0; i < count; i++) 
    { 
     method(); 
    } 
    stopwatch.Stop(); 
    var average = stopwatch.Elapsed.Ticks/count; 
    return new TimeSpan(average); 
} 

Quando corro questi, ottengo i seguenti risultati:
32 bit:

For: 00:00:00.0006080 
For2: 00:00:00.0005694 
Iter: 00:00:00.0001717

64 bit

For: 00:00:00.0007421 
For2: 00:00:00.0000814 
Iter: 00:00:00.0000818

Le cose che ho letto da questo sono che l'uso di LongLength è lento. Se uso array.Length, le prestazioni per il primo ciclo for sono piuttosto buone a 64 bit, ma non a 32 bit.

L'altra cosa che ho letto da questo è che l'iterazione su un array è efficiente come un ciclo for, e il codice è molto più pulito e facile da leggere!

+0

Ciò che trovo interessante è che ovviamente il compilatore JIT non ottimizza l'array. Accesso LongLength. – newgre

risposta

50

I processori x64 contengono registri generici a 64 bit con i quali possono calcolare operazioni su numeri interi a 64 bit in una singola istruzione. I processori a 32 bit non hanno questo. Ciò è particolarmente rilevante per il tuo programma poiché utilizza pesantemente le variabili long (integer a 64 bit).

Per esempio, in assemblea x64, di aggiungere un paio di 64 bit interi memorizzati nei registri, si può semplicemente fare:

; adds rbx to rax 
add rax, rbx 

a fare la stessa operazione su un processore x86 a 32 bit, si dovrà utilizzare due registri e manualmente utilizzare il riporto della prima operazione nella seconda operazione:

; adds ecx:ebx to edx:eax 
add eax, ebx 
adc edx, ecx 

Ulteriori istruzioni e meno registri significano più cicli di clock, memoria recupera, ... che concludersi con prestazioni ridotte. La differenza è molto notevole nelle applicazioni di sgranatura numerica.

Per le applicazioni .NET, sembra che il compilatore JIT a 64 bit esegua ottimizzazioni più aggressive migliorando le prestazioni generali.

Per quanto riguarda il punto sull'iterazione dell'array, il compilatore C# è abbastanza intelligente da riconoscere foreach sugli array e trattarli in modo specifico. Il codice generato è identico all'utilizzo di un ciclo for e si consiglia di utilizzare foreach se non è necessario modificare l'elemento dell'array nel ciclo.Inoltre, il runtime riconosce il pattern for (int i = 0; i < a.Length; ++i) e omette i controlli associati per gli accessi agli array all'interno del ciclo. Ciò non avverrà nel caso LongLength e si tradurrà in prestazioni ridotte (sia per case a 32 bit che a 64 bit); e dal momento che userete le variabili long con LongLength, le prestazioni a 32 bit si degraderanno ancora di più.

+4

Anche il numero di registri è aumentato nei processori x64, ma non utilizzano quei registri quando si esegue il codice a 32 bit, solo il codice a 64 bit. – Powerlord

+0

Grande commento sul compilatore C# e foreach, specialmente i limiti che controllano gli accessi agli array! –

1

Non sono sicuro del "perché", ma mi piacerebbe assicurarmi di chiamare il tuo "metodo" almeno una volta al di fuori del ciclo del timer in modo da non contare il primo passaggio. (Dal momento che questo mi sembra C#).

5

Il tipo di dati lungo è 64 bit e in un processo a 64 bit, viene elaborato come una singola unità di lunghezza nativa. In un processo a 32 bit, viene considerato come 2 unità a 32 bit. La matematica, specialmente su questi tipi "divisi", richiederà un uso intensivo del processore.

1

Oh, è facile. Presumo che si stia utilizzando la tecnologia x86. Di cosa hai bisogno per fare i loop in assembler?

  1. Una variabile indice i
  2. Un risultato variabile risultato
  3. Una lunga serie di risultati.

Quindi sono necessarie tre variabili. L'accesso alle variabili è più veloce se è possibile memorizzarle nei registri; se hai bisogno di spostarli dentro e fuori per la memoria, stai perdendo velocità. Per i bit a 64 bit è necessario il due registri su 32 bit e abbiamo solo quattro registri, quindi è probabile che tutte le variabili non possano essere memorizzate nei registri, ma devono essere memorizzate nell'archivio intermedio come lo stack. Questo da solo rallenterà notevolmente l'accesso.

Aggiunta di numeri: L'aggiunta deve essere due volte; la prima volta senza carry bit e la seconda volta con carry bit. 64 bit può fare in un ciclo.

Spostamento/caricamento: Per ogni variabile a 64 bit a 1 ciclo sono necessari due cicli per 32 bit per caricare/scaricare un intero lungo in memoria.

Ogni tipo di dati componente (tipi di dati che consiste di più bit di bit di registro/indirizzo) perde una notevole velocità. Il guadagno di velocità di un ordine di grandezza è la ragione per cui le GPU preferiscono ancora i float (32 bit) invece dei doppi (64 bit).

0

Come altri hanno detto, l'esecuzione dell'aritmetica a 64 bit su una macchina a 32 bit richiede qualche manipolazione in più, quindi se si fa moltiplicazione o divisione.

Torna alla tua preoccupazione sugli iteratori rispetto ai cicli semplici per gli iteratori, gli iteratori possono avere definizioni abbastanza complesse, e saranno veloci solo se inlining e l'ottimizzazione del compilatore è in grado di sostituirli con il modulo semplice equivalente. Dipende davvero dal tipo di iteratore e dall'implementazione del contenitore sottostante. Il modo più semplice per capire se è stato ottimizzato abbastanza bene è quello di esaminare il codice assembly generato. Un altro modo è metterlo in un loop di lunga durata, metterlo in pausa e osservare lo stack per vedere cosa sta facendo.

Problemi correlati