Как перенести функцию нечеткого поиска из 1С во внешнюю компоненту (DLL) и решить проблемы с кодировкой

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При работе с большими объемами текстовой информации в «1С:Предприятии» разработчики часто сталкиваются с проблемой производительности встроенного языка. Операции конкатенации, посимвольного обхода и особенно эмуляция сложных математических или побитовых вычислений работают в 1С сравнительно медленно. Отличным решением в таких случаях является перенос «тяжелых» алгоритмов во внешнюю компоненту (ВК), написанную на компилируемом языке программирования (C++, Delphi, Rust или C#).

В этой статье мы подробно разберем, почему встроенный язык 1С оказывается неэффективным для реализации алгоритмов нечеткого поиска, как решить классическую проблему искажения кодировки (ошибка «РџРµС‚Р»СЏ» при сборке DLL), а также рассмотрим современные альтернативы и лучшие практики создания высокопроизводительных внешних компонент по технологии Native API.

Почему алгоритмы нечеткого поиска (Bitap / Wu-Manber) работают медленно в 1С

Алгоритм Bitap (и его модификация Wu-Manber) — это чрезвычайно эффективный алгоритм нечеткого поиска, основанный на побитовых операциях. На компилируемых языках он работает практически со скоростью процессора, так как все операции сдвига, сложения и умножения масок выполняются на уровне регистров общего назначения за один или несколько тактов.

Однако при попытке перенести этот алгоритм на встроенный язык 1С мы сталкиваемся с жесткими платформенными ограничениями:

  1. Ограничение разрядности побитовых операций в 1С. Встроенные функции платформы, такие как ПобитовыйСдвигВлево, ПобитовоеИЛИ и другие, работают исключительно с 32-битными целыми числами (максимальное значение — $2^{32}-1$). Это означает, что длина искомого шаблона ограничена всего 32 символами. Если длина поисковой строки больше, стандартные функции использовать невозможно.
  2. Необходимость эмуляции двоичной арифметики через строки. Чтобы обойти ограничение в 32 бита, разработчикам приходится эмулировать битовые векторы при помощи обычных строк, состоящих из символов «0» и «1». Любое «побитовое» умножение или сложение превращается в посимвольный обход строки, вызовы функций Сред, Лев, Прав, Формат и массовую замену символов через СтрЗаменить.

Давайте проанализируем прототип такого алгоритма на встроенном языке 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++

При попытке перенести этот алгоритм в нативную динамическую библиотеку (DLL) на C++, используя официальные шаблоны с ИТС (1C Software Development Kit), разработчики часто сталкиваются со странным поведением платформы. Компонента успешно компилируется и подключается в 1С, но при вызове любого метода выбрасывается исключение: «Ошибка при вызове метода контекста (Петля)».

Проанализируем ситуацию и разберем физику этого процесса:

Как исправить эту проблему в MS Visual Studio?

Для устранения этой неприятной ошибки необходимо выполнить два простых шага:

  1. Пересохранить файлы исходного кода с BOM-сигнатурой. Откройте проблемный файл .cpp или .h в Visual Studio, перейдите в меню Файл -> Дополнительные параметры сохранения (Advanced Save Options) и выберите кодировку «Юникод (UTF-8, с сигнатурой), кодовая страница 65001».
  2. Использовать флаг компилятора. Если вы используете современную версию Visual Studio (начиная с VS 2015), вы можете явно указать компилятору интерпретировать исходные файлы как UTF-8. Для этого добавьте в свойствах проекта в разделе C/C++ -> Командная строка дополнительный флаг: /utf-8.

Создание внешней компоненты Native API на Delphi

Если вы не хотите погружаться в сложности настройки окружения 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). Чтобы этого избежать, обязательно используйте современные кроссплатформенные типы данных:

Современные альтернативы: Rust и C# (Native AOT)

Технологии не стоят на месте, и сегодня писать ВК на "чистом" C++ или Delphi уже не обязательно. Разберем две современные и крайне производительные альтернативы.

1. Разработка на языке Rust

Язык программирования 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) и обратно, гарантируя отсутствие утечек памяти и проблем с кодировками.

2. Разработка на C# с использованием Native AOT

Многие разработчики привыкли создавать внешние компоненты на 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

Давайте посмотрим на реальные цифры, полученные в результате тестирования алгоритма нечеткого поиска. Сравним чистую реализацию на 1С (с эмуляцией побитовых операций строками) и скомпилированную DLL-библиотеку на Delphi (где вычисления проводятся аппаратно на уровне процессора).

Для теста использовался фрагмент текста объемом 2500 символов. Производился циклический поиск (50 итераций) двух строк с опечатками (в каждой строке по 2 ошибки):

  1. Короткая поисковая строка (например, «Счfт на оjлату»).
  2. Длинная поисковая строка (фрагмент предложения длиной около 120 символов).

Результаты замеров времени выполнения (в секундах):

Такой колоссальный разрыв в производительности объясняется тем, что процессор выполняет ассемблерные инструкции битовых сдвигов и сложения масок мгновенно, в то время как виртуальная машина 1С тратит огромное количество ресурсов на создание, удаление и поиск подстрок в памяти.

Рекомендации для максимальной оптимизации алгоритма в DLL

Если вы решили написать собственную высокопроизводительную компоненту для нечеткого поиска, рекомендуем применить несколько классических подходов оптимизации:

  1. Используйте тип UInt64 для битовых масок. Большинство поисковых паттернов укладываются в длину до 64 символов. Использование беззнакового 64-битного целого числа позволит вам выполнять итерацию алгоритма Bitap всего за одну процессорную инструкцию.
  2. Организуйте цепочки битовых векторов для сверхдлинных шаблонов. Если искомая строка превышает 64 символа, не переходите на строковую эмуляцию. Вместо этого используйте статические массивы из нескольких элементов типа UInt64 (например, массив из 4 элементов позволит искать строки длиной до 256 символов с минимальными потерями в скорости).
  3. Используйте гибридный подход. Перед запуском точного побитового сравнения Bitap, прогоните текст через простой и быстрый алгоритм предварительной фильтрации (например, метод *N-грамм* или упрощенную эвристику *Бойера-Мура*). Это позволит мгновенно отсечь заведомо неподходящие блоки текста и запускать тяжелый посимвольный анализ только на потенциально успешных участках.

Перенос критичных к скорости вычислений алгоритмов во внешние компоненты — это стандартная и очень эффективная практика в мире «1С:Предприятия». Понимание принципов работы с памятью, кодировок и использование современных инструментов разработки позволит вам создавать быстрые и надежные решения для бизнеса.

← На главную