Почему не кешируются не захватывающие деревья выражений, которые инициализируются с помощью лямбда-выражений? - PullRequest
0 голосов
/ 13 октября 2018

Рассмотрим следующий класс:

class Program
{
    static void Test()
    {
        TestDelegate<string, int>(s => s.Length);

        TestExpressionTree<string, int>(s => s.Length);
    }

    static void TestDelegate<T1, T2>(Func<T1, T2> del) { /*...*/ }

    static void TestExpressionTree<T1, T2>(Expression<Func<T1, T2>> exp) { /*...*/ }
}

Это то, что генерирует компилятор (в немного менее читаемом виде):

class Program
{
    static void Test()
    {
        // The delegate call:
        TestDelegate(Cache.Func ?? (Cache.Func = Cache.Instance.FuncImpl));

        // The expression call:
        var paramExp = Expression.Parameter(typeof(string), "s");
        var propExp = Expression.Property(paramExp, "Length");
        var lambdaExp = Expression.Lambda<Func<string, int>>(propExp, paramExp);
        TestExpressionTree(lambdaExp);
    }

    static void TestDelegate<T1, T2>(Func<T1, T2> del) { /*...*/ }

    static void TestExpressionTree<T1, T2>(Expression<Func<T1, T2>> exp) { /*...*/ }

    sealed class Cache
    {
        public static readonly Cache Instance = new Cache();

        public static Func<string, int> Func;

        internal int FuncImpl(string s) => s.Length;
    }
}

Таким образом,делегат, переданный с первым вызовом, инициализируется один раз и повторно используется для нескольких вызовов Test.

Однако дерево выражений, переданное со вторым вызовом, не используется повторно - новое лямбда-выражение инициализируется при каждом вызове Test.

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

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

Я думаю, мне нужно уточнить, почему я думаю, что деревья выражений подходят для кэширования.

  1. Полученное дерево выражений известно во время компиляции (ну, это равно создается компилятором).
  2. Они неизменны.Таким образом, в отличие от примера массива, приведенного в X39 ниже, дерево выражений не может быть изменено после его инициализации, и поэтому его можно безопасно кэшировать.
  3. В базе кода может быть только столько деревьев выражений- Опять же, я говорю о тех, которые могут быть кэшированы, то есть о тех, которые инициализируются с помощью лямбда-выражений (а не тех, которые создаются вручную) без захвата какого-либо внешнего состояния / переменной.Автоинтернация строковых литералов была бы похожим примером.
  4. Они должны быть пройдены - их можно скомпилировать для создания делегата, но это не их основная функция.Если кому-то нужен скомпилированный делегат, он может принять его (Func<T> вместо Expression<Func<T>>).Принятие дерева выражений означает, что оно будет использоваться в качестве структуры данных.Таким образом, «они должны быть скомпилированы первыми» не является разумным аргументом против кэширования деревьев выражений.

Я спрашиваю о потенциальных недостатках кэширования этих деревьев выражений.Требования к памяти, упомянутые svick, являются более вероятным примером.

Ответы [ 2 ]

0 голосов
/ 02 ноября 2018

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

Я написал этот код в компиляторе, как в исходной реализации C # 3, так и вRoslyn rewrite.

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

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

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

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

Когда C # 3.0 разрабатывался, в Visual Studio была определенная дата, когда он должен был быть "выпущен в производство", странный термин с того времени, когда программное обеспечение распространялось в основном на компакт-дисках, которые нельзя было изменить после того, как онибыли напечатаны.Эта дата не была произвольной;скорее следовала целая цепочка зависимостей.Если бы, скажем, SQL Server имел функцию, которая зависела от LINQ, не было бы никакого смысла откладывать выпуск VS до тех пор, пока не выйдет SQL Server того же года, и поэтому расписание VS повлияло на расписание SQL Server, что, в свою очередь, повлияло на другие команды.расписания и т. д.

Таким образом, каждая команда в организации VS представила расписание, и команда с наибольшим количеством дней работы над этим графиком была «длинным полюсом».Команда C # была длинным полюсом для VS, а я был длинным полюсом для команды компиляторов C #, поэтому каждый день, когда я опаздывал с предоставлением функций компилятора, был днем, когда Visual Studio и каждый последующий продуктускользнуть от графика и разочаровать своих клиентов .

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

Как вы заметили, анонимные функции кэшируются.Когда я реализовал лямбда-выражения, я использовал тот же код инфраструктуры, что и для анонимных функций, так что кеш был (1) «потопленной» - работа была уже выполнена, и было бы больше работы, чтобы отключить ее, чем оставить ее включенной, и(2) уже были проверены и проверены моими предшественниками.

