Почему существует такая большая разница в исполнении различных способов передачи делегатов?

Я пытался сравнить три разных способа передачи делегата функции в С# - с помощью lambda, делегатом и прямой ссылкой. Меня действительно удивил метод прямой ссылки (т.е. ComputeStringFunctionViaFunc(object[i].ToString)) был в шесть раз медленнее других методов. Кто-нибудь знает, почему это?

Полный код выглядит следующим образом:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.CompilerServices;

namespace FunctionInvocationTest
{
    class Program
    {
        static void Main(string[] args)
        {
            object[] objectArray = new object[10000000];
            for (int i = 0; i < objectArray.Length; ++i) { objectArray[i] = new object(); }

            ComputeStringFunction(objectArray[0]);
            ComputeStringFunctionViaFunc(objectArray[0].ToString);
            ComputeStringFunctionViaFunc(delegate() { return objectArray[0].ToString(); });
            ComputeStringFunctionViaFunc(() => objectArray[0].ToString());

            System.Diagnostics.Stopwatch s = new System.Diagnostics.Stopwatch();
            s.Start();
            for (int i = 0; i < objectArray.Length; ++i)
            {
                ComputeStringFunction(objectArray[i]);
            }
            s.Stop();
            Console.WriteLine(s.Elapsed.TotalMilliseconds);

            s.Reset();
            s.Start();
            for (int i = 0; i < objectArray.Length; ++i)
            {
                ComputeStringFunctionViaFunc(delegate() { return objectArray[i].ToString(); });
            }
            s.Stop();
            Console.WriteLine(s.Elapsed.TotalMilliseconds);

            s.Reset();
            s.Start();
            for (int i = 0; i < objectArray.Length; ++i)
            {
                ComputeStringFunctionViaFunc(objectArray[i].ToString);
            }
            s.Stop();
            Console.WriteLine(s.Elapsed.TotalMilliseconds);

            s.Reset();
            s.Start();
            for (int i = 0; i < objectArray.Length; ++i)
            {
                ComputeStringFunctionViaFunc(() => objectArray[i].ToString());
            }
            s.Stop();
            Console.WriteLine(s.Elapsed.TotalMilliseconds);

            Console.ReadLine();
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static void ComputeStringFunction(object stringFunction)
        {
        }

        public static void ComputeStringFunctionViaFunc(Func<string> stringFunction)
        {
        }
    }
}

Ответ 1

После исправления кода для фактического вызова ToString()/stringFunction() и измерения с помощью Mono 2.10.9:

ComputeStringFunctionViaFunc(objectArray[i].ToString); является медленным, потому что object.ToString является виртуальным. Каждый объект проверяется в случае, если он переопределяет ToString и должен быть вызван переопределенный ToString. Ваши другие делегаты созданы для ссылки на не виртуальную функцию (быструю), которая напрямую вызывает виртуальную функцию (также быстро). Тот факт, что это причина, может быть замечена при изменении сгенерированного IL для изменения

ldelem.ref
dup 
ldvirtftn instance string object::ToString()

к

ldelem.ref
ldftn instance string object::ToString()

который всегда ссылается на object.ToString, никогда не является переопределяющей функцией. Затем три метода принимают примерно одно и то же время.

Обновление: один дополнительный метод, связанный непосредственно с objectArray[i], но по-прежнему вызывающий ToString виртуально:

for (int i = 0; i < objectArray.Length; ++i)
{
    ComputeStringFunctionViaFunc(objectArray[i].ToStringHelper);
}

static class Extensions
{
    public static string ToStringHelper(this object obj)
    {
        return obj.ToString();
    }
}

также дает примерно те же тайминги, что и другие не виртуальные делегаты.

Ответ 2

Давайте рассмотрим, что вы делаете в каждом случае:

Этот парень вообще не "создает" функцию. Он ищет элемент (в данном случае объект) в массиве и передает элемент в качестве параметра функции:

// The cost of doing the array lookup happens right here, before 
// ComputeStringFunction is called
ComputeStringFunction(objectArray[i]);

Это создает делегат без параметров и передает его функции. Сам делегат никогда не называется:

// Because ComputeStringFunctionViaFunc doesn't do anything, the
// statement objectArray[i] is never evaluated, so the only cost 
// is that of creating a delegate
ComputeStringFunctionViaFunc(delegate() { return objectArray[i].ToString(); });

Это делает то же самое, что и первое, за исключением того, что вместо передачи элемента сразу после извлечения из массива он вызывает .ToString() на нем. Опять же, здесь не создается никакой функции:

Как и первый, у этого есть стоимость поиска массива спереди, но затем создается делегат, ссылающийся на метод .ToString элемента (спасибо @hvd за это). Как и другие,.ToString никогда не оценивается. Стоимость (опять же, спасибо @hvd) заключается в поиске виртуального метода.

// The cost of doing the array lookup happens right here
ComputeStringFunctionViaFunc(objectArray[i].ToString);

Наконец, это создает функцию, используя лямбда и замыкание над элементом массива, и передает эту лямбду на функцию. В зависимости от сигнатуры функций лямбда может быть скомпилирована или нет:

// Again, we create a delegate but don't call it, so the array
// lookup and .ToString are never evaluated.
ComputeStringFunctionViaFunc(() => objectArray[i].ToString());

Важно отметить, что оценка поиска массива задерживается во втором и четвертом, в то время как он не задерживается в первом и третьем.

Эти тесты несколько бессмысленны, потому что все они делают совершенно разные вещи. Есть почти наверняка лучшие способы создания делегатов.