Обновление 15–08–2018: здесь результаты https://www.techempower.com/benchmarks/#section=test&runid=9d3b8d36-c4a8-499f-bd0a-0fe33cf820cf, Zebra преуспела лучше, чем ожидаемый, почти в 2 раза быстрее, чем Giraffe, больше возможностей для улучшения, но более чем что-либо, подтверждает аргументы архитектуры, обсуждаемые здесь. В приложениях со сложной маршрутизацией / синтаксическим анализом, цепочкой асинхронной привязки ускорение будет увеличиваться в геометрической прогрессии.

Введение

Я, наряду со многими другими, проделал изрядный объем работы, проектируя и тестируя архитектуру Giraffe, чтобы гарантировать, что она будет работать так же хорошо, как MVC, как это было исторически с Suave и другими функциональными фреймворками, благодаря всем функциональным конструкциям, которые у нас были. Чтобы поставить под угрозу производительность, мы заплатили за более элегантный функциональный API. Для тех, кто интересуется, как мы спроектировали эти элементы производительности, вы можете ознакомиться с вопросами Github Выражение вычисления задачи вместо асинхронного или Продолжение с привязкой Httphandler.

Проблемы со следующим продолжением

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

fun next ctx -> task { ... return! next ctx }

Проблемы с TaskBuilder.fs

Первоначальная медлительность Suave на ядре asp.net была связана с постоянным переключением между async и Task, эти конструкции являются частью асинхронных конвейеров с планированием и т. Д., Поэтому преобразование назад и вперед было не так просто, как преобразование целого числа в число с плавающей запятой. , это было чрезвычайно расточительно, поэтому с Giraffe я предложил вместо этого использовать задачи, и это было принято с помощью TaskBuilder.fs, что привело к значительному повышению производительности, хотя мы все еще очень сильно отставали от привязки C # async / await. Это связано с ограничениями на передачу состояния в вычислительных выражениях по сравнению с async / await, у которых есть специализированный вариант компилятора, который оптимизирован; использование одного AsyncMethodBuilder для каждого конвейера, чтобы все управление цепочкой асинхронных методов использовало один итератор состояния. TaskBuilder в целом - отличная библиотека, которая делает все возможное с ограничениями F # и CE, но при каждой привязке выделяет продолжение закрытия, а также Discriminated Union, который очень много болтает поверх всего планирования задач, и каждый CE создает собственный конечный автомат против возможности C # использовать по одному на конвейер из нескольких методов async / await.

Проблемы с маршрутизацией

Реализация маршрутизации по умолчанию в Giraffe очень неэффективна, поскольку он пытается каждый путь, один за другим, найти соответствие маршрута, что дает очень низкую несогласованную производительность для больших списков маршрутов. Кроме того, текущая маршрутизация использует RegEx, который плохо работает в .net, поскольку это неконтекстно сгенерированный IL, который не полностью оптимизирован в окружающий его код. Использование какой-то структуры Dictionary / Map позволяет повысить производительность поиска, и при создании TokenRouter я решил обе эти проблемы, токены в стиле Radix позволяют быстро спрыгивать вниз по дереву узлов вместе с ручными синтаксическими анализаторами, которые работают намного быстрее, чем Regex. Простым ответом на производительность маршрутизатора может быть использование TokenRouter, но для тестов это не разрешено как помеченная бета-версия, и в дополнение к этому есть две проблемы с TokenRouter, его API считается немного сложнее, чем подход по умолчанию. , и, как и реализация по умолчанию, в настоящее время он упаковывает каждый результат (часто ValueTypes) перед упаковкой в ​​кортеж, много ненужных выделений.

Стеки обратных вызовов, принуждение к типу и болтовня

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

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

Еще один побочный эффект множества небольших функций и объектно-ориентированного мышления заключается в том, что в конечном итоге нам нужно взломать / изменить систему типов, чтобы возвращаемые типы совпадали и придерживались своего рода моноидной компоновки, чрезмерного обобщения и избегания явности. Функциональное программирование хорошо подходит для CPS, так что обобщенные возвращаемые типы становятся менее важными, вместо этого мы можем вкладывать ветви выполнения с локализованными явными типизированными продолжениями, избегая попыток заполнить или обернуть квадратный блок круглым отверстием. Классическим примером этого является задача .Net. Мы создаем конвейеры с типом возврата Task ‹’ T ›, даже если у нас есть функции в конвейере, которые являются синхронными и, следовательно, должны обернуть задачу или использовать другой хак, например ValueTask‹ ’T›. Giraffe сокращает этот взлом, используя next продолжение, но есть еще кое-что, что можно сделать.

Потолок для жирафа