Я подумал о реализации аналогичного кэша на деревьях выражений, используя ту же логику, но понял, что это будет (1) работа, которая требует времени, которого у меня уже было мало, и (2) у меня не былоПредставление о влиянии производительности на кэширование такого объекта. Делегаты действительно маленькие .Делегаты являются одним объектом;если делегат логически статичен, что агрессивно кэширует C #, он даже не содержит ссылку на получателя.Деревья выражений, напротив, потенциально огромные деревья .Это граф маленьких объектов, но этот граф потенциально большой.Графики объектов делают работу сборщика мусора тем дольше, чем они живут!

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

Но риск может стоить того, если выгода велика.Так в чем же выгода?Начните с вопроса: «Где используются деревья выражений?»В LINQ запросы, которые будут удалены в базы данных. Это очень дорогая операция как по времени, так и по памяти .Добавление кеша не даст вам большого выигрыша, потому что работа, которую вы собираетесь сделать, в миллионы раз дороже, чем выигрыш;победа это шум.

Сравните это с победой делегатов.Разница между «выделить x => x + 1, затем назвать его« миллион раз »и« проверить кэш, если он не кэшируется, выделить его, вызвать его »заключается в обмене выделения на проверку, которая может сэкономить вам целые наносекунды.Это кажется несущественным, но вызов также займет наносекунды , поэтому в процентном отношении это важно.Кэширование делегатов - явная победа.Кэширование деревьев выражений не близко к чистой победе;нам нужны данные, что это преимущество, которое оправдывает риск.

Поэтому было легко принять решение не тратить время на эту ненужную, вероятно, незаметную, неважную оптимизацию в C # 3.

Во время C # 4 у нас было много более важных дел, чем пересмотреть это решение.

После C # 4 команда разделилась на две подгруппы, одну для переписывания компилятора Roslyn, идругой для реализации async-await в исходной базе кода компилятора.Команда async-await была полностью поглощена реализацией этой сложной и сложной функции, и, конечно, команда была меньше, чем обычно.И они знали, что вся их работа в конечном итоге будет воспроизведена в Рослине, а затем выброшена;этот компилятор был в конце своей жизни.Таким образом, не было никакого стимула тратить время или усилия на добавление оптимизаций.

Предлагаемая оптимизация была в моем списке вещей, которые нужно учитывать, когда я переписывал код в Roslyn, но нашим главным приоритетом было обеспечение работы компилятора до конца.до того, как мы оптимизировали небольшие его части, и я покинул Microsoft в 2012 году, до того, как эта работа была закончена.

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

Итак, если вы хотите, чтобы эта работа была выполнена, у вас есть несколько вариантов.

  • Компилятор с открытым исходным кодом;Вы могли бы сделать это самостоятельно.Если это звучит как большая работа, приносящая очень мало пользы для вас, то теперь у вас есть более понятное понимание того, почему никто не выполнял эту работу с тех пор, как эта функция была внедрена в 2005 году.

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

  • Процесс проектирования открыт.Введите проблему и в этом выпуске укажите убедительную причину, по которой вы считаете, что это улучшение того стоит.С данными.

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

Фактический выигрыш в избежании повторного выделения сложных деревьев выражений позволяет избежать сбор давления , и это серьезная проблема.Многие функции в C # разработаны, чтобы избежать давления при сборе, и деревья выражений НЕ являются одним из них. Мой совет, если вы хотите, чтобы эта оптимизация была сосредоточена на ее влиянии на давление, потому что именно здесь вы найдете самый большой выигрыш и сможете дать самый убедительный аргумент.

0 голосов
/ 17 октября 2018

Компилятор делает то, что он всегда делает, не кэшируя то, что вы вводите в него.

Чтобы понять, что это всегда происходит, посмотрите на передачу нового массива в ваш метод.

this.DoSomethingWithArray(new string[] {"foo","bar" });

получит

IL_0001: ldarg.0
IL_0002: ldc.i4.2
IL_0003: newarr    [mscorlib]System.String
IL_0008: dup
IL_0009: ldc.i4.0
IL_000A: ldstr     "foo"
IL_000F: stelem.ref
IL_0010: dup
IL_0011: ldc.i4.1
IL_0012: ldstr     "bar"
IL_0017: stelem.ref
IL_0018: call      instance void Test::DoSomethingWithArray(string[])

вместо кэширования массива один раз и повторного его использования каждый раз.

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

Чтобы получить кэшированную версию, используйте что-то вроде этого:

private static System.Linq.Expressions.Expression<Func<object, string>> Exp = (obj) => obj.ToString();
...