Ostatnio znalazłem fajny przykład pokazujący jak można nieoczekiwanie pogorszyć wydajność aplikacji. Załóżmy, że mamy metodę, która jako parametr wejściowy przyjmuje funkcję:
private static int Find(Predicate<int> filter) { // jakas logika - nie wazne... if (filter(0)) return 1; else return -1; }
Nie zwracajmy uwagi na logikę i bezsensowne wartości. Chodzi mi o to, że przekazujemy jakiś wskaźnik na metodę, tutaj konkretnie jest to Predicate<int> np.:
private static bool CustomFilter(int value) { return true; }
Następnie w pętli, chcemy wykonać serie przeszukiwań na różnych danych:
for (int i = 0; i < 10000000; i++) { Find(CustomFilter); }
Powyższy kod wygląda niewinnie, jednak zajrzyjmy do IL Asm:
IL_0000: ldc.i4.0 IL_0001: stloc.0 IL_0002: br.s IL_001a // loop start (head: IL_001a) IL_0004: ldnull IL_0005: ldftn bool ConsoleApplication15.Program::CustomFilter(int32) IL_000b: newobj instance void class [mscorlib]System.Predicate`1<int32>::.ctor(object, native int) IL_0010: call int32 ConsoleApplication15.Program::Find(class [mscorlib]System.Predicate`1<int32>) IL_0015: pop IL_0016: ldloc.0 IL_0017: ldc.i4.1 IL_0018: add IL_0019: stloc.0 IL_001a: ldloc.0 IL_001b: ldc.i4 10000000 IL_0020: blt.s IL_0004 // end loop
Jak widzimy, w każdej iteracji, tuż przed wywołaniem Find jest tworzona nowa delegata:
IL_000b: newobj instance void class [mscorlib]System.Predicate`1<int32>::.ctor(object, native int) IL_0010: call int32 ConsoleApplication15.Program::Find(class [mscorlib]System.Predicate`1<int32>)
Poprawne rozwiązanie to:
Predicate<int> customFilter = CustomFilter; for (int i = 0; i < 10000000; i++) { Find(customFilter); }
IL:
IL_0000: ldnull IL_0001: ldftn bool ConsoleApplication15.Program::CustomFilter(int32) IL_0007: newobj instance void class [mscorlib]System.Predicate`1<int32>::.ctor(object, native int) IL_000c: stloc.0 IL_000d: ldc.i4.0 IL_000e: stloc.1 IL_000f: br.s IL_001c // loop start (head: IL_001c) IL_0011: ldloc.0 IL_0012: call int32 ConsoleApplication15.Program::Find(class [mscorlib]System.Predicate`1<int32>) IL_0017: pop IL_0018: ldloc.1 IL_0019: ldc.i4.1 IL_001a: add IL_001b: stloc.1 IL_001c: ldloc.1 IL_001d: ldc.i4 10000000 IL_0022: blt.s IL_0011 // end loop
W tym przypadku, nowy obiekt jest tworzony tylko raz – przed pętlą. Napiszmy również prosty benchmark:
class Program { private static void Main(string[] args) { Stopwatch stopwatch = Stopwatch.StartNew(); TestGoodSolution(); Console.WriteLine(stopwatch.ElapsedTicks); stopwatch = Stopwatch.StartNew(); TestBadSolution(); Console.WriteLine(stopwatch.ElapsedTicks); } private static void TestBadSolution() { for (int i = 0; i < 10000000; i++) { Find(CustomFilter); } } private static void TestGoodSolution() { Predicate<int> customFilter = CustomFilter; for (int i = 0; i < 10000000; i++) { Find(customFilter); } } private static bool CustomFilter(int value) { return true; } private static int Find(Predicate<int> filter) { // jakas logika - nie wazne... if (filter(0)) return 1; else return -1; } }
Wynik:
Różnica spora a w rzeczywistości jest jeszcze większa. Pamiętajmy, że powyższy benchmark nie bierze pod uwagi Garbage Collector. Jeśli pętla jest wykonywana kilka tysięcy razey to również taka liczba obiektów zostanie zainicjalizowana i potem zwolniona przez GC, co może być bardzo kłopotliwe (promocja do następnej generacji itp.).