При работе с большими объемами текстовой информации в «1С:Предприятии» разработчики часто сталкиваются с проблемой производительности встроенного языка. Операции конкатенации, посимвольного обхода и особенно эмуляция сложных математических или побитовых вычислений работают в 1С сравнительно медленно. Отличным решением в таких случаях является перенос «тяжелых» алгоритмов во внешнюю компоненту (ВК), написанную на компилируемом языке программирования (C++, Delphi, Rust или C#).
В этой статье мы подробно разберем, почему встроенный язык 1С оказывается неэффективным для реализации алгоритмов нечеткого поиска, как решить классическую проблему искажения кодировки (ошибка «РџРµС‚Р»СЏ» при сборке DLL), а также рассмотрим современные альтернативы и лучшие практики создания высокопроизводительных внешних компонент по технологии Native API.
Алгоритм Bitap (и его модификация Wu-Manber) — это чрезвычайно эффективный алгоритм нечеткого поиска, основанный на побитовых операциях. На компилируемых языках он работает практически со скоростью процессора, так как все операции сдвига, сложения и умножения масок выполняются на уровне регистров общего назначения за один или несколько тактов.
Однако при попытке перенести этот алгоритм на встроенный язык 1С мы сталкиваемся с жесткими платформенными ограничениями:
ПобитовыйСдвигВлево, ПобитовоеИЛИ и другие, работают исключительно с 32-битными целыми числами (максимальное значение — $2^{32}-1$). Это означает, что длина искомого шаблона ограничена всего 32 символами. Если длина поисковой строки больше, стандартные функции использовать невозможно.Сред, Лев, Прав, Формат и массовую замену символов через СтрЗаменить.Давайте проанализируем прототип такого алгоритма на встроенном языке 1С. Этот код наглядно демонстрирует, какое огромное количество строковых манипуляций приходится выполнять платформе на каждой итерации цикла:
&НаСервере
Функция АлгоритмНечеткогоПоиска(Текст, СтрокаПоиска) Экспорт
// Алгоритм Bitap, модификация от Wu-Manber
m = СтрДлина(Текст);
n = СтрДлина(СтрокаПоиска);
ФорматнаяСтрока = "ЧЦ=" + Формат(n, "ЧГ=0") + "; ЧН=; ЧВН=; ЧГ=0";
U = Новый Соответствие;
Для i = 1 По n Цикл
Ключ = Сред(СтрокаПоиска, i, 1);
БитовыйВектор = U.Получить(Ключ);
Если БитовыйВектор = Неопределено Тогда
БитовыйВектор = Формат(0, ФорматнаяСтрока);
КонецЕсли;
БитовыйВектор = Лев(БитовыйВектор, i - 1) + "1" + Прав(БитовыйВектор, n - i);
U.Вставить(Ключ, БитовыйВектор);
КонецЦикла;
МассивНоваяПопытка = Новый Массив(m);
МаксОшибка = Макс(2, Цел(СтрДлина(СтрокаПоиска) / 10));
Для Ошибка = 0 По МаксОшибка Цикл
M1 = Формат(0, ФорматнаяСтрока);
МассивПредыдущаяПопытка = Новый Массив;
Для Каждого Стр Из МассивНоваяПопытка Цикл
МассивПредыдущаяПопытка.Добавить(Стр);
КонецЦикла;
Для j = 1 По m Цикл
БитовыйВектор = U.Получить(Сред(Текст, j, 1));
Если БитовыйВектор = Неопределено Тогда
БитовыйВектор = Формат(0, ФорматнаяСтрока);
КонецЕсли;
УмножениеСВектором = ПобитовоеУмножение(СдвигВПраво(M1, n), БитовыйВектор, ФорматнаяСтрока);
Если Ошибка = 0 Тогда
ПредыдущаяПопыткаj_1 = Формат(0, ФорматнаяСтрока);
ИначеЕсли j = 1 Тогда
ПредыдущаяПопыткаj_1 = Формат(0, ФорматнаяСтрока);
Иначе
ПредыдущаяПопыткаj_1 = МассивПредыдущаяПопытка[j - 2];
КонецЕсли;
Если Ошибка = 0 Тогда
ПредыдущаяПопыткаj = Формат(0, ФорматнаяСтрока);
Иначе
ПредыдущаяПопыткаj = МассивПредыдущаяПопытка[j - 1];
КонецЕсли;
ВставкаM1 = ПобитовоеСложение(УмножениеСВектором, ПредыдущаяПопыткаj_1, ФорматнаяСтрока);
УдалениеM1 = ПобитовоеСложение(УмножениеСВектором, СдвигВПраво(ПредыдущаяПопыткаj, n), ФорматнаяСтрока);
ЗаменаM1 = ПобитовоеСложение(УмножениеСВектором, СдвигВПраво(ПредыдущаяПопыткаj_1, n), ФорматнаяСтрока);
M1 = ПобитовоеСложение(ВставкаM1, УдалениеM1, ФорматнаяСтрока);
M1 = ПобитовоеСложение(M1, ЗаменаM1, ФорматнаяСтрока);
МассивНоваяПопытка[j - 1] = M1;
Если Прав(M1, 1) = "1" Тогда
СтрокаДляСравнения = Формат(0, "ЧЦ=" + Формат(Ошибка + 1, "ЧГ=0") + "; ЧН=; ЧВН=; ЧГ=0");
СтрокаДляСравнения = СтрЗаменить(СтрокаДляСравнения, "0", "1");
СтрокаДляСравнения = СтрокаДляСравнения + Формат(0, "ЧЦ=" + Формат(n - Ошибка - 1, "ЧГ=0") + "; ЧН=; ЧВН=; ЧГ=0");
Для Инт = -(j - 1) По 0 Цикл
Если МассивНоваяПопытка[-Инт] = СтрокаДляСравнения Тогда
Прервать;
КонецЕсли;
КонецЦикла;
Возврат (Новый Структура("Слово, позиция, ошибок", Сред(Текст, -Инт + 1, j + Инт), -Инт + 1, Ошибка));
КонецЕсли;
КонецЦикла;
КонецЦикла;
Возврат (Новый Структура("Слово, позиция, ошибок", "", 0, 0));
КонецФункции
Функция ПобитовоеУмножение(Число1, Число2, ФорматнаяСтрока)
Возврат СтрЗаменить(СтрЗаменить(Формат(Число(Число1) + Число(Число2), ФорматнаяСтрока), "1", "0"), "2", "1");
КонецФункции
Функция ПобитовоеСложение(Число1, Число2, ФорматнаяСтрока)
Возврат СтрЗаменить(Формат(Число(Число1) + Число(Число2), ФорматнаяСтрока), "2", "1");
КонецФункции
Функция СдвигВПраво(Число1, n)
Возврат "1" + Лев(Число1, n - 1);
КонецФункции
Как мы видим, вспомогательные функции ПобитовоеУмножение, ПобитовоеСложение и СдвигВПраво вызываются внутри вложенных циклов миллионы раз. Постоянное преобразование типов (строка -> число -> строка), конкатенация строк и вызовы тяжелых функций форматирования приводят к катастрофическому падению производительности.
При попытке перенести этот алгоритм в нативную динамическую библиотеку (DLL) на C++, используя официальные шаблоны с ИТС (1C Software Development Kit), разработчики часто сталкиваются со странным поведением платформы. Компонента успешно компилируется и подключается в 1С, но при вызове любого метода выбрасывается исключение: «Ошибка при вызове метода контекста (Петля)».
Проанализируем ситуацию и разберем физику этого процесса:
D0 9F D0 B5 D1 82 D0 BB D1 8F.Петля..cpp и .h сохранены в кодировке UTF-8, но без сигнатуры BOM (Byte Order Mark), компилятор старых версий Visual Studio (например, VS 2013) по умолчанию считает их написанными в системной ANSI-кодировке (для русскоязычной Windows это CP1251).L"Петля" или u"Петля", которые возвращаются платформе 1С через интерфейс регистрации имен методов GetMethodName, компилируются некорректно. Компилятор преобразует искаженную ANSI-строку в широкий формат (UTF-16), и 1С получает зарегистрированное имя метода как Петля вместо Петля. Естественно, при вызове нормального имени метода платформа выдает ошибку, так как не может сопоставить имя.Как исправить эту проблему в MS Visual Studio?
Для устранения этой неприятной ошибки необходимо выполнить два простых шага:
.cpp или .h в Visual Studio, перейдите в меню Файл -> Дополнительные параметры сохранения (Advanced Save Options) и выберите кодировку «Юникод (UTF-8, с сигнатурой), кодовая страница 65001»./utf-8.Если вы не хотите погружаться в сложности настройки окружения Visual Studio C++, отличной альтернативой является разработка компоненты в среде Delphi или кроссплатформенном Lazarus (Free Pascal). Шаблоны для Delphi работают стабильно и позволяют быстро скомпилировать библиотеку.
Однако при разработке на Delphi под современные требования 1С нужно учитывать важный нюанс — поддержку 64-битных платформ (x64). Серверы «1С:Предприятия» под управлением Windows и Linux практически всегда являются 64-разрядными, поэтому и компонента должна компилироваться в x64-версии.
В старых шаблонах Delphi для 1С часто использовался жестко заданный тип Integer или Cardinal для работы с адресами памяти и структурами tVariant. При переходе на 64-битную платформу указатели становятся 8-байтовыми, что приводило к падениям платформы с ошибками доступа к памяти (Access Violation). Чтобы этого избежать, обязательно используйте современные кроссплатформенные типы данных:
Integer для работы с указателями и дескрипторами используйте платформонезависимый тип NativeInt (в C++ ему соответствует intptr_t).Cardinal используйте NativeUInt (в C++ — uintptr_t)..so) рекомендуется использовать среду Lazarus (FPC), которая отлично справляется с выравниванием структур памяти в формате GCC.Технологии не стоят на месте, и сегодня писать ВК на "чистом" C++ или Delphi уже не обязательно. Разберем две современные и крайне производительные альтернативы.
Язык программирования Rust обеспечивает безопасность работы с памятью на уровне компилятора и производительность, аналогичную C/C++. В экосистеме Rust существует готовая библиотека (crate) с открытым исходным кодом, которая называется native_api_1c.
Используя эту библиотеку, вам больше не нужно вручную реализовывать десятки интерфейсных функций Native API (таких как RegisterMethods, CallAsFunc и т.д.). Достаточно описать структуру и пометить ее специальными макросами-атрибутами:
#[derive(AddIn)]
pub struct FuzzySearchAddIn {
#[add_in_prop(name = "MaxErrors", readable = true, writable = true)]
max_errors: i32,
}
#[add_in_concl]
impl FuzzySearchAddIn {
#[add_in_func(name = "Search", name_ru = "Найти")]
fn search(&self, text: String, pattern: String) -> String {
// Здесь пишется безопасный и сверхбыстрый код на Rust
todo!()
}
}
Компилятор Rust самостоятельно сгенерирует весь необходимый оберточный код C-интерфейса, корректно обработает преобразование строк из формата 1С (UTF-16/WCHAR_T) в безопасные строки Rust (UTF-8) и обратно, гарантируя отсутствие утечек памяти и проблем с кодировками.
Многие разработчики привыкли создавать внешние компоненты на C# по технологии COM. Однако COM-компоненты привязаны к реестру Windows и принципиально не работают на серверах под управлением Linux. Начиная с версии .NET 7/8+, у нас появилась возможность компилировать код C# напрямую в машинный код с использованием технологии Native AOT (Ahead-Of-Time).
Мы можем написать обычный высокопроизводительный класс на C# и экспортировать его функции наружу для платформы 1С через атрибут [UnmanagedCallersOnly]. Полученная DLL-библиотека не будет требовать установленного .NET Runtime на сервере и сможет подключаться в 1С как обычная Native-компонента как под Windows (dll), так и под Linux (so).
Давайте посмотрим на реальные цифры, полученные в результате тестирования алгоритма нечеткого поиска. Сравним чистую реализацию на 1С (с эмуляцией побитовых операций строками) и скомпилированную DLL-библиотеку на Delphi (где вычисления проводятся аппаратно на уровне процессора).
Для теста использовался фрагмент текста объемом 2500 символов. Производился циклический поиск (50 итераций) двух строк с опечатками (в каждой строке по 2 ошибки):
Результаты замеров времени выполнения (в секундах):
Такой колоссальный разрыв в производительности объясняется тем, что процессор выполняет ассемблерные инструкции битовых сдвигов и сложения масок мгновенно, в то время как виртуальная машина 1С тратит огромное количество ресурсов на создание, удаление и поиск подстрок в памяти.
Если вы решили написать собственную высокопроизводительную компоненту для нечеткого поиска, рекомендуем применить несколько классических подходов оптимизации:
UInt64 (например, массив из 4 элементов позволит искать строки длиной до 256 символов с минимальными потерями в скорости).Перенос критичных к скорости вычислений алгоритмов во внешние компоненты — это стандартная и очень эффективная практика в мире «1С:Предприятия». Понимание принципов работы с памятью, кодировок и использование современных инструментов разработки позволит вам создавать быстрые и надежные решения для бизнеса.