Пойдём издалека. Несколько дней назад, ковыряя забытый всеми C# 4.0 dynamic, задумался над такой штукой - что можно было бы делать, если бы язык предоставлял функциям доступ (при их желании) к некоторой реификации (материализованном представлении в виде значения) факта вызова этих функций в клиентском коде. То есть чтобы некоторая магическая функция F() могла уметь отличать факты вызова себя из разных мест клиентского кода. На первый взгляд это кажется совершенно дурной затеей - кто потом будет отлаживать функцию, поведение которой может отличаться при вызове из разных участков клиентского кода?

Однако, есть одна достаточно узкая область, где это может приносить некоторые плюшки - кэширование. В проекте, над которым я работаю, кэширование составляет очень важную составляющую продукта и код им обильно усыпан. Я заметил, что достаточно часто обращения к кэшу могут иметь либо параметры, лежащие в каких-то определённых рамках, специфичных конкретно этому факту обращения к кэшу, либо вообще константные параметры:

public IDeclaredType ResourceKey {
  get { return GetType("System.Windows.ResourceKey"); }
}

public IDeclaredType EventSetter {
  get { return GetType("System.Windows.EventSetter"); }
}

public IDeclaredType Trigger {
  get { return GetType("System.Windows.Trigger"); }
}

Функция GetType() тут осуществляет поиск в словаре по ключу - полному имени типа - и производит поиск типа по сборкам в случае промаха. Функцию GetType() можно считать чистой функцией в рамках промежутка между инвалидацией всего кэша. Понятно, что поиск в кэше-словаре фактически не особо и нужны - в каждом случае использования GetType() можно кэшировать результат в инвидиадуальном поле и таким образом превратить поиск по словарю в единственный if. Проблема в том, что в моём коде сейчас 121 такой вызов и в 90% случаев параметры контантны - создавать поле и явно проверять его (ещё и под lock‘ом) на каждый usage уж совсем не хочется и не разумно. При этом инвалидируется весь кэш, мягко говоря, достаточно часто (практически на каждое нажатие пользоваталем клавиши (!) вне тел методов, так как любое такое нажатие может привести к появлению новых/исчезновению старых типов. Даже при такой частоте инвалидации, доля попаданий этого кэша составляет 99.993%, так что он всё равно очень клёвый) и превращать процедуру инвалидации в зануление тьмы полей просто неприемлимо.

Так вот, имея возможность получать на стороне функции GetType() некоторый объект, характеризующий конкретный факт её вызова, можно было бы построить гораздо более гранулярный кэш. Такие техники уже применяются в том же C#, но об этом речь пойдёт чуть позже. Давайте попробуем представить код на языке с расширением, который я бы назвал first-class call site (то есть “вызывающая сторона, являющаяся значение”), упрощающим использование подобных техник для написания всеми любимых функций вычисления факториалов:

// декларация:
static int Fact(int x, Dictionary<int, int> cache = callsite) {
  int result;
  if (cache.TryGetValue(x, out result)) return result;

  result = (x == 0) ? 1 : x * Fact(x - 1, cache);
  cache[x] = result;
  return result;
}

// использование:
Fact(6);

Всё достаточно просто и явно - надо лишь научить компилятор генерировать скрытые поля на стороне вызова и автоматически передавать их в опциональные параметры, отмеченные некоторой аннотацией, например (я пока не пытаюсь ответить на вопросы инвалидации таких “разбросанных” кэшей).

Подобную технику использует C# 4.0 dynamic - вместо того, чтобы компилировать, например, выражение доступа к члену объекта типа dynamic вида d.Foo, в простой вызов какой-нибудь функции типа GetMemberDynamic(object target, string name), компилятор совершает гораздо больше телодвижений. На каждую dynamic-операцию один раз создаётся и хранится в статическом поле специальный callsite-объект, который хранит всякую инфраструктурную фигню и самый обычный .NET-делегат, который на самом деле и вызывается на каждую динамическую операцию. При первом исполнении кода, инфраструктура C# dynamic согласно статическим правилам компилятора C# пытается разрезолвить динамическую операцию и в случае успеха компилирует (!) постой делегат, совершающий требуемую операцию. Помимо требуемой операции в делегат попадает специальный инфраструктурный код, который будет осуществлять проверку - можно-ли при повторном вызове динамической операции переиспользовать скомпилированный делегат? Например, если код foo.Bar первый раз разрезолвился в обращение к свойству Foo у класса типа ConcreteFoo, то скомпилированный делегат можно будет переиспользовать если при повторном вызове foo будет типа ConcreteFoo. В противном случае процедуру резолва придётся повторить ещё раз и инфраструктура C# dynamic подменит старый делегат, скомпилировав новый (причём последние 10 делегатов останутся в callsite-объекте, так как могут ещё пригодиться).

Всё это шаманство приводит к тому, что немного “прогревшись”, большинство динамических операции обращаются в очень эффективные (по сравнению с первым резолвом, возбуждающим компиляцию) вызовы обычных делегатов с одним-двумя if, что в некотором роде доказывает состоятельность и целесообразность кэширования на стороне вызова в подобных сценариях. Фактически, эта техника аналогична модификации исполняемого кода во время исполнения для кэширования (polymorphic inline caching), которую делает большинство современных JavaScript-движков или, например, CLR JIT для компилирования вызовов через интерфейс.

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