2010-04-13 5 views
7

Sembra che C# sia più veloce nell'aggiungere due array di UInt16[] rispetto all'aggiunta di due array di int[]. Questo non ha senso per me, dal momento che avrei pensato che gli array sarebbero stati allineati a parole, e quindi int[] richiederebbe meno lavoro dalla CPU, no?Perché gli array UInt16 sembrano aggiungersi più velocemente degli array int?

ho eseguito il test-codice qui sotto, e ci hanno dato i seguenti risultati:

Int for 1000 took 9896625613 tick (4227 msec) 
UInt16 for 1000 took 6297688551 tick (2689 msec) 

Il codice di test esegue le seguenti operazioni:

  1. crea due array di nome a e b, una volta.
  2. Riempie con dati casuali una volta.
  3. Avvia un cronometro.
  4. Aggiunge a e b, articolo per articolo. Questo è fatto 1000 volte.
  5. Arresta il cronometro.
  6. Riporta quanto tempo è trascorso.

Questo viene fatto per int[] a, b e per UInt16 a,b. E ogni time eseguo il codice, i test per gli array UInt16 richiedono il 30% -50% di tempo in meno rispetto agli array int. Puoi spiegarmelo?

Ecco il codice, se si vuole provare se voi stessi:

public static UInt16[] GenerateRandomDataUInt16(int length) 
{ 
    UInt16[] noise = new UInt16[length]; 
    Random random = new Random((int)DateTime.Now.Ticks); 
    for (int i = 0; i < length; ++i) 
    { 
     noise[i] = (UInt16)random.Next(); 
    } 

    return noise; 
} 

public static int[] GenerateRandomDataInt(int length) 
{ 
    int[] noise = new int[length]; 
    Random random = new Random((int)DateTime.Now.Ticks); 
    for (int i = 0; i < length; ++i) 
    { 
     noise[i] = (int)random.Next(); 
    } 

    return noise; 
} 

public static int[] AddInt(int[] a, int[] b) 
{ 
    int len = a.Length; 
    int[] result = new int[len]; 
    for (int i = 0; i < len; ++i) 
    { 
     result[i] = (int)(a[i] + b[i]); 
    } 
    return result; 
} 

public static UInt16[] AddUInt16(UInt16[] a, UInt16[] b) 
{ 
    int len = a.Length; 
    UInt16[] result = new UInt16[len]; 
    for (int i = 0; i < len; ++i) 
    { 
     result[i] = (ushort)(a[i] + b[i]); 
    } 
    return result; 
} 


public static void Main() 
{ 
    int count = 1000; 
    int len = 128 * 6000; 

    int[] aInt = GenerateRandomDataInt(len); 
    int[] bInt = GenerateRandomDataInt(len); 

    Stopwatch s = new Stopwatch(); 
    s.Start(); 
    for (int i=0; i<count; ++i) 
    { 
     int[] resultInt = AddInt(aInt, bInt); 
    } 
    s.Stop(); 
    Console.WriteLine("Int for " + count 
       + " took " + s.ElapsedTicks + " tick (" 
       + s.ElapsedMilliseconds + " msec)"); 

    UInt16[] aUInt16 = GenerateRandomDataUInt16(len); 
    UInt16[] bUInt16 = GenerateRandomDataUInt16(len); 

    s = new Stopwatch(); 
    s.Start(); 
    for (int i=0; i<count; ++i) 
    { 
     UInt16[] resultUInt16 = AddUInt16(aUInt16, bUInt16); 
    } 
    s.Stop(); 
    Console.WriteLine("UInt16 for " + count 
       + " took " + s.ElapsedTicks + " tick (" 
       + s.ElapsedMilliseconds + " msec)"); 


} 
+2

Hai provato a eseguire l'aggiunta di elementi inline, senza chiamare le funzioni AddXXX, passando e restituendo gli array? Hai provato altre dimensioni di array? –

+0

@Grzegorz Gierlik: bella domanda. Così com'è, la routine 'int' probabilmente deve allocare il doppio della memoria. –

+2

Che tipo di hardware è? Arrivo a 15650msec e 14657msec (leggi: nessuna differenza significativa). Sospetto che il microbenchmark ti stia buttando fuori - i motori JIT e le VM ottimizzatrici sono famosi per questo. La velocità di aggiunta dei numeri (16/32 bit) * sarà la stessa * su qualsiasi CPU x86/x64 moderna. Tuttavia, numeri più grandi possono comportare una piccola penalità in termini di riempimento di più linee di cache e possibilmente richiedendo più bus da trasferire. –

risposta

6

Quello che succede è che vedi un'astrazione che perde. UInt16 prende la metà della memoria che int fa (16 vs 32 bit).

Ciò significa che l'area di memoria occupata dall'array int16 occupa metà dell'area che fa int32. Quindi più di quell'area può adattarsi alla cache del processore e quindi essere accessibile molto rapidamente.

Si potrebbe provare quel codice su un processore che ha più cache e la differenza è probabile che sia minore.

Prova anche con array molto più grandi.

+0

Al contrario, provalo con array più piccoli che si adattano all'interno di una linea della cache. –

2

Gli array sono allineati parola, ma non v'è alcun motivo per cui le voci nella matrice devono essere allineati parola.

1

Solo uno SWAG: l'uso più piccolo della memoria degli array UInt16 ha migliorato le caratteristiche della memoria (GC, cache, chissà cos'altro). Dal momento che non sembrano esserci troppe allocazioni, direi che la cache è il fattore principale.

Inoltre, dovresti fare in modo che il benchmarking possa essere un affare complicato: sembra che i tuoi tempi includano probabilmente alcune compilation JIT, che potrebbero essere dei risultati distorti. È possibile provare a invertire l'ordine di testare l'array int con l'array UInt16 e vedere se i tempi seguono o no.

Jon Skeet ha (o ha avuto) un semplice schema di riferimento che ha codificato fino a quando ha cercato di tenere conto di questi effetti. Non so se è ancora disponibile (o addirittura applicabile); forse commenterà.

1

paio di fattori

1) Si sono anche temporizzare il generazione della risultante array..so sarebbe interessante vedere quanto tempo ci sono voluti per solo per l'aggiunta vs creare l'array risultato che viene passato precedente

2) Sarebbe interessante vedere quale IL viene generato. Dato che il tuo codice è MOLTO semplice (iterare e aggiungere), il compilatore potrebbe ottimizzarlo, magari riempire più uint16 in un registro più grande e fare più aggiunte per istruzione

+1

L'ho controllato in Reflector e non è quello che sta succedendo. Il codice è algoritmicamente praticamente identico. Tutte le operazioni sono uguali ma corrette per i tipi di dati appropriati. L'unica differenza significativa è l'aggiunta di un'operazione 'conv.u2' che segue il comando' add' nel caso 'UInt16' (' add' restituisce un int, suppongo - non riesca a trovare la documentazione per eseguirne il backup, benché sia ​​valido ragionare perché è così che funziona anche C#). Se la differenza fosse nell'IL, mi aspetterei che la versione 'UInt16' fosse più lenta, grazie a quella conversione extra. La mia scommessa è sulla teoria della cache miss. – Dathan

1

Non sono esperto in .NET ma vorrei controllare due cose:

  1. Passando array più grande (N elementi di tipo int) richiede più tempo quindi array di N elementi ushort. Questo può essere testato usando varie dimensioni di matrici e stile di codifica - vedi il mio commento alla domanda). I numeri dei test corrispondono a questa teoria :).
  2. calcolata due ushort variabili possono essere implementati come aggiungendo due int con risultato di tipo int - colpo controllare traboccante. E presumo che lo che gestisce nel codice di qualsiasi tipo di l'eccezione (inclusa l'eccezione di overflow) è attività che richiede tempo. Questo può essere controllato nella documentazione di .NET.
+1

FYI, VS 2008 utilizza l'operazione 'add' IL durante la compilazione di quanto sopra e la [specifica CIL] (http://download.microsoft.com/download/7/3/3/733AD403-90B2-4064-A81E- 01035A7FE13C/MS% 20Partition% 20III.pdf) afferma che l'operazione 'add' non controlla l'overflow. – Dathan