Являются ли лямбда-функции более быстрыми, чем функции делегатов/анонимных?
Я предположил, что lambda functions
, delegates
и anonymous functions
с тем же самым телом будут иметь одинаковую "скорость", однако, выполняя следующую простую программу:
static void Main(string[] args)
{
List<int> items = new List<int>();
Random random = new Random();
for (int i = 0; i < 10000000; i++)
{
items.Add(random.Next());
}
Stopwatch watch;
IEnumerable<int> result;
Func<int, bool> @delegate = delegate(int i)
{
return i < 500;
};
watch = Stopwatch.StartNew();
result = items.Where(@delegate);
watch.Stop();
Console.WriteLine("Delegate: {0}", watch.Elapsed.TotalMilliseconds);
Func<int, bool> lambda = i => i < 500;
watch = Stopwatch.StartNew();
result = items.Where(lambda);
watch.Stop();
Console.WriteLine("Lambda: {0}", watch.Elapsed.TotalMilliseconds);
watch = Stopwatch.StartNew();
result = items.Where(i => i < 500);
watch.Stop();
Console.WriteLine("Inline: {0}", watch.Elapsed.TotalMilliseconds);
Console.ReadLine();
}
Я получаю:
Делегат: 4.2948 мс
Лямбда: 0,0019 мс
Анонимный: 0,0034 мс
Несмотря на незначительность, почему эти три - очевидно одинаковые - методы работают на разных скоростях? Что происходит под капотом?
Update:
Как было предложено в комментариях, следующие "силы" Where
, вызывая ToList()
на нем. Кроме того, для добавления дополнительных данных запуска добавлен цикл:
while (true)
{
List<int> items = new List<int>();
Random random = new Random();
for (int i = 0; i < 10000000; i++)
{
items.Add(random.Next());
}
Stopwatch watch;
IEnumerable<int> result;
Func<int, bool> @delegate = delegate(int i)
{
return i < 500;
};
watch = Stopwatch.StartNew();
result = items.Where(@delegate).ToList();
watch.Stop();
Console.WriteLine("Delegate: {0}", watch.Elapsed.TotalMilliseconds);
Func<int, bool> lambda = i => i < 500;
watch = Stopwatch.StartNew();
result = items.Where(lambda).ToList();
watch.Stop();
Console.WriteLine("Lambda: {0}", watch.Elapsed.TotalMilliseconds);
watch = Stopwatch.StartNew();
result = items.Where(i => i < 500).ToList();
watch.Stop();
Console.WriteLine("Inline: {0}", watch.Elapsed.TotalMilliseconds);
Console.WriteLine(new string('-', 12));
}
Приведенный выше код приводит к ~ 120 мс для каждой функции.
Ответы
Ответ 1
Лямбда-выражение является анонимной функцией. "Анонимная функция" означает либо выражение лямбда, либо анонимный метод (это то, что вы назвали "делегатом" в вашем коде).
Все три операции используют делегаты. Второй и третий используют лямбда-выражения. Все три будут выполняться одинаково с теми же характеристиками производительности.
Обратите внимание, что разница между производительностью может быть разной:
Func<int, int> func = x => ...;
for (int i = 0; i < 10000; i++) {
CallFunc(func);
}
и
for (int i = 0; i < 10000; i++) {
CallFunc(x => ...) // Same lambda as before
}
Это зависит от того, способен ли компилятор кэшировать делегат, созданный выражением лямбда. Это, в свою очередь, зависит от того, захватывает ли он переменные и т.д.
Например, рассмотрим этот код:
using System;
using System.Diagnostics;
class Test
{
const int Iterations = 1000000000;
static void Main()
{
AllocateOnce();
AllocateInLoop();
}
static void AllocateOnce()
{
int x = 10;
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
Func<int, int> allocateOnce = y => y + x;
for (int i = 0; i < Iterations; i++)
{
sum += Apply(i, allocateOnce);
}
sw.Stop();
Console.WriteLine("Allocated once: {0}ms", sw.ElapsedMilliseconds);
}
static void AllocateInLoop()
{
int x = 10;
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
for (int i = 0; i < Iterations; i++)
{
sum += Apply(i, y => y + x);
}
sw.Stop();
Console.WriteLine("Allocated in loop: {0}ms", sw.ElapsedMilliseconds);
}
static int Apply(int loopCounter, Func<int, int> func)
{
return func(loopCounter);
}
}
Компилятор умный, но все равно разница. Используя Reflector, мы можем видеть, что AllocateInLoop
эффективно скомпилирован для:
private static void AllocateInLoop()
{
Func<int, int> func = null;
int x = 10;
Stopwatch stopwatch = Stopwatch.StartNew();
int sum = 0;
for (int i = 0; i < Iterations; i++)
{
if (func == null)
{
func = y => y + x;
}
sum += Apply(i, func);
}
stopwatch.Stop();
Console.WriteLine("Allocated in loop: {0}ms", stopwatch.ElapsedMilliseconds);
}
Таким образом, создается только один экземпляр делегата, но в нем существует дополнительная логика - дополнительный тест нечетности на каждой итерации.
На моей машине разница в производительности составляет 15%.
Ответ 2
Результаты других людей показывают, что производительность одинакова:
http://blogs.microsoft.co.il/blogs/alex_golesh/archive/2007/12/11/anonymous-delegates-vs-lambda-expressions-vs-function-calls-performance.aspx
Как отмечается в комментариях, микро-тесты часто вводят в заблуждение. Слишком много факторов, над которыми у вас нет контроля, оптимизации JIT, циклов сбора мусора и т.д.
Смотрите этот вопрос:
Если не использовать лямбда-выражения
Наконец, я думаю, что ваш тест в корне ошибочен! Вы используете метод расширения Linq Where
для выполнения вашего кода. Однако Linq использует ленивую оценку, ваш код будет выполнен только в том случае, если вы начнете повторять результаты!