Pull to refresh

Comments 20

А Вы не могли бы добавить в бенчмарк JSON-сериализации еще и библиотеку Jil, которая также строит сериализаторы с помощью генерации IL-кода в рантайме ?


По поводу быстрой компиляции Expression: есть такая библиотека FastExpressionCompiler. Она компилирует выражения в IL без создания динамической assembly.

Спасибо, ради таких дополнений и писал статью.
Померил Jil — Jil в два раза быстрее.
В статью добавлять пока не буду — потому что не понимаю трейдофф от компиляции через Reflection.Emit. Кроме того github.com/dotnet/corefx/issues/29365. Jil/Sigil на windows в Core запускается (монстры), но что дальше вопрос. И что-то там все равно не так — asterix ами накрыло ветку Dependencies Benchmark проекта в VS/SolExp после добавления Jil (хотя запускается). В общем надо подумать. А так — да — ставлю в очередь — сесть и написать интерпертатор DSL Includes компилирующий сериализатор через Reflection.Emit (точнее искать компилятор Expression Tree — Reflection Emit), чтобы всё понять.

Меня больше заинтриговал вопрос как они кэшируют скомпилированные функции? По типу? По месту вызова — рассматривая стек? Я на такое не решился.

А, вот FastExpressionCompiler — и есть такой компилятор. Надо пробовать.
Сериалайзер построенный с FastCompile — несущественно добавил к перформансу (0.2 ms), Jil не обогнал (еще 0.8 ms не хватило) а в целом jil у ComposeFormatter выигрывает 1ms. Все лямбды скомпилированы при помощи FastCompile. Рабочая гипотеза — Jil строит компилятор с минимум работы со стеком, тут и выигрыш.

компилятор выведен как параметр
```
var composeFormatterFastCompileDelegate = JsonManager.ComposeFormatter(includeWithLeafs, compile: (ex)=>ex.CompileFast());
```

вообще, надо брать windbg и смотреть код.
не буду обещать что сделаю, но было бы интересно.

«Jil строит компилятор» читать как «Jil строит сериализатор»
На сколько я знаю, кешируют по типу при первой (де)сериализации объекта
В статью добавлять пока не буду — потому что не понимаю трейдофф от компиляции через Reflection.Emit.

Быстрее. Как пример, клонятор использует Reflection.Emit, когда возможно и заваливается на ExpressionTree, если не получается. Код по идее идентичен, но на экспрешшенах тупо медленнее. Или же где-то есть ошибка, или же особенность задачи приводит к разным операциям, но в целом голый il код получается производительнее.

PS: А вообще, Jil делает очень упоротые оптимизации в плане сериализации данных в строку, так что он ещё там может выигрывать.
Да, трейдофф «быстрее» за ограниченность платформы (не Standard).
В прочем моей удачей было бы не перегнать клонятор или Jil по скорости, а им о Include DSL рассказать. И убедить поддерживать.
Там степень оптимизаций настолько упорота, что даже порядок свойств определённый.
Это то ладно, но Jil меня больше удивил тем, что нет и не надо четкого разделения между потоковым сериализатором и компилирующим функцию. И надо было и мне (покрайней мере для статьи) смело идти путем кэширования по ключу: тип + [CallerMemberName] + [CallerName]. Статья вообще построена на противопоставлении потоковых клонеров, сериалайзерах — компилирующим. А оказалось компилирующие просто имитируют потоковость — и противопоставление преодолено. Опять же DSL Include — удобная запись метаданных и может быть использована везде где нужно задать дерево обхода (вычислений) с проименованием узлов, но статья должна строится немного по другому.
Я сверил сериализацию jil c просто кодом записанным `.Append(«I1:»).Append(e.I1).Append(",")` и т.п. 12 атрибутов, массив 600 элеменов. Jil победил но с разницей в 30 **нано**секуд. Т.е. если сгенерировать по мете сериализатор в .cs и избавится от имитации потоковости а вызывать сериализатор в контроллере — эффективность jil будет фактически повторена. Это не считая тех случаев когда «тонкой сериализацией» получается убрать DTO — в таком случае и сейчас ComposeFormatter — эффективней. А идеал лежит в другой плоскости заменить пару Select + ToList одним ToJson(cache, IQueryable, IModel). Правда Include DSL тут уже не будет (`IQueryable` не`Include`).
LambdaExpression.Compile компилирует только верхнюю Lambda.

Это вообще как? А куда деваются остальные?

не компилируются в IL (не создаются в динамическом assembly) т.е. исполняются в каком-то режиме интерпретации.

хорошая тема для нового исследования — запустить из windbg и посмотреть что происходит точно.
что режим интерпретации не возможен?

вот косвенное доказательтво: у LambdaExpression.Compile есть параметр preferInterpretation

Не верю что он автоматически используется для вложенных замыканий.
А верите ли, что некомпилируемые вложенные LambdaExpression не создаются в динамическом ассембли по крайней мере отдельными динамическими функциями? А если каждую вложенную компилировать (для последующего заворачивания в константу) — то создаются и все начинает усорятся? Это как раз просто доказать (без поисков «неизвестного» в WinDbg — это весть день портатить надо).
Возможно надо попасть в какую-то тютельку, подсовывая в параметры вызова то что надо, и компилировать внутренюю лямбду будут. Например если параметры «не дженерик» делегаты. Инетересно что FastCompile получив полное выражение без оптимизации «скомпилированная внутреннея лямбда» — тоже не смог большего чем простой Compile — хотя казалось бы- разобрал — скомпилировал — собрал — но есть какие-то сложности.

А разница по факту — ровно на порядок. Компилируешь внутренние лямбды — 2 ms, не компилируешь — 20 ms (и никакой FastExpressionCompiler, как я сказал — не исправляет — в случае моих выражений, по крайней мере)
Sign up to leave a comment.

Articles