Skip to content

Дешевле обёртка вызова методов контекста - #1747

Merged
EvilBeaver merged 1 commit into
EvilBeaver:developfrom
sfaqer:feature/context-call-marshalling
Sep 27, 2026
Merged

EvilBeaver merged 1 commit into
EvilBeaver:developfrom
sfaqer:feature/context-call-marshalling

Conversation

@sfaqer

@sfaqer sfaqer commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Обёртка, через которую машина вызывает методы контекста, стоила 25–33 нс на вызов сверх самого метода. Две причины:

  • скомпилированная лямбда звала C#-метод не напрямую, а через делегат из замыкания. Делегат остался от ручных обёрток CreateFunction<T1, T2, …>, которые были до перехода на деревья выражений (9e82cea); в дереве он не нужен — теперь Expression.Call прямо на методе;
  • ConvertParam<T> на каждый аргумент шёл через необобщённый ConvertValueType: Nullable.GetUnderlyingType, сравнение SystemType по строковому id, цепочка type == typeof(...) и упаковка числа в object. ConvertReturnValue<T> упаковывал bool/int и разбирал их switch-ем.

Теперь преобразование выбирается один раз, при построении обёртки (ContextValuesMarshaller.GetParameterConverter / GetReturnValueConverter). Для параметров int/decimal/bool/string это отдельные методы: значение нужного вида берётся сразу, остальное (пропущенный аргумент, ссылка на переменную, Неопределено, строка вместо числа) идёт прежним ConvertParam<T>. Возврат bool/int/decimal — без упаковки. Публичные ConvertParam<T> / ConvertReturnValue<T> не менялись. Так же собираются конструкторы в TypeFactory. Тест ContextValuesMarshallerTest на граничные случаи проходит и на develop, и на ветке.

Итого: сама обёртка на вызовах с числами дешевле на 30–40% и без аллокаций, на IValue — на 7%. В скриптах — −4…−8% там, где в вызове числа (Массив.Количество, Массив.Установить, СтрНайти, Рефлектор.ВызватьМетод), и на 24 Б меньше аллокаций на вызов; остальное в шуме, пиковая память та же.

Вместе с #1746 почти закрывает регрессию Заблокировать() из #1745 (проверял локальным слиянием трёх веток): пара «заблокировать/разблокировать» за вычетом пустого цикла — 141 нс на develop, 211 нс на #1745, 158 нс со всеми тремя. Остаток — реестр блокировок и внедрение процесса.

