C# CachedAnonymousMethodDelegate
Попытаюсь немного осветить такую мутную тему, как кэширование экземпляров делегатов компилятором С#.
Не смотря на то, что делегаты нынче вызываются приблизительно так же быстро, как вызовы методов через интерфейс, существует проблема производительности создания экземпляров делегатов. Вообще говоря, “проблема” - это как-то громко звучит, на самом деле заметная деградация производительности может проявиться только на performance critical участках кода, в обычной разработке думать об этом вовсе не следует. Много ссылок по данной теме выкладывали недавно в этом топике на rsdn.
Интерес представляет поведение и генерируемый компилятором код, поэтому рассмотрим такой метод, содержащий лямбда-выражение:
static IEnumerable<Person> FilterDevelopers(this IEnumerable<Person> source) {
return source.Where(x => x.IsDeveloper);
}
Эта запись для многих выглядит очень “натурально” и как-то совершенно забывается, что на самом деле здесь создаётся экземпляр типа делегата:
static IEnumerable<Person> FilterDevelopers(this IEnumerable<Person> source) {
return source.Where(new Func<Person, bool>(x => x.IsDeveloper));
}
В данном примере лямбда-выражение не замыкается на какие-либо внешние переменные, this или поля, поэтому оно может быть (и будет) эффективно скомпилировано в виде обычного статического метода. Тогда возникает вопрос - зачем каждый раз создавать экземпляр делегата? Ведь делегаты в .NET являются неизменяемыми и несколько экземпляров делегатов на один и тот же статический метод абсолютно взаимозаменяемы. Компилятор C# использует это знание и применяет в данном случае кэширование экземпляра в статическом поле, реально скомпилированный код выглядит примерно вот так:
[CompilerGenerated]
static Func<Person, bool> CS9_CachedAnonymousMethodDelegate1;
static IEnumerable<Person> FilterDevelopers(this IEnumerable<Person> source) {
return source.Where(
CS9_CachedAnonymousMethodDelegate1 != null
? CS9_CachedAnonymousMethodDelegate1
: (CS9_CachedAnonymousMethodDelegate1 =
new Func<Person, bool>(x => x.IsDeveloper)));
}
Данное кэширование применяется при создании экземпляров делегатов из любых статических методов, лямбда-выражение выше - лишь частный случай такого метода.
Долгое время я считал, что в случае появления любого замыкания, кэширование становится неприменимо (экземпляры делегатов из лямбда-выражений/анонимных методов, замыкающихся на внешний контекст, не являются взаимозаменяемыми). Однако оказалось, что статические методы - не единственный случай кэширования, подробнее - в следующих постах.
Важно помнить, что описанное кэширование - это implementation detail компилятора в чистом виде, ни в коем случае нельзя строить логику на описанных выше эффектах (ссылочное равенство создаваемых делегатов), хотя это может открыть некоторые забавные возможности…