Проблемы производительности / возможности / рекомендации C # Lambda - PullRequest
28 голосов
/ 29 ноября 2011

Я тестирую различия в производительности, используя различные синтаксисы лямбда-выражений.Если у меня есть простой метод:

public IEnumerable<Item> GetItems(int point)
{
    return this.items.Where(i => i.IsApplicableFor(point));
}

, то здесь происходит подъем переменной, связанный с параметром point, потому что это свободная переменная с точки зрения лямбды.Если бы я назвал этот метод миллион раз, было бы лучше оставить его как есть или изменить его каким-либо образом, чтобы улучшить его производительность?

Какие варианты у меня есть и какие на самом деле есть?возможно? Насколько я понимаю, мне нужно избавиться от свободных переменных, чтобы компилятору не приходилось создавать класс замыкания и создавать его экземпляры при каждом вызове этого метода.Это создание обычно занимает значительное количество времени по сравнению с незакрытыми версиями.

Дело в том, что я хотел бы придумать какое-то лямбда-руководство по написанию , которое обычно работает, потому что оноКажется, я теряю время каждый раз, когда пишу сильно лямбда-выражение.Я должен вручную проверить его, чтобы убедиться, что он будет работать, потому что я не знаю, каким правилам следовать.

Альтернативный метод

и пример кода консольного приложения

IМы также написали другую версию того же метода, который не требует поднятия переменной (по крайней мере, я думаю, что это не так, но вы, ребята, которые понимают это, дайте мне знать, если это так):

public IEnumerable<Item> GetItems(int point)
{
    Func<int, Func<Item, bool>> buildPredicate = p => i => i.IsApplicableFor(p);
    return this.items.Where(buildPredicate(point));
}

Проверьте Суть здесь .Просто создайте консольное приложение и скопируйте весь код в файл Program.cs внутри блока namespace.Вы увидите, что второй пример намного медленнее, хотя он и не использует свободные переменные.

Противоречивый пример

Причина, по которой я хотел бы построить лучшую лямбду Руководство по использованию состоит в том, что я столкнулся с этой проблемой до , и, к моему удивлению, одна из них работала быстрее, когда использовалось построитель предикатов лямбда-выражение.

Теперь объясните это тогда.Я совершенно потерян, потому что может оказаться, что я не буду использовать лямбда-выражения вообще, когда знаю, что в моем коде есть какой-то метод интенсивного использования.Но я хотел бы избежать такой ситуации и разобраться во всем этом.

Редактировать

Ваши предложения не работают

Я пытался реализоватьПользовательский класс поиска, который внутренне работает аналогично тому, что компилятор делает со свободной переменной lambda.Но вместо того, чтобы иметь класс замыкания, я реализовал элементы экземпляров, которые имитируют похожий сценарий.Это код:

private int Point { get; set; }
private bool IsItemValid(Item item)
{
    return item.IsApplicableFor(this.Point);
}

public IEnumerable<TItem> GetItems(int point)
{
    this.Point = point;
    return this.items.Where(this.IsItemValid);
}

Интересно, что это работает так же медленно, как и медленная версия.Я не знаю почему, но, кажется, ничего не делает, кроме быстрого.Он использует те же функциональные возможности, потому что эти дополнительные члены являются частью одного и того же экземпляра объекта.Тем не мение. Я сейчас в замешательстве !

Я обновил Источник Gist с этим последним дополнением, так что вы можете проверить себя.

Ответы [ 4 ]

3 голосов
/ 29 ноября 2011

Что заставляет вас думать, что вторая версия не требует какого-либо переменного подъема? Вы определяете Func с помощью лямбда-выражения, и для этого потребуются те же самые хитрости, что и в первой версии.

Кроме того, вы создаете Func, который возвращает Func, который немного изгибает мой мозг и почти наверняка потребует переоценки при каждом вызове.

Я бы посоветовал вам скомпилировать это в режиме выпуска, а затем использовать ILDASM для проверки сгенерированного IL. Это должно дать вам некоторое представление о том, какой код генерируется.

Еще один тест, который вы должны запустить, который даст вам больше понимания, - это сделать вызов предиката отдельной функцией, которая использует переменную в области видимости класса. Что-то вроде:

private DateTime dayToCompare;
private bool LocalIsDayWithinRange(TItem i)
{
    return i.IsDayWithinRange(dayToCompare);
}

public override IEnumerable<TItem> GetDayData(DateTime day)
{
    dayToCompare = day;
    return this.items.Where(i => LocalIsDayWithinRange(i));
}

Это скажет вам, если подведение переменной day на самом деле вам чего-то стоит.

Да, для этого требуется больше кода, и я бы не советовал вам его использовать. Как вы указали в своем ответе на предыдущий ответ, в котором предлагалось нечто подобное, это создает то, что составляет замыкание с использованием локальных переменных. Дело в том, что либо вы, либо компилятор должны сделать что-то подобное, чтобы все заработало. Помимо написания чистого итеративного решения, вы не сможете применить магию, которая помешала бы компилятору сделать это.

Здесь я хочу сказать, что «создание замыкания» в моем случае - это простое присвоение переменной. Если это значительно быстрее, чем ваша версия с лямбда-выражением, вы знаете, что в коде, который компилятор создает для замыкания, есть некоторая неэффективность.

