При обработке текстовых файлов больших объемов (например, тяжелых CSV-выгрузок или XML-логов) разработчики часто сталкиваются со снижением производительности. Классический сценарий, когда файл целиком считывается в одну строковую переменную, а затем разбирается посимвольно с помощью функции Сред() в цикле, показывает крайне низкую скорость работы. В этой статье мы подробно разберем, как устроена работа со строками в платформе «1С:Предприятие 8» на низком уровне, выясним истинные причины медленной работы посимвольного обхода и рассмотрим эффективные методы оптимизации.
Существует распространенное заблуждение, что при вызове функции Сред(Строка, Поз, 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) платформа не просто обращается к ячейке памяти, она создает абсолютно новый строковый объект в системной куче (heap), копирует туда один символ и возвращает его. На миллионах итераций цикла это приводит к колоссальным затратам ресурсов на выделение памяти, копирование данных и последующую работу сборщика мусора.Символ = Сред(...) создает огромный оверхед на трансляцию кода, по сравнению с оптимизированными низкоуровневыми функциями платформы, написанными на C++.Основное правило работы с большими файлами — никогда не загружайте файл целиком в одну строковую переменную. Это приводит к чрезмерному потреблению оперативной памяти и падению производительности. Вместо этого используем объект ЧтениеТекста, который позволяет читать данные порциями (построчно).
Рассмотрим пример правильного построчного чтения файла:
ПутьКФайлу = "C:\temp\huge_data.csv";
ФайлЧтения = Новый ЧтениеТекста(ПутьКФайлу, КодировкаТекста.UTF8);
// Читаем файл построчно, не перегружая память
ТекущаяСтрока = ФайлЧтения.ПрочитатьСтроку();
Пока ТекущаяСтрока <> Неопределено Цикл
// Обрабатываем отдельную строку
ОбработатьСтроку(ТекущаяСтрока);
ТекущаяСтрока = ФайлЧтения.ПрочитатьСтроку();
КонецЦикла;
ФайлЧтения.Закрыть();
Если вам необходимо разбить строку на подстроки по определенному разделителю (например, строки CSV по запятой или переводы строк в файле), забудьте о ручном посимвольном поиске. Платформа предоставляет высокопроизводительную функцию СтрРазделить(), которая выполняется на уровне скомпилированного кода C++ и работает в сотни раз быстрее ручных циклов.
Посмотрим на пример использования:
// Исходная строка с разделителями
СтрокаДанных = "Иванов;Иван;Петрович;35;Москва";
// Быстрое разбиение в массив за один вызов
МассивЭлементов = СтрРазделить(СтрокаДанных, ";", Ложь);
Для Каждого Элемент Из МассивЭлементов Цикл
Сообщить(Элемент);
КонецЦикла;
Часто в алгоритмах разбора строк разработчики собирают итоговый результат обратно с помощью конкатенации: Итог = Итог + Символ. Из-за неизменяемости строк эта операция имеет квадратичную сложность $O(N^2)$. При увеличении объема данных в 10 раз время выполнения вырастет в 100 раз!
Чтобы избежать этого, используйте Массив в качестве буфера (аналог класса StringBuilder в других языках), а в конце объединяйте его функцией СтрСоединить(). Рассмотрим разницу:
// ПЛОХОЙ ВАРИАНТ (Квадратичная сложность):
Результат = "";
Для Шаг = 1 По 100000 Цикл
Результат = Результат + "А"; // Постоянное перевыделение памяти!
КонецЦикла;
// ХОРОШИЙ ВАРИАНТ (Линейная сложность):
Буфер = Новый Массив;
Для Шаг = 1 По 100000 Цикл
Буфер.Добавить("А"); // Быстрое добавление в массив
КонецЦикла;
Результат = СтрСоединить(Буфер, ""); // Мгновенная сборка одной операцией
Ниже представлен оптимизированный вариант функции для разбора CSV-данных. Алгоритм учитывает возможность нахождения разделителей и переносов строк внутри кавычек. В кодовой базе исправлены медленные конструкции и оптимизирована логика проверки кавычек:
&НаКлиенте
Функция ПрочитатьCSV(Знач ТекстовыйДокумент, Разделитель, КоличествоКолонок) Экспорт
Результат = Новый Массив; // Массив строк (каждая строка - массив колонок)
ВнутриКавычек = Ложь;
НачалоПодстроки = 1;
НомерКолонки = 0;
СтрокаФайла = Новый Массив;
ТекущееЗначение = "";
ВсегоСтрок = ТекстовыйДокумент.КоличествоСтрок();
Для НомерСтроки = 1 ПО ВсегоСтрок Цикл
// Имитируем перевод строки в конце, чтобы корректно обработать последний элемент
ТекСтрока = ТекстовыйДокумент.ПолучитьСтроку(НомерСтроки) + Символы.ПС;
ДлинаСтроки = СтрДлина(ТекСтрока);
Для Позиция = 1 По ДлинаСтроки Цикл
Символ = Сред(ТекСтрока, Позиция, 1);
// Если встречаем кавычку, инвертируем флаг экранирования
Если Символ = """" Тогда
ВнутриКавычек = НЕ ВнутриКавычек;
Продолжить;
КонецЕсли;
// Если встречается разделитель или перенос строки вне кавычек
Если (Символ = Разделитель Или Символ = Символы.ПС) И НЕ ВнутриКавычек Тогда
КонецПодстроки = Позиция;
НомерКолонки = НомерКолонки + 1;
// Извлекаем текущее значение колонки
ТекущееЗначение = СокрЛП(Сред(ТекСтрока, НачалоПодстроки, КонецПодстроки - НачалоПодстроки));
// Очищаем от обрамляющих кавычек, если они есть
Если Лев(ТекущееЗначение, 1) = """" Тогда
ТекущееЗначение = Сред(ТекущееЗначение, 2);
КонецЕсли;
Если Прав(ТекущееЗначение, 1) = """" Тогда
ТекущееЗначение = Сред(ТекущееЗначение, 1, СтрДлина(ТекущееЗначение) - 1);
КонецЕсли;
СтрокаФайла.Добавить(ТекущееЗначение);
// Если строка полностью заполнена или достигнут конец строки файла
Если НомерКолонки = КоличествоКолонок Или Символ = Символы.ПС Тогда
Результат.Добавить(СтрокаФайла);
СтрокаФайла = Новый Массив; // Создаем новый массив для следующей строки
НомерКолонки = 0;
КонецЕсли;
ТекущееЗначение = "";
НачалоПодстроки = Позиция + 1;
КонецЕсли;
КонецЦикла;
ТекущееЗначение = ТекущееЗначение + Сред(ТекСтрока, НачалоПодстроки);
НачалоПодстроки = 1;
КонецЦикла;
Возврат Результат;
КонецФункции
Подведем итоги: Поиск символа по индексу в 1С работает быстро и имеет константную сложность, однако посимвольный обход огромных строк "в лоб" неэффективен из-за накладных расходов на создание новых объектов в памяти. Применяя построчное чтение с помощью ЧтениеТекста, заменяя ручной поиск системной функцией СтрРазделить и оптимизируя алгоритмы сборки строк, вы добьетесь ускорения работы ваших обработок в десятки раз.