Одна из причин, по которой Giraffe в настоящее время превосходит MVC в некоторых тестах, заключается в том, что ветвление обработчика создается при запуске, дерево вложенных продолжений, которое необходимо применять только во время выполнения. Это позволило значительно улучшить однозначное значение по сравнению с предыдущей привязкой опций. Формат продолжения также предоставил элегантный способ предотвращения пауз на горячих путях из асинхронных оберток (которые были предназначены только для поддержки асинхронного конвейера) ... но после всех этих оптимизаций все еще остается немного жира / раздувания от aspnetcore MW (промежуточного программного обеспечения), которое также является в тесте и на чем построен Giraffe. Просматривая код, выявляет очевидную проблему с болтливым TaskBuilder.fs с большим объемом памяти, и улучшение его производительности оказалось трудным, поскольку оригинал хорошо построен и делает все возможное для общего назначения Task CE, чтобы превзойти ограничения и тесты, которые нам нужны, чтобы изменить дизайн некоторых вещей и специализироваться.

Представляем Zebra

Чтобы обойти ограничения и опробовать новые шаблоны дизайна, я создал Zebra, меньшего размера, более быстрый жираф, похожий на животное, название которого вдохновлено Steffen & Krzy.

Ничего не возвращайте, вы сами

Я хотел уйти от принудительного переноса Task. Задача и лежащая в ее основе инфраструктура продолжения с конечным автоматом не возвращают задачи на каждом этапе, так почему мы… все ядро ​​asp.net заботится о том, чтобы оно получало уведомление о завершении всего запроса, чтобы оно могло удалить и вернуть объединенные ресурсы. Исходя из этого, мы вместо этого смотрим, чтобы образно отправиться в дикую природу с ракетницей, без движения вперед и назад, только последний сигнал о завершении, мы можем сделать это с помощью TaskCompletionSource, который предоставляет ядру asp.net сигнал о завершении задачи, который ему нужен. .

Конструкция исполнения с фронтальной загрузкой

Согласованный шаблон для любого высокопроизводительного приложения использует оптимизированное состояние структуры данных, которое позволяет меньше выделять время выполнения, а также сокращает путь выполнения во время выполнения. Алгоритмы и структуры данных очень похожи, структура данных является подмножеством алгоритма, пропорциональным его оптимизации, структуры данных оптимизируют выполнение алгоритма, улучшая локальность соответствующего состояния, минимизируя распределение и ветвление. Вы не можете «запустить» структуру данных, алгоритм (выполнение) необходим всегда, но все больше и больше логики алгоритма может быть поглощено структурами данных для повышения производительности.

Ключом к быстроте Giraffe является предварительное построение древовидной структуры выполнения, в то время как Suave привязывает конвейер во время выполнения. То, как Giraffe может построить эту структуру, можно увидеть в функции compose, но это похоже на построение одностороннего BTree, поскольку каждое замыкание имеет next захваченный, ошибка сообщается с помощью опции возврата. Чтобы расширить это, и, учитывая, что у нас нет возвращаемого типа, я решил явно реализовать структуру BTree под крышками, каждый узел имеет (1) обработчик, (2) следующий узел и (3) узел сбоя, fail будет сокращать переключить нас прямо на желаемый резервный узел, не привязываясь к типам возвращаемых данных и не отбрасывая нагрузку фреймов стека возврата.

Зебра Handler

Переработанный обработчик ничего не вернет (единица измерения), а полное управление путем выполнения будет обрабатываться входящим (ввод функции) состоянием. Перед выполнением обработчика внедряется узел BTree Execution, чтобы предоставить ссылку на следующие обработчики и обработчики ошибок, подпись обработчика теперь теряет следующее продолжение, ничего не возвращает, поэтому просто State<’T> -> unit. Функции ничего не ждут, поскольку они внутренне управляют состоянием всего конвейера.

Представляем Zebra’s State

В Giraffe мы передаем HttpContext, и это состояние ... Я понял, что если я хочу использовать один общий конечный автомат Task для конвейера, мне нужно будет передать его как состояние. Затем у меня возникла проблема, заключающаяся в том, что я хотел иметь CE, который мог бы каким-то образом внутренне использовать этот конечный автомат задачи. Поэтому я изначально подумал о гибридной задаче CE, которая принимает конечный автомат в качестве аргумента конструктора в каждом выражении. Этот API был немного утомительным и уродливым, выделял каждое выражение CE, и после всех жалоб / проблем, связанных с Giraffe, я знал, что мне нужно было сделать его как можно более простым ... Затем мне пришло в голову, что я могу сломать общую форму CE, я мне не нужен был универсальный универсальный Task CE, я хотел новую специализированную Zebra CE.

Zebra State CE

CE с отслеживанием состояния может быть создан с новым асинхронным конечным автоматом в начале конвейера, совместно используемым, поскольку он связывает каждую функцию, и ему не нужно возиться с возвратом возврата / возврата задачи, мы на самом деле не возвращаем данные / work, мы изменяем HttpContext, я решил, что возврат CE может быть просто логическим значением, аналогично тому, как работала привязка параметра контекста, но избегает обертывания синхронизации горячего пути, поскольку мы можем справиться с этим под покровом ... и благодаря Zero в построителе, вы можете иметь выражение без возврата, и он будет неявно применять истинный случай для перехода к следующему обработчику.

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

