c# strip - Prova ad aumentare il mio codice?




3 Answers

Uno degli ingegneri di Roslyn , specializzato nella comprensione dell'ottimizzazione dell'utilizzo dello stack, ha dato un'occhiata a questo e mi ha riferito che sembra esserci un problema nell'interazione tra il modo in cui il compilatore C # genera archivi di variabili locali e il modo in cui il compilatore JIT si registra programmazione nel codice x86 corrispondente. Il risultato è una generazione di codice subottimale sui carichi e sugli scaffali dei locali.

Per qualche motivo non chiaro a tutti noi, il percorso di generazione del codice problematico viene evitato quando il JITter sa che il blocco si trova in una regione protetta dal tentativo.

Questo è abbastanza strano. Daremo seguito al team di JITter e vediamo se possiamo ricevere un bug in modo che possano risolvere il problema.

Inoltre, stiamo lavorando su miglioramenti per Roslyn agli algoritmi dei compilatori C # e VB per determinare quando i locali possono essere resi "effimeri" - cioè, semplicemente spinti e spuntati nello stack, piuttosto che allocare una posizione specifica nello stack per la durata dell'attivazione. Crediamo che il JITter sarà in grado di fare un lavoro migliore di assegnazione del registro e quant'altro se gli diamo suggerimenti migliori su quando i locali possono essere resi "morti" prima.

Grazie per averlo portato alla nostra attenzione e ci scusiamo per il comportamento strano.

html agility

Ho scritto un codice per testare l'impatto del try-catch, ma ho visto alcuni risultati sorprendenti.

static void Main(string[] args)
{
    Thread.CurrentThread.Priority = ThreadPriority.Highest;
    Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.RealTime;

    long start = 0, stop = 0, elapsed = 0;
    double avg = 0.0;

    long temp = Fibo(1);

    for (int i = 1; i < 100000000; i++)
    {
        start = Stopwatch.GetTimestamp();
        temp = Fibo(100);
        stop = Stopwatch.GetTimestamp();

        elapsed = stop - start;
        avg = avg + ((double)elapsed - avg) / i;
    }

    Console.WriteLine("Elapsed: " + avg);
    Console.ReadKey();
}

static long Fibo(int n)
{
    long n1 = 0, n2 = 1, fibo = 0;
    n++;

    for (int i = 1; i < n; i++)
    {
        n1 = n2;
        n2 = fibo;
        fibo = n1 + n2;
    }

    return fibo;
}

Sul mio computer, questo costantemente stampa un valore intorno a 0,96 ..

Quando avvolgo il ciclo for all'interno di Fibo () con un blocco try-catch come questo:

static long Fibo(int n)
{
    long n1 = 0, n2 = 1, fibo = 0;
    n++;

    try
    {
        for (int i = 1; i < n; i++)
        {
            n1 = n2;
            n2 = fibo;
            fibo = n1 + n2;
        }
    }
    catch {}

    return fibo;
}

Ora stampa costantemente 0,69 ... - in realtà corre più veloce! Ma perché?

Nota: l'ho compilato utilizzando la configurazione Release e ho eseguito direttamente il file EXE (all'esterno di Visual Studio).

EDIT: l' eccellente analisi di Jon Skeet mostra che try-catch sta in qualche modo facendo sì che il CLR x86 usi i registri della CPU in un modo più favorevole in questo caso specifico (e penso che dobbiamo ancora capire perché). Ho confermato che Jon ha scoperto che il CLR x64 non ha questa differenza e che era più veloce del CLR x86. Ho anche provato a utilizzare i tipi int all'interno del metodo Fibo invece dei tipi long , quindi il CLR x86 era altrettanto veloce del CLR x64.

AGGIORNAMENTO: Sembra che questo problema sia stato risolto da Roslyn. Stessa macchina, stessa versione CLR - il problema rimane come sopra quando compilato con VS 2013, ma il problema scompare quando compilato con VS 2015.




I disassemblaggi di Jon mostrano che la differenza tra le due versioni è che la versione veloce utilizza una coppia di registri ( esi,edi ) per memorizzare una delle variabili locali in cui la versione lenta non lo fa.

Il compilatore JIT fa diverse assunzioni sull'uso del registro per il codice che contiene un blocco try-catch rispetto al codice che non lo fa. Questo fa sì che faccia diverse scelte di allocazione del registro. In questo caso, questo favorisce il codice con il blocco try-catch. Un codice diverso può portare all'effetto opposto, quindi non lo conterei come tecnica di accelerazione generale.

Alla fine, è molto difficile dire quale codice finirà per essere il più veloce. Qualcosa come l'allocazione del registro ei fattori che lo influenzano sono dettagli di implementazione di basso livello che non vedo come una tecnica specifica possa produrre in modo affidabile un codice più veloce.

Ad esempio, considerare i seguenti due metodi. Sono stati adattati da un esempio di vita reale:

interface IIndexed { int this[int index] { get; set; } }
struct StructArray : IIndexed { 
    public int[] Array;
    public int this[int index] {
        get { return Array[index]; }
        set { Array[index] = value; }
    }
}

static int Generic<T>(int length, T a, T b) where T : IIndexed {
    int sum = 0;
    for (int i = 0; i < length; i++)
        sum += a[i] * b[i];
    return sum;
}
static int Specialized(int length, StructArray a, StructArray b) {
    int sum = 0;
    for (int i = 0; i < length; i++)
        sum += a[i] * b[i];
    return sum;
}

Uno è una versione generica dell'altro. Sostituire il tipo generico con StructArray renderebbe i metodi identici. Poiché StructArray è un tipo di valore, ottiene la sua versione compilata del metodo generico. Tuttavia, il tempo di esecuzione effettivo è significativamente più lungo rispetto al metodo specializzato, ma solo per x86. Per x64, i tempi sono praticamente identici. In altri casi, ho osservato anche le differenze per x64.




Avrei dovuto inserire questo come un commento in quanto non sono sicuro che sia probabile che sia il caso, ma poiché ricordo che non c'è una dichiarazione try / except implica una modifica al meccanismo di eliminazione dei rifiuti di il compilatore funziona, in quanto cancella le allocazioni della memoria degli oggetti in modo ricorsivo rispetto allo stack. Potrebbe non esserci un oggetto da chiarire in questo caso o il ciclo for può costituire una chiusura che il meccanismo di garbage collection riconosce sufficiente per imporre un metodo di raccolta diverso. Probabilmente no, ma ho pensato che valesse la pena menzionarlo visto che non l'avevo visto discusso da nessun'altra parte.




Related

c# .net clr try-catch performance-testing