Я не уверен, откуда вы получаете информацию о необходимости устранения свободных переменных и о стоимости закрытия. Можете ли вы дать мне несколько ссылок?

1 голос
/ 29 ноября 2011

Ваш второй метод работает в 8 раз медленнее, чем первый для меня. Как говорит @DanBryant в комментариях, это связано с созданием и вызовом делегата внутри метода, а не с подъемом переменной.

Ваш вопрос сбивает с толку, поскольку мне кажется, что вы ожидаете, что второй образец будет быстрее первого. Я также прочитал это, поскольку первое является каким-то недопустимо медленным из-за «переменного подъема». Во втором примере все еще есть свободная переменная (point), но она добавляет дополнительные издержки - я не понимаю, почему вы думаете, что она удаляет свободную переменную.

Как подтверждает опубликованный вами код, первый пример выше (с использованием простого встроенного предиката) выполняет на 10% медленнее, чем простой цикл for - из вашего кода:

foreach (TItem item in this.items)
{
    if (item.IsDayWithinRange(day))
    {
        yield return item;
    }
}

Итак, в итоге:

  • Цикл for является самым простым подходом и является «лучшим случаем».
  • Встроенный предикат медленнее на * из-за некоторых дополнительных издержек.
  • Создание и вызов Func, который возвращает Func в каждой итерации, значительно медленнее, чем любая из них.

Не думаю, что это удивительно. «Рекомендация» - использовать встроенный предикат - если он работает плохо, упростите, перейдя к прямому циклу.

0 голосов
/ 21 февраля 2012

Когда выражение LINQ, использующее отложенное выполнение, выполняется в той же области, в которой находятся свободные переменные, на которые он ссылается, компилятор должен обнаружить это и не создавать замыкание через лямбду, поскольку оно не требуется.

проверить это можно, протестировав его следующим образом:

public class Test
{
   public static void ExecuteLambdaInScope()
   {
      // here, the lambda executes only within the scope
      // of the referenced variable 'add'

      var items = Enumerable.Range(0, 100000).ToArray();

      int add = 10;  // free variable referenced from lambda

      Func<int,int> f = x => x + add;

      // measure how long this takes:
      var array = items.Select( f ).ToArray();  
   }

   static Func<int,int> GetExpression()
   {
      int add = 10;
      return x => x + add;  // this needs a closure
   }

   static void ExecuteLambdaOutOfScope()
   {
      // here, the lambda executes outside the scope
      // of the referenced variable 'add'

      Func<int,int> f = GetExpression();

      var items = Enumerable.Range(0, 100000).ToArray();

      // measure how long this takes:
      var array = items.Select( f ).ToArray();  
   }

}
0 голосов
/ 29 ноября 2011

Я профилировал ваш эталон для вас и определил много вещей:

Прежде всего, он проводит половину своего времени на линии return this.GetDayData(day).ToList();, вызывая ToList. Если вы удалите это и вместо этого вручную выполните итерации по результатам, вы можете измерить относительные различия в методах.

Во-вторых, поскольку IterationCount = 1000000 и RangeCount = 1, вы синхронизируете инициализацию различных методов, а не количество времени, которое требуется для их выполнения. Это означает, что в вашем профиле выполнения доминируют создание итераторов, экранирование записей переменных и делегатов, а также сотни последующих сборок мусора gen0, которые возникают в результате создания всего этого мусора.

В-третьих, «медленный» метод действительно медленный на x86, но примерно такой же быстрый, как «быстрый» метод на x64. Я считаю, что это связано с тем, как разные JITters создают делегатов. Если вы исключаете создание делегата из результатов, «быстрый» и «медленный» методы одинаковы по скорости.

В-четвертых, если вы на самом деле вызываете итераторы значительное количество раз (на моем компьютере, нацеленный на x64, с RangeCount = 8), «slow» на самом деле быстрее, чем «foreach», а «fast» быстрее, чем все они .

В заключение, аспект "подъема" незначителен. Тестирование на моем ноутбуке показывает, что для захвата такой переменной, как вы, требуется дополнительная 10 нс каждый раз, когда создается лямбда ( не каждый раз, когда она вызывается), и это включает в себя дополнительные издержки GC , Кроме того, в то время как создает итератор в вашем методе "foreach" несколько быстрее, чем создание лямбда-выражений, фактически вызов этого итератора медленнее, чем вызов лямбда-выражений.

Если несколько дополнительных наносекунд, необходимых для создания делегатов, слишком много для вашего приложения, рассмотрите возможность их кэширования. Если вам требуются параметры для этих делегатов (т. Е. Замыканий), рассмотрите возможность создания собственных классов замыканий, чтобы их можно было создать один раз, а затем просто измените свойства, когда вам потребуется повторно использовать их делегаты. Вот пример:

public class SuperFastLinqRangeLookup<TItem> : RangeLookupBase<TItem>
    where TItem : RangeItem
{

    public SuperFastLinqRangeLookup(DateTime start, DateTime end, IEnumerable<TItem> items)
        : base(start, end, items)
    {
        // create delegate only once
        predicate = i => i.IsDayWithinRange(day);
    }

    DateTime day;
    Func<TItem, bool> predicate;

    public override IEnumerable<TItem> GetDayData(DateTime day)
    {
        this.day = day; // set captured day to correct value
        return this.items.Where(predicate);
    }
}
...