Pull to refresh
1
0
Send message

новость хорошая, но на момент прочтения статьи последняя доступная для скачивания версия .net на официальной странице 8.0.0-rc2

Результат выполнения будет абсолютно одинаковым. Но я не могу взглянув на этот код с уверенностью в 100% сказать, что после замены goto на return даже в этих местах компилятор в итоге выдаст абсолютно одинаковый asm код. Чтобы ответить почему именно разработчики оставили в этих местах goto, нужно скомпилировать и смотреть в дизассемблер и делать бенчмарки. Я не думаю, что код, который парсит все Json в Asp.Net Core сервере писали глупые люди. У них 100% есть куча бенчмарков для этого кода, который запускался тысячи раз на разных архитектурах. Возможно есть причина в этих goto, и выигрыш даже 0.5% на ровном месте стоит чуть больше "красивого" кода. В противном случае, почему бы не открыть PR в кодовую базу dotnet и не предложить им улучшить читаемость кода и понимание control flow?

Так я и не критиковал автора комментария. Но если писать 17 лет на C# и ни разу не сталкиваться с goto, то может просто круг решаемых задач не требовал знания такой вещи, как goto. Допустим я в своей практике занимался "творческими" задачами и приходилось пользоваться этим оператором. Возможно когда-нибудь моё творчество тоже кто-то раскритикует, назовёт код лапшой и перепишет все без goto, заодно уронит производительность какого-нибудь "творческого" алгоритма раз в 10. По поводу goto в dotnet можно ещё вот так посмотреть:
https://github.com/search?l=C%23&q=org%3Adotnet+goto&type=Code

но это уже совсем для смелых.

Я понимаю, что для перекидывания Json'ами между микросервисами оператор goto не нужен, собственно потому некоторые разработчики о нём и не слышали. Но для того, чтобы такие разработчики могли перекидывать свои Json'ы максимально быстро, другим разработчикам приходится терпеть боль от использования такого "плохого" инструмента и скрепя зубами писать код используя его, т.к. по-другому скорости не получить. Возможно для Вас они не авторитет, они всего-лишь разрабатывают dotnet:
https://github.com/dotnet/runtime/blob/main/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs

Слабонервным лучше не открывать, 60 операторов goto в 1-м файле.

FRP - fast reverse proxy, туннелирование TCP/UDP трафика, не то же самое делает?
https://github.com/fatedier/frp

В моём случае сверху заселилась мать с 2-мя детьми, которая регулярно ездила в командировки и дети, оставаясь одни, могли прыгать с кровати на пол, бегать и заниматься борьбой на полу часто до 3-х ночи. Да и без командировок младший передвигался по квартире исключительно на пятках. До 23:00-01:00 почти каждый день не заснуть, в 6:30 утра у них сборы в школу, я поднимался и уходил спать в другую комнату на пол и накрывал голову подушкой. Разговоры не помогли. Рассматривал вариант виброколонок, но портить аккуратный гипсокартонный потолок я не решился. Была акустическая система и усилитель 150 Ватт на канал, но другие соседи не заслуживали таких мер. Спустя год я принял единственно верное решение продать квартиру.

Вы когда в следующий раз будете проверять производительность парсера, делайте запуск 10-100 раз в цикле, замеряйте время выполнения и считайте среднее/медианное время (и на С++ и на PHP) Перед основным запуском лучше тоже запустить пару раз. И в тесте на производительность именно парсера лучше исключить чтение из файла на каждой итерации, по-хорошему надо загрузить сначала в оперативную память, и уже из памяти запускать парсер, чтобы исключить время чтения с диска на работу алгоритма. А так, то что с Json разница между 1-м и 2-м запуском небольшая — это потому что там размер грамматики крохотный и DFA строится очень быстро, вот на грамматике PHP это уже должно было отразиться.
Тут даже семантических предикатов мало, нужен уже полноценный парсер, чтобы #if #elif #else #endif реализовать. Я боялся, что это станет проблемой, но пока обошлось только обычным лексером с предикатами, правда для ограниченного сверху кол-ва #elif условий, получилось такое:

Лексер условных C# директив
// Conditional directive

@fragment
directive_eval_condition
: { return EvalCondition(context); }? directive_skip_condition
;

@fragment
directive_inverse_eval_condition
: { return EvalCondition(context) == false; }?
;

@fragment
directive_skip_condition
: ~[\n]*
;

@fragment
directive_to_endif
: ~[#]* '#endif'
;

@fragment
directive_to_elif
: ~[#]* '#elif'
;

@fragment
directive_to_else
: ~[#]* '#else'
;

@fragment
directive_to_else_or_endif
: ~[#]* ('#else' | '#endif')
;

@fragment
directive_skip_to_endif
: directive_to_elif* directive_to_else? directive_to_endif
;

@fragment
directive_skip_to_elif
: directive_to_elif
;

@skip
directive_if
: '#if' (directive_eval_condition | directive_inverse_eval_condition (directive_try_elif | directive_try_else))
;

@fragment
directive_try_elif
: directive_skip_to_elif (directive_eval_condition | directive_inverse_eval_condition (directive_try_elif_1 | directive_try_else))
;

@fragment
directive_try_elif_1
: directive_skip_to_elif (directive_eval_condition | directive_inverse_eval_condition (directive_try_elif_2 | directive_try_else))
;

@fragment
directive_try_elif_2
: directive_skip_to_elif (directive_eval_condition | directive_inverse_eval_condition (directive_try_elif_3 | directive_try_else))
;

@fragment
directive_try_elif_3
: directive_skip_to_elif (directive_eval_condition | directive_inverse_eval_condition directive_try_else)
;

@fragment
directive_try_else
: directive_to_else_or_endif
;

@skip
directive_elif
: '#elif' directive_skip_to_endif
;

@skip
directive_else
: '#else' directive_skip_to_endif
;

@skip
directive_endif
: '#endif'
;

Не знал про такой lookahead в регулярных выражения, спасибо, интересно.
Да, я знаю, что в ANTLR можно рекурсивные правила в лексере писать, я тоже планировал добавить такую возможность в свой велосипед. Только такие правила уже не смогут работать через конечный автомат, получается на этих правилах лексер будет работать также, как и парсер со стеком, бэктрекингом, lookahead и т.д. Я это так понимаю, не вижу другой альтернативы (не смотрел как в ANTLR это реализовано), и, как следствие, снижение производительности в разы для таких правил. Но это крутая функция, т.к. позволяет на уровне лексера реализовать #if #else директивы из C#, или интерполированные строки представить в виде лексем.
И какие получились числа времени парсинга PHP и большого Json между GdlParser и ANTLR на 2-м запуске?
Мы может друг друга не так понимаем (или я вас не так понимаю), но lookahead — это когда для принятия решения читается следующий символ из входящего потока и производится его анализ. Лексер и регулярные выражения на основе DFA не заглядывают в поток вперёд, они на каждом символе из потока переходят из 1-го детерминированного состояния в другое, очень примерно это выглядит так:
while ((ch = stream.ReadNext()) != -1)
{
	dfaState = dfaState.GetNext(ch);
	
	if (dfaState.Break)
		break;
}

Вообще в регулярных выражениях используется расширение оператора Клини, ленивые квантификаторы, и тогда правило для комментария выглядит так '/*' .*? '*/', и в реализации конечного автомата при переходе из одного состояния в другое при прохождении символа через ленивый оператор такое состояние помечается атрибутом, который просто «запрещает» повторное вхождение в эту альтернативу. Но никакого lookahead там нет.
А вы когда с ANTLR сравниваете производительность, вы проверяете 1-й одиночный запуск, или хотя бы 2-й? Во время первого запуска там ATN дессериализуется и DFA строится, а вот 2-й и следующие должны быть быстрее.
Если ваша цель показать, что все может быть гораздо проще и возможно реализовать интерпретатор произвольной грамматики в 400 строк, то ОК, вы справились с задачей. Я вас не критикую, а дал рекомендацию почитать теорию, в случае, если вы планируете продолжать свою работу, т.к. сам прошёл похожий путь. Вы говорите отдельный лексер не нужен, какой там у вас получился результат в парсинге Json в 100мб и какой результат у ANTLR? Если разница в 2-3...5 раз, то ладно, а если она в 100-1000 раз отличается в пользу ANTLR, то практического применения кроме как несложных грамматик и небольших документов немного. Когда я производил замеры производительности, то в в синтетическом тесте па парсингу большого Json документа мой интерпретатор работал раз в 20 медленней, чем парсер Newtonsoft Json (который написан вручную) и для меня это был неприемлемый результат, на сегодня этот тест (очень приблизительно, т.к. точных цифр не помню) чистый .Net Core и его парсер Json: ~80мс, мой интерпретатор ~250мс, Newtonsoft ~450мс (тут не очень честно, т.к. внутри получаемый документ сложнее), ANTLR ~1000-1200мс (очень неточно, т.к. проверял давно, но порядок примерно такой). И Json, с точки зрения его грамматики, просто примитивнейший пример. Как работает лексер я не то чтобы представляю, я его реализовал через DFA и там нет никаких lookahead. Продукция — это термин из теории формальных языков, в ANTLR его назвали альтернатовой, вы назвали statement, я вообще назвал transition, но моё мнение, что правильнее это называть продукцией.
Абстрактное дерево — это действительно назначение, но ниже вам написали, что оно не бинарное, в общем случае оно n-арное, правило — это узел, из которого выходит n (= кол-ву элементов продукции) ветвей. Я не буду навязывать вам свою точку зрения, как-то вы критически воспринимаете мой комментарий. Делайте как считаете нужным, но как сказал Terence Parr:
Despite the power of Parser Expression Grammars (PEGs) and GLR, parsing is not a solved problem ©, всё не так просто, как может показаться.
Если вам(или кому-то) интересно и вы не читали «LL(*): The Foundation of the ANTLR Parser Generator»
Это на C#. ~30к строк кода — это все строки исходного кода, включая объявления классов/свойств и т.д., непосредственно исполнимых строк всего 7к (так считает метрики Visual Studio) ~5к это сам генератор, который по файлу грамматики создаёт C# код с описанием грамматики и классы синтаксического дерева, а оставшиеся 25к — это исполняющая среда. В результате оно и получается аналогом ANTLR (по функциональности), но целевой язык только C#.
Вот и я не читал книгу дракона в 1-й год написания своего велосипеда, и в моём интерпретаторе тоже не было отдельного лексического анализатора. Первый же тест Json документа на пару мб показал, что нужно почитать теорию. Почитайте немного про лексический/синтаксический анализ (а ещё немного про теорию формальных языков). То, что вы называете statement в правиле, называется продукцией. А результат синтаксического анализа текста представляется абстрактным синтаксическим деревом, а не бинарным. Если это представлять классом, то правило — это абстрактный класс, все продукции этого правила — это наследники этого класса. Каждый элемент продукции будет свойством этого класса. Квантификаторы — это операторы Клини, и ваша первая проблема с разбором комментариев по правилу '/*' .* '*/' элементарно решается лексическим анализатором на основе DFA. Если вы посмотрите профилировщиком, то проблема производительности вашего интерпретатора скорее всего окажется в лексическом анализе, хотя на самом деле при отдельном лексере (если лексическая часть грамматики контекстно независима), то лексер на основе DFA выполняет свою работу за O(n) и может занимать лишь пару процентов от всего времени работы парсера. И как уже написали ниже, наивный парсинг легко становится экспоненциальным, как пример (сильно упрощённый), грамматика выражений C# или Java:

expression: assignment_expression | non_assignment_expression;
non_assignment_expression : unary_expression | binary_expression ;
assignment_expression : unary_expression '=' unary_expression ;
unary_expression : number | member_invocation ;

если мы хотим проанализировать выражения: a(b(c(d))) и a(b(c(d))) = 1, то пока мы не дойдём до самого конца этого выражения мы не будем знать, оно assignment_expression или non_assignment_expression, и на каждом вхождении в правило expression кол-во альтернатив будет удваиваться, и я нарочно поставил assignment_expression первой альтернативой, в реальной грамматике реального ЯП такие выражения будут редкостью, но наивный парсер такой порядок заставит выполнить полный перебор всех возможных комбинаций.
Когда 2 года назад я начинал делать интерпретатор грамматики (а позднее уже и полноценный генератор парсера), мне это не показалось очень сложной задачей. Спустя ~30к строк кода моё мнение немного изменилось. Если планируете продолжать, почитайте книгу дракона (пару первых глав про лексический анализ). Говорить про скорость синтаксического анализатора(парсера) без отдельного лексического анализатора(лексера) и сравнивать с ANTLR ещё, возможно, рано. Для примера, можно попробовать несложную грамматику Json или Xml и проанализировать файл размером в 100мб.
Вы придрались к одной фразе, которую я уже пояснил и согласился, что она слишком категорична. Как бы больше нет причины продолжать спор по этому поводу.
Я не знаю про типичных программистов, может они и фреймворки, типа asp.net core, не разрабатывают. В любом случае, нужно просто правильно выбирать инструмент для решения задачи, а не топить за Linq добро/зло и разоблачающие статьи на эту тему писать.
Согласен, разница в утверждениях есть, я потому мысль свою уточнил. Упоминания System.Linq в roslyn и asp.net core я тоже смотрел, там где Linq не влияет на производительность он используется, что и логично. Но вы же не станете спорить, что Linq добавляет некоторый overhead, и если в ядре какого-нибудь фреймворка метод выполняться будет сотни миллионов раз, и написание + пары строк для простого цикла сэкономит хотя бы 1% производительности, то можно пожертвовать выразительностью?

Information

Rating
Does not participate
Registered
Activity