Специализированный CE также позволяет выполнять customOperations, для начала я создал `text` и` json`. что еще приятнее, так это то, что каждый запрос имеет собственный пул MemoryStream, поэтому вся запись с помощью customOperations синхронизируется с буфером, что позволяет выполнять несколько накопительных операций записи, при этом конец конвейера автоматически устанавливает заголовки длины содержимого и т. д. и выполняет асинхронное копирование. Такой подход обеспечивает идеальную производительность для рендеринга json и представления без фрагментов.

Привязка задач Zebra

Обработка задач Zebra не только значительно улучшена за счет использования единого асинхронного конечного автомата для всего конвейера, но и практически не выделяется, поскольку использует преимущества хранения структур аппликатора в слоте многоразового конечного автомата, структур, которые не помещаются в коробку. для вызова их методов асинхронного интерфейса из-за передачи ограничения struct ref. Единственным распределением в этих конвейерах должно быть закрывающее обертывание продолжения связывания (если есть захваченная переменная, что неизбежно).

Если мы посмотрим на TaskBuilder.fs:

Если нам нужно выполнить ожидание, мы выделяем как для Await (awt, _) ` DU, так и для ` (fun () - ›continue (awt.GetResult ( )) `

В Zebra мы используем mutableStruct refs для предотвращения боксов и минимизации выделения кучи:

Вариант привязки к продолжению и обратно

Ирония полного круга не теряется для меня в связи с тем, что я настаиваю на продолжении привязки Option… только для того, чтобы теперь обеспечить привязку bool. Хотя это похоже на привязку типа bool, оно скрыто с помощью bool для применения состояния / контекста к функции Next / Fail в предварительно построенном дереве, как это было раньше ... это гибрид обоих, что, как мне кажется, не влияет на производительность существенно но возвращает api в предпочтительный, возможно, даже лучший формат неявных / явных возвращений bool CE. HttpContext, который обернут в Option для Suave или Giraffe, является потраченным впустую выделением, поскольку на изменяемый HttpContext можно ссылаться в функции привязки из другого места, упаковка и передача имеет смысл, когда / если он был неизменным и т. Д.

Для более простого API дополнительными операциями являются: 1) Ввод нового узла next / fail в состояние перед запуском обработчика. 2) Попросите Return оценить if / else, чтобы определить, как двигаться вперед, next или fail.

Маршрутизация парсеров с ограничениями статического типа

Чтобы решить проблему с маршрутизацией, я по умолчанию интегрировал в систему модифицированный маршрутизатор Token, но улучшил удобство использования API и, чтобы избежать всех упаковок, я смог создать систему ограниченного синтаксического анализа с полным типом, которая избегает боксов и связывает различные парсеры. непосредственно в аппликативные функции, снова минимизируя выделение. Это означает, что больше не будет объектов Tuple, и теперь, когда построение продолжения управляется через узлы, мы теперь можем гарантировать, что маршруты синтаксического анализа построены при запуске, а также иметь более удобный формат композиции, которого ожидают люди.

Одним из побочных эффектов явного формата синтаксического анализа карри является то, что маршруты синтаксического анализа должны иметь количество параметров, добавленных в конце вместо «f», поэтому в соответствии с маршрутизацией метода стиля Сатурна вы получите «get1» или «post2» и т. Д.

Мы используем встроенный тернарный оператор, чтобы можно было связать различные формы обработчиков вместе, чтобы разработчик мог легко взаимодействовать с ними, им нужно только использовать =>, чтобы склеить элементы вместе.

Так как же будет выглядеть приложение Zebra?

Ниже приведен лишь базовый набор функций.

Что означает зебра для жирафа / Сатурна?

Ничего, это доказательство концептуального дизайна, созданного для лучшей работы в тестах, но при этом достаточно удобного для пользователя, чтобы его можно было рассматривать как фреймворк. Я не буду строить или расширять его, поскольку Giraffe и Saturn уже предлагают богатый полноценный опыт разработки, и я не хочу отвлекать эти проекты от моего причудливого домашнего проекта. Дизайн был результатом / продуктом работы, которую я проводил по интеграции TokenRouter в Saturn, в сочетании с участием Giraffe в тесте TechEmpower Benchmark.

Значит, это чисто для теста, и его нельзя использовать?

Это нестабильная альфа-версия, требующая некоторых хаков api, но она может быть интересным источником для некоторых странных паттернов, которые вы редко встретите где-либо еще, с:

  • передача выражений вычислений с сохранением состояния
  • привязка тернарного оператора в нескольких случаях
  • Листовое обратное построение функций BTrees
  • Перегрузка типа оператора SRTP
  • Приложения структуры AsyncMethodbuilder

Просто назвать несколько.

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

Я не собираюсь выпускать это как пакет nuget или что-то в этом роде ... Я просто надеюсь немного повысить рейтинг F # в Benchmark, сохранив при этом статус Framework (сопоставимый с MVC). @Gerardtoconnor в твиттере, если есть вопросы.

Https://github.com/gerardtoconnor/FrameworkBenchmarks/tree/master/frameworks/FSharp/Zebra