Code Review: Przekazywanie metody jako parametr

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:

image

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.).

Leave a Reply

Your email address will not be published.