Attualmente sto ottimizzando una libreria di basso livello e ho trovato un caso contro-intuitivo. Il commit che ha causato questa domanda è here.C# Perché utilizzare il metodo di istanza come delegato alloca oggetti temporanei di GC0 ma il 10% più veloce di un delegato memorizzato nella cache
C'è un delegato
public delegate void FragmentHandler(UnsafeBuffer buffer, int offset, int length, Header header);
e un metodo di istanza
public void OnFragment(IDirectBuffer buffer, int offset, int length, Header header)
{
_totalBytes.Set(_totalBytes.Get() + length);
}
In this line, se uso il metodo come delegato, il programma alloca molti GC0 per la temperatura delegato involucro, ma la performance è del 10% più veloce (ma non stabile).
var fragmentsRead = image.Poll(OnFragment, MessageCountLimit);
Se invece cache il metodo in un delegato fuori del ciclo come questo:
FragmentHandler onFragmentHandler = OnFragment;
allora il programma non alloca affatto, numeri sono molto stabile, ma molto più lento.
Ho guardato attraverso IL generato e sta facendo la stessa cosa, ma nel caso successivo newobj
viene chiamato solo una volta e quindi variabile locale se caricato.
Con IL_0034 delegato cache:
IL_002d: ldarg.0
IL_002e: ldftn instance void Adaptive.Aeron.Samples.IpcThroughput.IpcThroughput/Subscriber::OnFragment(class [Adaptive.Agrona]Adaptive.Agrona.IDirectBuffer, int32, int32, class [Adaptive.Aeron]Adaptive.Aeron.LogBuffer.Header)
IL_0034: newobj instance void [Adaptive.Aeron]Adaptive.Aeron.LogBuffer.FragmentHandler::.ctor(object, native int)
IL_0039: stloc.3
IL_003a: br.s IL_005a
// loop start (head: IL_005a)
IL_003c: ldloc.0
IL_003d: ldloc.3
IL_003e: ldsfld int32 Adaptive.Aeron.Samples.IpcThroughput.IpcThroughput::MessageCountLimit
IL_0043: callvirt instance int32 [Adaptive.Aeron]Adaptive.Aeron.Image::Poll(class [Adaptive.Aeron]Adaptive.Aeron.LogBuffer.FragmentHandler, int32)
IL_0048: stloc.s fragmentsRead
Con allocazioni temporanee IL_0037:
IL_002c: stloc.2
IL_002d: br.s IL_0058
// loop start (head: IL_0058)
IL_002f: ldloc.0
IL_0030: ldarg.0
IL_0031: ldftn instance void Adaptive.Aeron.Samples.IpcThroughput.IpcThroughput/Subscriber::OnFragment(class [Adaptive.Agrona]Adaptive.Agrona.IDirectBuffer, int32, int32, class [Adaptive.Aeron]Adaptive.Aeron.LogBuffer.Header)
IL_0037: newobj instance void [Adaptive.Aeron]Adaptive.Aeron.LogBuffer.FragmentHandler::.ctor(object, native int)
IL_003c: ldsfld int32 Adaptive.Aeron.Samples.IpcThroughput.IpcThroughput::MessageCountLimit
IL_0041: callvirt instance int32 [Adaptive.Aeron]Adaptive.Aeron.Image::Poll(class [Adaptive.Aeron]Adaptive.Aeron.LogBuffer.FragmentHandler, int32)
IL_0046: stloc.s fragmentsRead
Perché il codice con le allocazioni è più veloce qui? Cosa è necessario per evitare allocazioni ma mantenere le prestazioni?
(test su .NET 4.5.2/4.6.1, x64, di uscita, su due macchine diverse)
Aggiornamento
Qui è esempio standalone che si comporta come previsto: delegato cache esegue più di 2x più veloce con 4 sec contro 11 sec. Quindi la domanda è specifica per il progetto di riferimento - quali problemi sottili con il compilatore JIT o qualcos'altro potrebbero causare il risultato inaspettato?
using System;
using System.Diagnostics;
namespace TestCachedDelegate {
public delegate int TestDelegate(int first, int second);
public static class Program {
static void Main(string[] args)
{
var tc = new TestClass();
tc.Run();
}
public class TestClass {
public void Run() {
var sw = new Stopwatch();
sw.Restart();
for (int i = 0; i < 1000000000; i++) {
CallDelegate(Add, i, i);
}
sw.Stop();
Console.WriteLine("Non-cached: " + sw.ElapsedMilliseconds);
sw.Restart();
TestDelegate dlgCached = Add;
for (int i = 0; i < 1000000000; i++) {
CallDelegate(dlgCached, i, i);
}
sw.Stop();
Console.WriteLine("Cached: " + sw.ElapsedMilliseconds);
Console.ReadLine();
}
public int CallDelegate(TestDelegate dlg, int first, int second) {
return dlg(first, second);
}
public int Add(int first, int second) {
return first + second;
}
}
}
}
vi suggerisco di postare un [MCVE] del problema che non è collegato con il vostro codice specifico del dominio, quindi questo può essere riprodotto su altre macchine. –
Il commit è un po 'poco chiaro, introducendo una variabile con lo stesso nome del campo della classe ad eccezione del trattino basso principale e della vecchia versione che si riferisce al campo della classe. Sei sicuro di aver provato con la variabile locale, spero? – hvd
@hvd Le variabili locali all'interno del ciclo si comportano allo stesso modo dell'utilizzo del metodo all'interno della funzione '.Poll()'. La variabile locale all'esterno del ciclo si comporta in modo simile a un campo. –