Comments 25
Я постарался раписать достаточно подробно и понятно, но не знаю, получилось ли. Если нужно добавить информацию про что-то, добавлю.
Ну и конечно открыт вашему фидбеку и гениальным идеям.
тут вышло как-то болеее элегантнее без необходимости трогать Linq.Expression
Это статическая генерация. По сути то же самое, что написать функцию прямо в коде. Только синтаксис чуть более красивый, и все.
Во-вторых, сорс-генераторы генерят C#-код, который потом компилится по сути в то же дерево, что и в статье. То есть это некий шаг назад. Я сразу в Linq.Expression компилю, и не заставляю никого парсить код.
Добавлю, что если перформанс не очень важен, то можно обойтись и без компиляции — имея только примитивный парсер и AST можно легко вычислять значения выражений. Вот когда-то сделал простой примерчик.
Конечно, можно. В библиотеке, о которой идет речь, есть
Entity expr = "1 + 2";
// вычисляет с произвольной наперед заданной точностью
var num = expr.EvalNumerical();
// вычисляет булево значение (если выражение логическое)
var bo = expr.EvalBoolean();
Только это не AST, а полноценное дерево математических выражений. Если хотите, больше всего информации о проекте можно найти на сайте.
А тут, конечно, все сделано только ради производительности.
Кешируем одинаковые поддеревья
Эквивалентен. Я не ограничиваю JIT в кешировании поддеревьев, захочет — сделает. Пока не делает — его проблема, которая не должна волновать пользователя.
Промежуточные результаты перевычисляются несколько раз? JIT'у не хватает контекста, чтобы кешировать результаты вызовов функций, он не знает, есть ли у функций сайдэффекты или нет. Это должен сделать сам программист.
Просто с такой цифрой и без кода результаты тестов вызывают сомнения в самих тестах. Я нисколько не приуменьшаю вару работу, но хотелось бы видеть реальные осмысленные тесты.
Решал задачу связанную с генерацией кода (быстрый доступ к приватным полям/методам классов), только использовал DynamicDelegate.CreateDelegate. Для коротких методов (4-6 инструкций) разница со скомпилированным при сборке кодом была значительной — 10 раз. (но всё равно быстрее, чем чистый reflection)
Кешировании чего именно?
Поддеревьев, как я уже сказал. В начале я упомянул, что выражение — это дерево. Соотвественно, мы можем запоминать значение каждого уникального поддерева, а если мы уже какое-то встречали — подставлять вместо него переменную, в которую мы его записали.
Просто с такой цифрой и без кода результаты тестов вызывают сомнения в самих тестах.
Код тестов я тоже предоставил, посмотрите пожалуйста еще раз.
Попробуйте протестировать вот эту лямбду:
public readonly Func<Complex, Complex, Complex, Complex> NormalComplicatedOptimizedLambda = (a, b, c) => {
var d = Complex.Sin(a + b + Complex.Cos(c));
return d * 6 + Complex.Pow(3, d) + Complex.Tan(d) - Complex.Log(d, 3) + d + d + d + d + d + d + d + d + d + d + d;
};
Да, JIT не может такие вещи делать. А мы можем. В математике все детерминировано, и мы этим пользуемся. Я не вижу смысла тестировать вашу лямбду, потому что надеюсь, что мы друг друга поняли. Ваша лямбда скомпилится в ту же, что и у меня.
В этом и смысл. Я хочу увидеть честное быстродействие Linq Expressions на честном примере. Я понимаю, что вы хотите показать способность библиотеки вашей библиотеки оптимизировать задачу, но сейчас нет возможности отделить одно от другого (накладные расходы linq/выигрыш от оптимизации).
Linq.Expression скомпилится в тот же делегат, что тут тестить? Оно транслируется в тот же IL, который, в свою очередь, транслируется тем же JIT-ом. Так что оверхед нулевой.
Кстати, посмотрите еще внимательнее — в статье в таблице есть пример, в котором в выражении нет идентичных поддеревьев. И там одинаковая производительность.
> Оно транслируется в тот же IL, который, в свою очередь, транслируется тем же JIT-ом.
Я примерно то же ожидал от DynamicDelegate, но нет: похоже, там есть накладные расходы на проверки видимости, даже если она отключена: имеет значение, укажите ли вы класс, в который «добавляется» метод (по умолчанию это Object): разница в 2 раза.
Причём в контексте той задачи, что я решал (повторяющийся доступ к приватным свойствам объекта, способный пережить ~1200 вызовов в секунду) в тестах других разработчиков LinqExpressions были не самыми быстрыми.
у меня нет пакетов netstandard2.0, только .Net Framework
netstandard2.0 поддерживает почти все рантаймы, и даже многие древние версии фреймворка. Рекомендую обновить рантайм, если у вас все-таки настолько древний, что netstandard2.0 не поддерживает. Ну и поставить новую VS с новым Розлином
.Net Standart 2.0 в списке пакетов есть, но больше похоже на проблему с версией языка. static у лямбд не работает в частности, а так же проверка типов более строгая (требует explicit cast) для == между 'Entity.Number.Integer' и 'int'. Причём это довольно странно, implicit cast объявлены, должно работать (я так делал, но не на этом типе проекта)
Попробуйте новую VS, там должен быть новый Розлин, чтобы все эти фичи реализовать. А про каст — ну может там и должен быть явный, я не помню).
К слову об классах-обёртках для int: из личных издевательств над математическими библиотеками знаю, что лучше использовать `ref int`, `ref double` чем класс-обёртку. Удаление таковой из Cassovary.Net дало 30% прирост производительности.
Что значит ref int? Ссылка на инт? А зачем? Мы хотим произвольную точность для целых чисел, дробных, рациональных и комплексных. Как предлагаете сделать по-другому?
Классы чисел у вас выступают как константы? Если да, то даже ref не нужен
Всё зависит от ваших задач. Возможно, ничего не стоит менять, так как это наверняка уменьшит читабельность и увеличит количество кода.
В моём случае мне нужно было рассчитать весь layout «пока пользователь не заметил», буквально (это порядка 0.01 секунды)
Вот изменение, которое я делал в Cassovary.Net:
github.com/krypt-lynx/Cassowary.net/commit/3fd07e5078f4ff55e06418cb00578be8bbbae6e0
Собственно, Cassovary.Net — это порт с Java, видимо обёртка нужна для того, чтобы сделать значение мутабельным. На C# в этом не было никакого смысла.
Я так понимаю, что ref double быстрее из-за способа обращения к значению: в случае с обёрткой обращение идёт к значению по указателю в управляемой памяти со сдвигом, ref double просто значение по указателю? Если значение константное (ref не нужен, просто double), то оно будет лежать прямо в стеке
Компилируем математические выражения