Алгоритмы И Решения Для Разработки Движка Javascript На C#

Здравствуйте, дорогие хабровчане! Чуть меньше года назад я тоже в песочнице опубликовал статью о начале разработки Движок JavaScript на C# .

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

Но в этой статье я не буду пиарить себя, проводить сравнения с конкурентами, измерять производительность и т. д. Здесь я напишу о том, через что мне пришлось пройти, какой кровью все это стоило и с чем мне пришлось столкнуться.

Конфликт ввода Эта проблема возникла одной из первых.

И это следует из того, что в самом начале целью было обеспечить простое взаимодействие с .

NET-классами.

В результате все встроенные типы стандартной библиотеки JavaScript реализованы одноименными типами в C#.

Всем известно, что в JavaScript можно вызвать любую функцию в контексте любого объекта.

Некоторые из этих случаев даже описаны в спецификации.

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

прототип ? Все верно, он будет работать как с обычным массивом и возвращать правильный результат. Как это реализовать на С#?! Оказалось, есть такой способ:

  1. Получите это через отражение Информация о методе метод, который нужно вызвать.

  2. Получать MethodHandle.GetFunctionPointer()
  3. Через Активатор создайте делегат с аргументами тех же типов, что и в исходной функции, но добавьте аргумент первого типа объект .

Теперь мы можем вызвать этот делегат, передав объект любого ссылочного типа в качестве первого аргумента.

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

Но к тому, что будет после этого, создатели платформы явно не подготовились — Visual Studio не может определиться с типом и при отладке даже не пытается вызвать Нанизывать и оператор является проблемы истинный при проверке типа, объявившего метод, даже если они связаны только с проверяемым объектом путем наследования от объект .

Сохранения GetType , который не обманывает и возвращает реальный тип.

Понимая опасность такой операции, я добавил атрибут Алловунсафекалл , конструктор которого требует явно указать альтернативный тип, готовый к приему внутри помеченного им метода (придут и наследники указанного типа).

Разреженный массив Массивы JavaScript обладают замечательным свойством хранить элементы с индексами 0, 1, 2 и 10005000, не занимая при этом несколько гигабайт памяти, и даже при переборе по ним возвращают существующие индексы в порядке возрастания, что не характерно для хеш-таблиц.

вы можете подумать.

В поисках подходящей структуры данных, обладающей такими свойствами и при этом быстрой, было просмотрено множество книг и веб-сайтов.

В какой-то момент вся эта каша информации выродилась в «префиксное двоичное дерево поиска, оптимизированное под кэш процессора».

Звучит громко и долго, но на самом деле реализация этой структуры занимает чуть больше 100 строк кода.

Для хранения объектов создаются три поля: один массив для «узла дерева», второй массив для самих значений и целое число выбранных элементов.

Каждый узел имеет 4 поля:

  1. Ключ, по которому осуществляется доступ.

    Беззнаковое целое число 32 бита

  2. Индекс первого дочернего элемента (или нулевой, что более уместно в данном случае)
  3. Индекс второго ребенка
  4. Битовый индекс.

    Создан для оптимизации

По умолчанию мы считаем, что объект с индексом 0 уже добавлен в массив.

Добавление:

  1. Позволять я = 31;
  2. Если ключ равен ключу текущего элемента, записываем добавленное значение в массив значений по индексу текущего элемента и выходим.

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

  4. Брать i-й ключевой бит. Если он равен 0, сделайте первый дочерний элемент текущим, в противном случае второй дочерний элемент уменьшите.

    я на 1. Если элемент с индексом 0 считается потомком, добавьте новый элемент, назначьте его потомком предыдущего, потомками нового элемента является элемент с индексом 0, сделайте ключ равным добавленному ключу , битовый индекс равен я и запишите значение.

    В противном случае перейдите к шагу 2.

Квитанция: Здесь без формализации, так как это то же самое, что сложение, с той лишь разницей, что шага 3 нет, на втором шаге мы возвращаем значение, а не записываем его, и если у элемента нет нужного потомка, мы обрабатываем отсутствие запрошенного значения и не добавляем новое.

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

При этом есть одна особенность: если перед формальным выполнением алгоритма добавления или получения мы «наступим» на случайный элемент, то есть вероятность, что он приведет нас к искомому элементу за меньшее количество шагов (здесь значение индекса бита, который был обработан при добавлении этого элемента, хранится в поле 4).

Еще одной приятной особенностью было то, что при последовательном добавлении элементов они сохраняются в индексах, точно соответствующих их ключам, что сокращает время поиска до O(1).

Вот такая конструкция получилась.

Когда я это реализовал, время завершения SunSpider 0.9.1 уменьшилось на 10%.

На мой взгляд, это неплохо.

Веревочная струна Уже в хабе написал об этом , поэтому напишу здесь только идею.

Когда в .

NET происходит конкатенация строк, создается новая строка, значение которой сначала копируется из первой строки, затем из второй.

На грани O(n+m).

Rope String — это объект, который хранит ссылки на исходные объекты, а не копирует их значения.

По сути, это обещание, что эти две строки, при необходимости, будут одной строкой.

Получается О(1).

Конечно, если мы вызовем ToString для каждого такого объекта, что в данном случае будет принимать O(n+m), мы не получим никакого выигрыша, но когда они создают Rope from Rope и Rope from Rope. они могут подключаться в линейное время с помощью StringBuilder. Когда я добавил это решение, время на том же SunSpider уменьшилось в 5 раз.

Тип прогнозирования Эта оптимизация специфична для динамической природы JavaScript. Другой пример:

  
   

var a = 1; var b = 2; var c = a + b;

Какая из множества реализаций «+» должна вызываться в третьей строке? Ну и о сложении цифр здесь, конечно, думать не стоит. Но то, что очевидно для человека, не всегда понятно машинам.

И поэтому, когда выполнение доходит до третьей строки, виртуальная машина должна подумать и «пройтись» по огромной таблице сопоставлений.

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

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

Сейчас эта оптимизация еще в процессе интеграции, но уже при замене операторов «+», «<”, “> ”, “<=", “> =" эффект хорошо заметен.

Отдельно еще раз упомяну о конкатенации.

Сейчас этот эффект перекрывается Rope String, но когда-то, заменяя

"abc" + 1234 + 567

Для оператора слияния нескольких строк (с одним проходом по всем элементам) я получил двукратное ускорение.

В заключение немного о проекте.

Двигатель остается интерпретативным.

Были попытки добавить JIT с помощью System.Linq.Expressions, но это решение почему-то приводило только к замедлению, поэтому от этого направления отказались.

Теги: #nil #JavaScript #C++ #.

NET #JavaScript #.

NET #Алгоритмы

Вместе с данным постом часто просматривают: