Ускорит ли внешняя компонента на C++ ваш код 1С: как посчитать до того, как писать
Иван Недомолков · Dərc edilib:
Расчёт крутится минуту, разбор файла тормозит обмен, поиск дублей в справочнике не дождаться. Первая мысль опытного разработчика: вынести узкое место во внешнюю компоненту на C++ (технология Native API). Интерпретатор 1С медленный, машинный код быстрый, значит будет быстрее.
Иногда так и выходит, причём в сотни раз. А иногда компонента получается медленнее кода, который заменила, и неделя работы уходит в минус. Различить эти случаи можно заранее, без единой строки на C++. Для этого нужно знать три вещи: как подключена компонента, сколько 1С будет готовить данные для неё и разбирать её ответ, и есть ли у задачи готовая платформенная функция.
Ниже методика с цифрами. Они взяты из замера 31 задачи: каждую мы решили на встроенном языке (до трёх вариантов, от наивного до лучшего) и своей компонентой, результаты всех вариантов сверены по MD5, время снято от начала до конца с раскладкой по фазам. Стенд: Windows 10 x64, Core i7-9750H, 16 ГБ, платформа 8.3.27.1606, клиент-серверная база, всё исполняется на сервере, сервер без отладки.
Время компоненты складывается из трёх частей
время компоненты = подготовка данных в 1С + вызов (передача туда и обратно + расчёт) + разбор ответа в 1С
Компонента принимает и возвращает только строки, числа и двоичные данные. Структуру, массив или таблицу значений она вернуть не может. Значит, если на входе у вас таблица, 1С сначала превращает её в текст, а если результат нужен коллекцией, 1С собирает её из текста ответа. Обе операции идут на встроенном языке, то есть на той же медленной скорости, от которой вы бежите.
Как это выглядит в цифрах, 100 000 записей, компонента в процессе 1С:
| Задача | Лучшая 1С | Компонента всего | Подготовка | Вызов | Разбор ответа |
|---|---|---|---|---|---|
| JSON в массив структур | 590 мс (ПрочитатьJSON) | 1 133 мс | 0 | 52 мс | 1 003 мс |
| JSON в таблицу значений | 966 мс | 1 480 мс | 0 | 62 мс | 1 389 мс |
| JSON из массива структур | 540 мс (ЗаписатьJSON) | 703 мс | 656 мс | 42 мс | 0 |
| сумма и количество по ключу в JSON | 1 105 мс | 43 мс | 0 | 43 мс | 0,2 мс |
Компонента в первых трёх строках работала 42-62 мс. Проиграла она из-за того, что до и после неё делала 1С. В последней строке на вход идёт готовый текст, на выход несколько чисел, и выигрыш 26 раз.
Отсюда первое правило оценки: прикиньте, сколько стоит подготовка и разбор на встроенном языке. Если это больше, чем лучший вариант решения задачи целиком на 1С, компонента не поможет при любом C++.
Цена вызова: одна строка подключения меняет её в 30 раз
На 8.3.27 у ПодключитьВнешнююКомпоненту есть четвёртый параметр типа ТипПодключенияВнешнейКомпоненты со значениями Изолированно и НеИзолированно. Без него компонента на нашем сервере вела себя ровно как с явным Изолированно: работала в отдельном процессе, и каждый вызов шёл через обмен между процессами.
// Компонента в процессе 1С: вызов почти бесплатный,
// но сбой компоненты роняет рабочий процесс вместе с сеансами
Подключена = ПодключитьВнешнююКомпоненту(Адрес, "Nativka", ТипВнешнейКомпоненты.Native,
ТипПодключенияВнешнейКомпоненты.НеИзолированно);
Сколько стоит граница в каждом режиме:
| Что | Изолированно | В процессе 1С |
|---|---|---|
| пустой вызов | около 123 мкс | около 4 мкс |
| передать 1 млн символов | 90 мс | 1,1 мс |
| получить 1 млн символов | 38 мс | 2,4 мс |
| 1 МБ двоичных данных туда и обратно | 205 мс | 1,4 мс |
Для сравнения, пустая процедура на 1С стоит около 1,4 мкс на вызов. Компонента в процессе 1С дороже её втрое, изолированная - примерно в девяносто раз.
Эти четыре строки дают второй инструмент оценки. Посчитайте, сколько текста уйдёт в компоненту и сколько вернётся, умножьте на цену из таблицы. Если в изолированном режиме одна передача уже съедает время лучшего варианта на 1С, писать компоненту под этот режим бессмысленно.
Про изолированный режим ещё два наблюдения на этом сервере, оба стоили нам полдня. Двоичные данные больше 163 803 байт приходят в изолированную компоненту строкой Base64 в варианте для адресов, а не двоичными данными; обратно двоичные данные больше 70 182 байт возвращаются тоже строкой. И изолированная компонента, однажды подключённая в базе, не подменялась новой сборкой даже под другим именем подключения и другим именем DLL, пока не перезапустили сервер. В процессе 1С обоих эффектов не было.
На 8.3.20 этого параметра в платформе нет вовсе. Как там ведёт себя граница, мы не мерили.
Выбор режима: скорость против устойчивости
Изоляция придумана не зря. Падение компоненты в отдельном процессе не роняет рабочий процесс 1С, а в процессе 1С роняет вместе со всеми сеансами, которые на нём живут. Поэтому правило такое:
- компонента своя, покрыта тестами и не бросает исключений через границу:
НеИзолированно, и вы получаете цифры из правой колонки; - компонента чужая или сырая: изоляция, и тогда выигрывают только задачи с маленьким входом и большим расчётом (о них ниже).
Какие задачи выигрывают и с какого объёма
Сводка по всем группам. “Окупается с” - наименьший объём из лестницы замера, на котором компонента в процессе 1С обогнала лучший вариант на 1С.
| Задача | Объём замера | Лучшая 1С | В процессе 1С | Изолированно | Окупается с |
|---|---|---|---|---|---|
| рюкзак 0/1, 100 предметов | вместимость 50 000 | 6 252 мс | 5 мс | 4,8 мс | 1 000 |
| сумма квадратов в цикле | 10 млн | 8 946 мс | 7,5 мс | 7,5 мс | 100 000 |
| Монте-Карло, число пи | 1 млн точек | 3 115 мс | 8,8 мс | 8 мс | 100 000 |
| нечёткие дубли, Левенштейн до 2 | 1 000 наименований | 1 403 мс | 6 мс | 16 мс | 100 |
| построчное сравнение текстов | 20 000 строк | 1 205 мс | 7,5 мс | 166 мс | 1 000 |
| расстояние Левенштейна | 10 000 пар | 4 602 мс | 39 мс | 119 мс | 100 |
| переложить JSON с переименованием полей | 100 000 записей | 1 376 мс | 49 мс | 1 612 мс | 1 000 |
| итоги по ключу в JSON | 100 000 записей | 1 105 мс | 43 мс | 1 253 мс | 1 000 |
| итоги дат и чисел из текста | 1 млн строк | 2 959 мс | 165 мс | 2 055 мс | 10 000 |
| 50 подстрок в тексте | 10 млн символов | 832 мс | 229 мс | 2 799 мс | 100 000 |
| Base64, декодирование | 50 МБ | 1 149 мс | 324 мс | дольше 10 с | 1 млн байт |
| разбор CSV в массив массивов | 100 000 строк | 962 мс | 375 мс | 1 712 мс | 1 000 |
| SHA-256 | 100 МБ | 469 мс | 505 мс | - | паритет |
| 20 массовых замен | 10 млн символов | 253 мс | 136 мс | 3 884 мс | 100 000 |
| сортировка чисел | 1 млн | 2 701 мс | 3 094 мс | 4 148 мс | не окупается |
| регулярное выражение | 10 млн символов | 440 мс | 3 476 мс | 5 322 мс | не окупается |
| линейная регрессия | 100 000 точек | 173 мс | 268 мс | 356 мс | не окупается |
| число в строку, вызов на каждое | 1 млн | 1 219 мс | 7 197 мс | - | не окупается |
Из таблицы складываются три типа задач.
Мало данных на входе, много арифметики. Динамическое программирование, перебор, моделирование, попарные расстояния. Компонента выигрывает в десятки и сотни раз в любом режиме, окупается уже на сотнях элементов. Рюкзак, Монте-Карло и матрицы почти не чувствуют изоляцию.
Текст на входе, короткий итог на выходе. Агрегат по JSON, поиск по тексту, разбор дат и чисел с суммированием, сравнение текстов. В процессе 1С выигрыш от 2,5 раза у CSV до 28 у JSON, у сравнения текстов 160. В изолированном режиме он пропадает: десятки миллисекунд расчёта тонут в секунде передачи текста между процессами.
Результат нужен коллекцией или задача уже решена платформой. Разбор JSON в структуры, таблица значений, сортировка, регулярки, хеш. Здесь компонента проигрывает или идёт вровень: либо время съедает сборка коллекции на 1С, либо платформенная функция уже работает в машинном коде. Регулярное выражение в 8 раз медленнее, потому что std::regex уступает движку платформы, и СтрНайтиВсеПоРегулярномуВыражению (с 8.3.23) здесь вне конкуренции.
Где компонента не нужна совсем
Самая дорогая ошибка - писать компоненту под задачу, у которой уже есть штатная функция. CRC32 одного мегабайта самописным кодом на встроенном языке занимает 6-7 секунд, ХешированиеДанных - около миллисекунды. Регулярки, паузу, распаковку RAR и 7z, буфер обмена, WebSocket платформа научилась делать в 8.3.22-8.3.27, и многие компоненты под эти задачи потеряли смысл. Список с проверкой, где какая замена работает, мы собрали отдельно: что заменить штатным в 8.3.22-8.3.27.
Вторая ошибка - вызов компоненты в цикле на каждый элемент. Возвести в квадрат 100 000 чисел на 1С заняло 95 мс, тем же циклом с вызовом компоненты на каждое число - 546 мс в процессе 1С и 7 930 мс изолированно. Компоненте надо отдавать работу пачкой: весь текст, весь массив, один вызов.
Потоки и точность
Внутри одного вызова компонента может распараллелить работу, чего во встроенном языке нет совсем. Монте-Карло на 50 млн точек: 372 мс в один поток, 42 мс на всех двенадцати потоках шестиядерного процессора, в 9 раз быстрее. А нечёткие дубли на 3 000 наименований ускорились только с 31 до 11 мс: работы мало, запуск потоков съедает выигрыш. Число потоков стоит делать параметром с умолчанием 1: компонента, которая без спроса займёт все ядра сервера, мешает остальным сеансам.
Точность - отдельный повод остановиться. Линейная регрессия на миллионе точек не прошла сверку результатов: 1С считает десятичными числами и дала свободный член 49,425190, компонента в double - 49,425202. Для денег и количеств в учёте перенос расчёта в double без такой сверки опасен.
Чек-лист перед тем, как писать компоненту
- Есть ли платформенная функция? Хеш, JSON, регулярки, сортировка коллекции, архивы. Если есть, компонента не нужна.
- Сколько данных уходит и возвращается? Умножьте объём на цену границы из таблицы для вашего режима.
- Нужен ли результат коллекцией? Если да, оцените время сборки этой коллекции на 1С. Часто оно одно больше всего остального.
- В каком режиме будет работать компонента? Если изолированно, выигрывают только задачи с малым входом.
- Не вызывается ли она в цикле? Пачка вместо поэлементных вызовов.
- Совпадает ли результат? Сверка с вариантом на 1С на тех же данных, особенно для дробных чисел.
Наши цифры сняты на одной машине под Windows. На Linux мы не мерили, и на вашем сервере абсолютные миллисекунды будут другими. Чтобы не гадать, мы собрали бенчмарк в виде внешней обработки: компонента лежит внутри макетом, 31 тест прогоняется на вашем сервере в обоих режимах подключения, результат сохраняется в JSON рядом с описанием окружения. Десять выигравших ядер из таблицы выше доступны из неё как обычные функции.
Если узкое место уже найдено, а разбираться, где именно уходит время, некогда, напишите нам в WhatsApp: замерим по фазам и скажем, что даст больше, оптимизация на встроенном языке, платформенная функция или компонента.