Бенчи. BenchmarkDotNet 0.14 (проект — #1748), .NET 8, i7-13700KF, Windows 11. По 3 запуска на версию: JIT от процесса к процессу компилирует горячий путь обёртки то в быстрый, то в медленный вариант — у обеих версий, — и в одном запуске разница бывает случайной.

BenchmarkDotNet: develop → ветка, время и аллокации на операцию

Обёртка, вызов из C#:

develop ветка
Массив.Установить(0, x) 32,8 нс, 24 Б 19,9 нс (0,61), 0 Б
Массив.Количество() 28,9 нс, 24 Б 19,7 нс (0,68), 0 Б
Массив.Получить(0) 25,8 нс, 24 Б 15,2 нс (0,59), 0 Б
Соответствие.Получить(x) 33,0 нс, 32 Б 30,4 нс (0,93), 32 Б

Из 1Script, стековая машина, на итерацию цикла:

develop ветка
пустой цикл 62,9 нс, 33 Б 61,7 нс (0,98), 33 Б
Заблокировать() + Разблокировать() 203,1 нс, 129 Б 196,6 нс (0,97), 129 Б
Массив.Установить(0, Сч) 308,6 нс, 177 Б 296,7 нс (0,96), 153 Б
Р = Массив.Количество() 157,6 нс, 105 Б 145,3 нс (0,92), 81 Б
Массив.Добавить(Сч) 292,1 ± 13,9 нс, 155 Б 300,5 ± 16,0 нс (1,03), 155 Б
Структура.Вставить("Ключ", Сч) 382,1 нс, 153 Б 373,2 нс (0,98), 153 Б
Р = Соответствие.Получить(1) 256,5 нс, 161 Б 259,0 нс (1,01), 161 Б
Р = СтрНайти("строка", "к") 198,5 нс, 225 Б 182,5 нс (0,92), 201 Б
Р = Сложить(Сч, 1) — функция скрипта 321,5 нс, 593 Б 328,3 нс (1,02), 593 Б
Р = СПоУмолчанию(Сч) 258,3 нс, 609 Б 260,2 нс (1,01), 609 Б
Р = ЭтотОбъект.Сложить(Сч, 1) 476,1 нс, 881 Б 485,1 нс (1,02), 881 Б
Рефлектор.ВызватьМетод(М, "Установить", …) 797,5 нс, 497 Б 757,1 нс (0,95), 473 Б

Функции и методы скрипта обёртку не затрагивают — это контроль; разница до ~3% между сборками — шум раскладки кода.

Весь процесс: время, CPU, аллокации, сборки, пиковая память (develop → ветка)

Каждая нагрузка — отдельный процесс oscript, 4 прогона на прогрев + 4 замера, 5 раундов, медианы; CPU и память — по всему процессу через DOTNET_STARTUP_HOOKS.

Нагрузка время замера, мс CPU процесса, мс выделено, МБ сборок gen0 пик WS, МБ
старт (пустой скрипт) — 141 → 141 0% 2 → 2 0% 0 → 0 0% 38 → 38 0%
Массив.Количество() 354 → 336 −5% 3125 → 3078 −2% 1589 → 1223 −23% 106 → 81 −24% 57 → 57 0%
Массив.Установить(0, Сч) 508 → 483 −5% 4609 → 4406 −4% 2017 → 1742 −14% 134 → 116 −13% 58 → 58 0%
Массив.Добавить(Сч) 536 → 535 0% 4641 → 4625 0% 1723 → 1723 0% 117 → 117 0% 154 → 154 0%
Структура.Вставить("Ключ", Сч) 401 → 396 −1% 3766 → 3703 −2% 1162 → 1162 0% 77 → 77 0% 58 → 58 0%
Соответствие.Получить(1) 424 → 428 +1% 3953 → 4016 +2% 1833 → 1833 0% 122 → 122 0% 58 → 58 0%
СтрДлина(...) ¹ 227 → 228 +1% 2156 → 2156 0% 491 → 491 0% 32 → 32 0% 57 → 57 0%
СтрНайти(..., ...) 338 → 304 −10% 3062 → 2766 −10% 2566 → 2291 −11% 171 → 153 −11% 57 → 57 0%
Сложить(Сч, 1) — функция скрипта 324 → 329 +2% 2875 → 2938 +2% 4519 → 4519 0% 302 → 302 0% 57 → 57 0%
СПоУмолчанию(Сч) — 1 из 3 парам. 276 → 270 −2% 2453 → 2438 −1% 4641 → 4641 0% 310 → 310 0% 57 → 57 0%
ПустаяПроцедура() 225 → 232 +3% 2078 → 2141 +3% 2291 → 2291 0% 153 → 153 0% 57 → 57 0%
ЭтотОбъект.Сложить(Сч, 1) 514 → 523 +2% 4281 → 4422 +3% 6716 → 6716 0% 449 → 449 0% 59 → 59 0%
Рефлектор.ВызватьМетод → скрипт 531 → 522 −2% 4891 → 4797 −2% 4580 → 4580 0% 306 → 306 0% 59 → 59 0%
Рефлектор.ВызватьМетод → Массив 830 → 791 −5% 7156 → 6812 −5% 3787 → 3604 −5% 253 → 240 −5% 57 → 58 +2%
Заблокировать()/Разблокировать() ² 341 → 364 +7% 3109 → 3281 +6% 1467 → 1467 0% 98 → 98 0% 57 → 57 0%

¹ СтрДлина компилируется во встроенную инструкцию, а не в вызов метода — контрольная нагрузка.
² Процесс попадает то в режим ~338 мс, то в ~365 мс — у обеих сборок; здесь develop попал в быстрый режим в 4 раундах из 5, ветка — в 2. BenchmarkDotNet на тех же сборках: 0,97.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of integer, decimal, boolean, and string values passed to or returned from context methods.
    • Integer values that exceed the supported range now produce a clear invalid-argument error instead of overflowing.
    • Preserved support for optional, default, skipped, and empty arguments when calling context methods and constructors.

@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 0b7705a1-23f2-430c-a1b5-22a1ffd71a3f

📥 Commits

Reviewing files that changed from the base of the PR and between 350ac33 and b8d046b.

📒 Files selected for processing (4)
  • src/ScriptEngine/Machine/Contexts/ContextMethodMapper.cs
  • src/ScriptEngine/Machine/Contexts/ContextValuesMarshaller.cs
  • src/ScriptEngine/Machine/TypeFactory.cs
  • src/Tests/OneScript.Core.Tests/ContextValuesMarshallerTest.cs

Included review availability: Your plan provides up to 4 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

The marshaller adds specialized conversions for selected primitive parameter and return types. Generated expressions call wrapped context methods directly and use converter selectors. Constructors also use the parameter converter selector. Tests cover conversions, defaults, overflow, and wrapped method calls.

Changes

Primitive conversion and context invocation

Layer / File(s) Summary
Primitive converter selection and implementations
src/ScriptEngine/Machine/Contexts/ContextValuesMarshaller.cs
Converter maps select specialized parameter converters for int, decimal, bool, and string, and return converters for int, decimal, and bool. Other types use generic converters. Integer overflow produces InvalidArgumentValue.
Context and constructor conversion wiring
src/ScriptEngine/Machine/Contexts/ContextMethodMapper.cs, src/ScriptEngine/Machine/TypeFactory.cs, src/Tests/OneScript.Core.Tests/ContextValuesMarshallerTest.cs
Generated expressions call wrapped methods directly on their instances and use the converter selectors. Constructors use the parameter converter selector. Tests cover primitive conversions, defaults, skipped arguments, overflow, and wrapped method calls.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Refactor

Suggested reviewers: evilbeaver

Merge Risk: ⚪ Minimal · up to b8d04

The wrapper changes have no identified behavior regression and are ready to merge after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 626ed

The invocation path changes, but the reviewed code continues to call the same exposed methods, pass the same process context, and use existing conversion behavior for other values. No new security issue was established. Exception behavior for host methods that throw has not been compared end to end.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The affected exposure is script-supplied arguments reaching already mapped host context methods and constructors; the reviewed invocation change does not show an expanded set of callable methods.

Trust Boundaries and Controls

  • observed — Script arguments continue through parameter conversion, while the process object is passed separately by the caller. Values outside the specialized primitive cases continue through the existing conversion path.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 27 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: reducing the overhead of wrappers for context-method calls.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

// ConvertParam<TypeOfArgN>(args[i], defaults[i]));
// ConvertParam<TypeOfArgN>(args[N-1], defaultN, process));
// }
// Метод вызывается напрямую, без промежуточного делегата: это заметно дешевле на каждом вызове.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Кажется эту последнюю строчку комментария надо удалить. Она интересна в контексте текущего PR, но на будущее ее нет смысла хранить, т.к. она объясняет уже ушедшее из кода состояние (промежуточный делегат)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Убрал.

public static T ConvertParam<T>(IValue value, T defaultValue, IBslProcess process)
{
// Частые случаи - без упаковки и перебора типов в ConvertValueType, результат тот же.
// Для значимых T проверки typeof JIT вычисляет при компиляции

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Покорежило перевод. "значимых" имелся в виду value-type? Тогда надо другое слово, "примитивных" или "типов-значений" как-то так.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Этого комментария больше нет: быстрые пути ушли из ConvertParam<T>, подробности в общем комментарии.

public static IValue ConvertReturnValue<TRet>(TRet param)
{
// Частые значимые типы - без упаковки, проверки JIT вычисляет при компиляции
if (typeof(TRet) == typeof(bool))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не очень мне, конечно, нравится, что разъехалась в разные места логика конвертации примитивов, но чего не сделаешь ради производительности...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

В новой версии быстрые пути собраны в одном месте — в начале ContextValuesMarshaller, рядом с выбором. И всё, что не подошло по виду значения, они отдают в общий ConvertParam<T>, так что правила преобразования по-прежнему в одном месте.

@sfaqer
sfaqer force-pushed the feature/context-call-marshalling branch from 285534b to 626ed2f Compare September 25, 2026 06:11
@sfaqer

sfaqer commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

Перепроверил BenchmarkDotNet (бенч-проект пришлю отдельным PR) и нашёл у себя ошибку: проверки typeof(T) == typeof(int) в обобщённых ConvertParam<T> / ConvertReturnValue<T> для ссылочных T не бесплатны — на IValue +1,5–2 нс, и Соответствие.Получить в BSL стал на 7% медленнее.

Переделал: преобразование выбирается один раз, при построении обёртки (ContextValuesMarshaller.GetParameterConverter / GetReturnValueConverter), публичные ConvertParam<T> / ConvertReturnValue<T> как в develop. Так же собираются конструкторы в TypeFactory. Одобрение было на прошлую версию — глянь, пожалуйста, ещё раз.

{
private static readonly Dictionary<Type, MethodInfo> _primitiveParameterConverters = new Dictionary<Type, MethodInfo>
{
[typeof(int)] = GetOwnMethod(nameof(ConvertInt32Param)),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

обычный switch не будет тут выгоднее Dictionary?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Выбор делается один раз — при построении обёртки метода, а не на вызове, так что скорость тут не важна. Словарь — ради точного совпадения типа: switch по Type.GetTypeCode отнёс бы к int и перечисления с базовым int, а цепочка if по typeof вышла бы длиннее. Если читается хуже — переделаю на if.

Скомпилированная лямбда вызывает C#-метод напрямую, а не через делегат
из замыкания - делегат остался от ручных оберток до перехода на деревья
выражений.

Для параметров int/decimal/bool/string и возврата bool/int/decimal
обертка при построении выбирает отдельные методы преобразования: значение
нужного вида берется сразу, без упаковки и перебора типов, остальное идет
прежним путем ConvertParam<T>. Так же собираются конструкторы в TypeFactory.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@sfaqer
sfaqer force-pushed the feature/context-call-marshalling branch from b8d046b to 4450f74 Compare September 27, 2026 00:41
@EvilBeaver
EvilBeaver merged commit 5e30c04 into EvilBeaver:develop Sep 27, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants