Как оптимизировать посимвольный обход и выделение символов в длинных строках 1С?

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

При обработке текстовых файлов больших объемов (например, тяжелых CSV-выгрузок или XML-логов) разработчики часто сталкиваются со снижением производительности. Классический сценарий, когда файл целиком считывается в одну строковую переменную, а затем разбирается посимвольно с помощью функции Сред() в цикле, показывает крайне низкую скорость работы. В этой статье мы подробно разберем, как устроена работа со строками в платформе «1С:Предприятие 8» на низком уровне, выясним истинные причины медленной работы посимвольного обхода и рассмотрим эффективные методы оптимизации.

Внутреннее устройство строк в платформе 1С

Существует распространенное заблуждение, что при вызове функции Сред(Строка, Поз, 1) платформа каждый раз выполняет последовательный поиск нужного символа с самого начала строки, что приводит к линейной сложности $O(N)$ для каждого обращения. Давайте разберем, почему это не так.

Начиная с версии платформы 8.3.11, в процессе перехода на стандарт C++17 внутреннее представление строк было модернизировано. Платформа ушла от использования системно-зависимого типа wchar_t и сторонних библиотек. В современной архитектуре 1С строки хранятся в оперативной памяти как непрерывный массив двухбайтовых элементов типа char16_t (кодировка UTF-16).

Поскольку строка в памяти — это упорядоченный массив фиксированной ширины (по 2 байта на символ для базовой многоязыковой плоскости), взятие символа по его индексу (позиции) на уровне платформы выполняется за константное время $O(1)$. Адрес нужного элемента в памяти вычисляется мгновенно по формуле смещения. Никакого перебора символов от начала строки не происходит.

Однако здесь кроется важный нюанс, связанный с кодировкой UTF-16. Символы, выходящие за пределы базовой плоскости (например, сложные иероглифы или эмодзи), кодируются четырьмя байтами — так называемыми суррогатными парами. На уровне встроенного языка 1С такие символы воспринимаются как два отдельных элемента. Проанализируем это на примере:


// Символ эмодзи состоит из суррогатной пары
СтрокаЭмодзи = Символ(55357) + Символ(56832); 

Длина = СтрДлина(СтрокаЭмодзи); // Платформа вернет 2
ПерваяПоловина = Сред(СтрокаЭмодзи, 1, 1); // Вернет некорректный "битый" символ
ЦелыйСимвол = Сред(СтрокаЭмодзи, 1, 2); // Вернет полноценный символ эмодзи

Почему посимвольный обход работает медленно?

Если поиск символа по индексу выполняется за $O(1)$, почему алгоритмы посимвольного разбора огромных строк работают медленно? Выделим две главные причины:

  1. Неизменяемость строк (Immutability): Строки во встроенном языке 1С неизменяемы. При каждом вызове функции Сред(Строка, Поз, 1) платформа не просто обращается к ячейке памяти, она создает абсолютно новый строковый объект в системной куче (heap), копирует туда один символ и возвращает его. На миллионах итераций цикла это приводит к колоссальным затратам ресурсов на выделение памяти, копирование данных и последующую работу сборщика мусора.
  2. Накладные расходы интерпретатора: Выполнение любого цикла на встроенном языке проходит через виртуальную машину (интерпретатор) 1С. Выполнение миллионов итераций вида Символ = Сред(...) создает огромный оверхед на трансляцию кода, по сравнению с оптимизированными низкоуровневыми функциями платформы, написанными на C++.

Решение 1. Построчное чтение вместо загрузки всего файла

Основное правило работы с большими файлами — никогда не загружайте файл целиком в одну строковую переменную. Это приводит к чрезмерному потреблению оперативной памяти и падению производительности. Вместо этого используем объект ЧтениеТекста, который позволяет читать данные порциями (построчно).

Рассмотрим пример правильного построчного чтения файла:


ПутьКФайлу = "C:\temp\huge_data.csv";
ФайлЧтения = Новый ЧтениеТекста(ПутьКФайлу, КодировкаТекста.UTF8);

// Читаем файл построчно, не перегружая память
ТекущаяСтрока = ФайлЧтения.ПрочитатьСтроку();
Пока ТекущаяСтрока <> Неопределено Цикл
    // Обрабатываем отдельную строку
    ОбработатьСтроку(ТекущаяСтрока);
    ТекущаяСтрока = ФайлЧтения.ПрочитатьСтроку();
КонецЦикла;

ФайлЧтения.Закрыть();

Решение 2. Быстрое разделение строк с помощью СтрРазделить()

Если вам необходимо разбить строку на подстроки по определенному разделителю (например, строки CSV по запятой или переводы строк в файле), забудьте о ручном посимвольном поиске. Платформа предоставляет высокопроизводительную функцию СтрРазделить(), которая выполняется на уровне скомпилированного кода C++ и работает в сотни раз быстрее ручных циклов.

Посмотрим на пример использования:


// Исходная строка с разделителями
СтрокаДанных = "Иванов;Иван;Петрович;35;Москва";

// Быстрое разбиение в массив за один вызов
МассивЭлементов = СтрРазделить(СтрокаДанных, ";", Ложь);

Для Каждого Элемент Из МассивЭлементов Цикл
    Сообщить(Элемент);
КонецЦикла;

Решение 3. Избавление от квадратичной сложности при сборке строк

Часто в алгоритмах разбора строк разработчики собирают итоговый результат обратно с помощью конкатенации: Итог = Итог + Символ. Из-за неизменяемости строк эта операция имеет квадратичную сложность $O(N^2)$. При увеличении объема данных в 10 раз время выполнения вырастет в 100 раз!

Чтобы избежать этого, используйте Массив в качестве буфера (аналог класса StringBuilder в других языках), а в конце объединяйте его функцией СтрСоединить(). Рассмотрим разницу:


// ПЛОХОЙ ВАРИАНТ (Квадратичная сложность):
Результат = "";
Для Шаг = 1 По 100000 Цикл
    Результат = Результат + "А"; // Постоянное перевыделение памяти!
КонецЦикла;

// ХОРОШИЙ ВАРИАНТ (Линейная сложность):
Буфер = Новый Массив;
Для Шаг = 1 По 100000 Цикл
    Буфер.Добавить("А"); // Быстрое добавление в массив
КонецЦикла;
Результат = СтрСоединить(Буфер, ""); // Мгновенная сборка одной операцией

Практический кейс: Оптимизированный разбор CSV

Ниже представлен оптимизированный вариант функции для разбора CSV-данных. Алгоритм учитывает возможность нахождения разделителей и переносов строк внутри кавычек. В кодовой базе исправлены медленные конструкции и оптимизирована логика проверки кавычек:


&НаКлиенте
Функция ПрочитатьCSV(Знач ТекстовыйДокумент, Разделитель, КоличествоКолонок) Экспорт
    
    Результат = Новый Массив; // Массив строк (каждая строка - массив колонок)
    
    ВнутриКавычек = Ложь;
    НачалоПодстроки = 1;
    НомерКолонки = 0;
    СтрокаФайла = Новый Массив;
    ТекущееЗначение = "";
    
    ВсегоСтрок = ТекстовыйДокумент.КоличествоСтрок();
    Для НомерСтроки = 1 ПО ВсегоСтрок Цикл
        
        // Имитируем перевод строки в конце, чтобы корректно обработать последний элемент
        ТекСтрока = ТекстовыйДокумент.ПолучитьСтроку(НомерСтроки) + Символы.ПС; 
        ДлинаСтроки = СтрДлина(ТекСтрока);
        
        Для Позиция = 1 По ДлинаСтроки Цикл
            
            Символ = Сред(ТекСтрока, Позиция, 1);
            
            // Если встречаем кавычку, инвертируем флаг экранирования
            Если Символ = """" Тогда
                ВнутриКавычек = НЕ ВнутриКавычек;
                Продолжить;
            КонецЕсли;
            
            // Если встречается разделитель или перенос строки вне кавычек
            Если (Символ = Разделитель Или Символ = Символы.ПС) И НЕ ВнутриКавычек Тогда
                
                КонецПодстроки = Позиция;
                НомерКолонки = НомерКолонки + 1;
                
                // Извлекаем текущее значение колонки
                ТекущееЗначение = СокрЛП(Сред(ТекСтрока, НачалоПодстроки, КонецПодстроки - НачалоПодстроки));
                
                // Очищаем от обрамляющих кавычек, если они есть
                Если Лев(ТекущееЗначение, 1) = """" Тогда
                    ТекущееЗначение = Сред(ТекущееЗначение, 2);
                КонецЕсли;
                Если Прав(ТекущееЗначение, 1) = """" Тогда
                    ТекущееЗначение = Сред(ТекущееЗначение, 1, СтрДлина(ТекущееЗначение) - 1);
                КонецЕсли;
                
                СтрокаФайла.Добавить(ТекущееЗначение);
                
                // Если строка полностью заполнена или достигнут конец строки файла
                Если НомерКолонки = КоличествоКолонок Или Символ = Символы.ПС Тогда
                    Результат.Добавить(СтрокаФайла);
                    СтрокаФайла = Новый Массив; // Создаем новый массив для следующей строки
                    НомерКолонки = 0;
                КонецЕсли;
                
                ТекущееЗначение = ""; 
                НачалоПодстроки = Позиция + 1;
                
            КонецЕсли;
        КонецЦикла;
        
        ТекущееЗначение = ТекущееЗначение + Сред(ТекСтрока, НачалоПодстроки); 
        НачалоПодстроки = 1; 
    КонецЦикла;
    
    Возврат Результат;
    
КонецФункции

Подведем итоги: Поиск символа по индексу в 1С работает быстро и имеет константную сложность, однако посимвольный обход огромных строк "в лоб" неэффективен из-за накладных расходов на создание новых объектов в памяти. Применяя построчное чтение с помощью ЧтениеТекста, заменяя ручной поиск системной функцией СтрРазделить и оптимизируя алгоритмы сборки строк, вы добьетесь ускорения работы ваших обработок в десятки раз.

← На главную