Имя: Пароль:
IT
 
Эффективный способ конкатенации строк в 1С
0 dimzon
 
10.07.07
14:28
Корни данного поста растут отсюда: Простенькая задачка по производительности

На самом деле конкатенация строк является достаточно дорогой с точки зрения производительности операцией. Всем сомневающимся рекомендую замерить время выполнения данного кода:

  строка = "";
  Для сч = 0 по 333333 цикл
      строка = строка + "abcd";
  КонецЦикла;

Причина столь медленного выполнения заключается в особенности поведения строк. Конкатенация строк в цикле ЕСТЬ ЗЛО!

Итак, что происходит при выполнении операции S=S+"Some Text"

Происходит т.н. ReAlloc (перевыделение памяти):
 a) Выделяется область памяти размером = размер(S) + размер("Some Text")
 b) В выделенную область копируется S
 c) В выделенную область дописывается "Some Text"
 d) Адрес дескриптора строки S устанавливается на новую область
 e) Старая область памяти, использовавшаяся S освобождается

На небольших строках/циклах потери производительности незаметны, но на больших циклах (десятки тысяч) и/или на больших объёмах (десятки мегабайт) подобный код может служить серьёзным bottleneck-ом...


Для ускорения данного кода необходимо уменьшить количество realloc-ов. В "нормальных" языках для решения подобной проблемы есть специальные механизмы навроде StringBuilder-а в .NET, StringBuffer-а в Java...
В С++ стандартный stl::string также имеет специальный метод append, реализующий логику аналогичную StringBuilder-у из .NET

В других языках можно придумать специальные трюки для минимизации ReAlloc-ов и ускорения данного кода. Например в VBScript/VB/JavaScript можно использовать конструкцию Join:

  Option Explicit
  Dim i,arr, s: s=""
  ReDim arr(333333)
  For i=0 to 333333
     arr(i)="abcd"
  Next
  s = Join(arr,"")


К сожалению для 1С подобных трюков с использованием "чистого 1С" найти  не удалось... Тем не менее ускорить конкатенацию можно используя COM-объект ADODB.Stream. Вот пример кода:

  str = Новый COMОбъект("ADODB.Stream");
  str.Open();
  Для i=0 По 333333 Цикл
       str.WriteText("abcd",0);
  КонецЦикла;
  str.Position = 0;
  строка = str.readText();

Как можно убедиться разница во времени выполнения между первым вариантом (сложение в цикле) и последним (с использованием Stream) просто ошеломляющая...
1 END
 
10.07.07
14:44
Познавательно, спасиб.
2 selenat
 
10.07.07
14:49
(0) Респект!
3 Злопчинский
 
12.07.07
03:58
+1!
4 dimzon
 
12.07.07
04:08
+(0) если лень мерить я приведу свои цифры, можете им верить
в первом варианте (конкатенация в цикле) - 2500 секунд
во втором варианте (vbscript, использование join) - 0.5 секунды
в третьем варианте (использование ADODB.Stream) - 1.5 секунды

первый вариант проверялся и в 1C v8.1 и в 1C v7.7 и в VBScript и даже в .NET, результаты практически одинаковые - около 2500 секунд ;)
5 dimzon
 
12.07.07
04:24
+(0) ещё хочу добавить что с увеличением числа итераций время растёт нелинейно.
посудите сами - на каждой итерации размер строки которая копируется на шаге b) увеличивается, соответсвенно увеличивается и время необходимое на выполнение шага b)
6 Андрюха
 
12.07.07
05:04
(0) Познавательно, интересно, однако малоприменимо на практике, ибо ни разу не приходилось пользоваться такой странной конкатенацией да еще в цикле от 0 до 333333 :)
7 VladZ
 
12.07.07
05:06
(6) +1
8 orefkov
 
12.07.07
08:53
(0)
Ты проводил исследования, насколько эта проблемя является "бутылочным горлышком" уже существующих решений? У тебя есть масса трасс замера производительности при реальной работе, указывающей на затык из-за этой проблемы?
Решить высосаную из пальца проблему и рекомендовать ее в БЗ - феерично!
9 АЛьФ
 
12.07.07
09:14
2(8) +1
От себя: 1С - это в первую очередь база данных и основные тормоза при обращении именно к базе. Именно это место и стоит оптимизировать. Остальное от лукавого, т.к. любой вычислительный алгоритм можно вынести в ВК и там хоть обоптимизироваться.
Автору рекомендую заглянуть сюда, чтобы побольше узнать о том как работает движок 1С: http://www.1cpp.ru/wiki/?wakka=HomePage
10 dimzon
 
12.07.07
13:31
(6)(7)
Ну насчёт "На практике" - данный цикл это реальный код: v8: У кого нибудь есть готовый вариант кодирования/декодирования BASE64?

кроме того возможны несколько другие варианты - например цикл более короткий да строки длинные... вот например в этой ветке во всех "тормозах" Выгрузить ТЗ в текстовый файл. скорее всего виновата именно конкатенация...

основная проблема в том что многие её не осознают и пишут код не думая... вот например ещё код: v8: Чем заменен метод ВСписокСРазделителями? вопрос на засыпку - что случится если коллекция будет достаточно большая...

(8)(9) - уважаемые, то что я не 1С-нег не означает что я "тупица"... да и 1С тут не причём, работа со практически везде одинакова. естественно я знаю что говорю, естественно я НЕОДНОКРАТНО находил и расшивал подобные bottleneck-и...
11 Господин ПЖ
 
12.07.07
13:42
(10) По поводу Выгрузить ТЗ в текстовый файл..

1. Сама постановка задачи убогая.
2. Проблема не в конкатенации, а в работе 1С с файлами, что давно решается через fso.
12 dimzon
 
12.07.07
13:49
(11) На самом деле не факт. Ибо возможны 2 различных способа реализации
1. Сначала набирается строка которая потом одним махом пишется в файл. Тогда проблема как раз та
2. Пишется в файл по кусочкам. Тогда проблема с буферизацией

НО, косвенно судя по "отъедаемой" памяти Выгрузить ТЗ в текстовый файл. рискну предположить что реализация именно 1
13 Nordok
 
12.07.07
13:52
(10) Я тебе писал цикл, который выполняется за 2 сек., чем он плох ?
14 Андрюха
 
12.07.07
13:55
(10) Никто и не говорит, что ты тупица, просто долбишь немножко не в том направлении.
15 dimzon
 
12.07.07
14:23
(13) Он неплох { Простенькая задачка по производительности } , но, видишь ли, дело даже не совсем в цикле...
как бы объяснить попонятнее... вообще на практике частенько встречается когда объявляется строковая переменная которая потом по ссылке передаётся в различные процедуры и там к ней прибавляется...

кроме того в твоём методе ускорение заключается не в уменьшении количества ReAlloc-ов а в уменьшении объёмов, копируемых в b)


Не знаю как в 1С но в наших проектах я неоднократно встечал подобные проблемы при формировании CSV, XML, HTML. Ещё помнится был код который делал SELECT а потом из результатов генерировал SQL-Batch c INSERT-ами, там тоже было...
16 Nordok
 
12.07.07
14:51
(15) Я просто все не осилил, написано оптимизировать код, я и оптимизировал :)
17 dimzon
 
12.07.07
15:25
кто-нить может на 7.7 замерить время выполнения такого кода:
    Ч = 0;
    М = 0;
    С = 0;
    ТекущееВремя(Ч,М,С);
    Сп = СоздатьОбъект("СписокЗначений");
    Для А=1 По 333333 Цикл
         Сп.ДобавитьЗначение("abcd");
    КонецЦикла;    
    Стр = Сп.ВСтрокуСРазделителями();
    Ч1 = 0;
    М1 = 0;
    С1 = 0;
    ТекущееВремя(Ч1,М1,С1);
    Х = (Ч*60+М)*60+С;
    Х1 = (Ч1*60+М1)*60+С1;
    Сообщить(" Время вып. = "+Строка(Х1-Х));

есть подозрение что реализация ВСтрокуСРазделителями не далеко ушла от { v8: Чем заменен метод ВСписокСРазделителями? }

Заранее спасибо!
18 asady
 
12.07.07
15:51
(17)
 тД1=ТекущаяДата();
   т = Новый  СписокЗначений;
   Для А=1 По 333333 Цикл
        т.Добавить("abcd");
   КонецЦикла;    
   Стр = Строка(т);
   
   тД2=ТекущаяДата();
   Сообщить(" Время вып. = "+(тД2-тД1));

время выыполнения на нашем перегруженном терминале 9 сек.
19 asady
 
12.07.07
16:00
(17)
вариант нордока


   с1="";
   с="";
Для сч = 0 по 340 цикл
   Для Сч1 = 0 По 1000 Цикл
        с1 = с1 + "abcd";
     КонецЦикла;
  с=с+с1; с1="";  
  КонецЦикла;
   тД2=ТекущаяДата();
   Сообщить(" Время вып. = "+(тД2-тД1));

дает у меня 6 секунд.
20 asady
 
12.07.07
16:04
(17)
Вариант через ADO стрим

   тД1=ТекущаяДата();
str = Новый COMОбъект("ADODB.Stream");
 str.Open();
 Для i=0 По 333333 Цикл
      str.WriteText("abcd",0);
 КонецЦикла;
 str.Position = 0;
 строка = str.readText();
   тД2=ТекущаяДата();
   Сообщить(" Время вып. = "+(тД2-тД1));


дает у меня 9 секунд!!!!
21 dimzon
 
12.07.07
16:12
(18) результирующая строка неправильная (посмотри длину, должна быть больше мегабайта)
22 asady
 
12.07.07
16:23
(21) да (18) работает некорректно длина всего 10

зато (19) и (20) работают правильно длина больше мегабайта.
23 dimzon
 
12.07.07
16:23
(19)(20)
похоже на правду (у меня подобные пропорции для VBScript-а получились)
24 Nordok
 
12.07.07
16:26
думаю (19) можно заставить работать еще быстрее
25 dimzon
 
12.07.07
16:29
(24) Возможно, если поиграться с верхними границами массивов...
26 dimzon
 
12.07.07
16:46
(24)(25)

вот такая зависимость:
2000*170 ~ 61
170*2000 ~ 15
340*1000 ~ 15
34*10000 ~ 20
17*20000 ~ 20
10*33333 ~ 54
8*40000 ~ 98

к сожалению данный подход не всегда поможет... например при экспорте таблички в CSV или при построении html-отчёта - во внешнем цикле будут строки, во внутреннем - столбцы...
27 Nordok
 
12.07.07
17:03
например при экспорте таблички в CSV или при построении html-отчёта - во внешнем цикле будут строки, во внутреннем - столбцы... На вскидку не совсем поянл, почему не походят, циклом мы всего лишь регулируем порции конкатенации.

И вот он лидер, у меня 1 сек. (может ошибся?)
Для к = 0 По 70 цикл  
   Для к1 = 0 По 70 цикл  
       Для сч = 0 По 70 цикл с = с + "abcd";    КонецЦикла;
   с1 = с1 + с;
   С="";
   КонецЦикла;
с2 = с2 + с1;
с1 = "";
КонецЦикла;
28 asady
 
12.07.07
17:05
(0) ИМХО автору стоит переименовать ветку.
29 dimzon
 
12.07.07
17:13
(28) и как переименовать?
30 dimzon
 
12.07.07
17:15
(27) Ну вот  смотри, есть табличка в ней nRow строк и nCol столбцов... Напиши эффективный код формирования CSV ;)
31 Nordok
 
12.07.07
17:52
(30)
Процедура Сформировать()
Строк = 500;
Столбцов = 500;
Тз = СоздатьОбъект("ТаблицаЗначений");
Сообщить("Генерация колонок...");
Для Ном = 1 По Столбцов Цикл
   Тз.НоваяКолонка("Кол"+Ном);    
КонецЦикла;

Сообщить("Заполнение...");
Для Ном = 1 По Строк Цикл
   Тз.НоваяСтрока();    
   Для Ном1 = 1 По Столбцов Цикл
       Тз.УстановитьЗначение(Ном, Ном1,"ф");
       Состояние(""+Ном+":"+Ном1);
   КонецЦикла;
КонецЦикла;
Сообщить("Формирование CSV");

Буфер1="";
Буфер2="";
ЦСВ="";

Сообщить(ТекущееВремя());
Сч=0;
Для Ном = 1 По Строк Цикл
   Тз.НоваяСтрока();    
   Для Ном1 = 1 По Столбцов Цикл
       Зн=Тз.ПолучитьЗначение(Ном, Ном1);
       Буфер1=Буфер1+Зн+", ";
       Если Сч=1000 Тогда
           Сч=0;
           ЦСВ=ЦСВ+Буфер1;    
       Буфер1="";
       КонецЕсли;
       
       Сч=Сч+1;
       Состояние(""+Ном+":"+Ном1);
   КонецЦикла;
КонецЦикла;
Сообщить(СтрДлина(ЦСВ));
Сообщить(ТекущееВремя());
Сообщить(Лев(ЦСВ,30));


КонецПроцедуры
                       
//Процедура Базовый() //2м46сек
//Сообщить(ТекущееВремя());
//Для Ном = 1 По Строк Цикл
//    Тз.НоваяСтрока();    
//    Для Ном1 = 1 По Столбцов Цикл
//        Зн=Тз.ПолучитьЗначение(Ном, Ном1);
//        ЦСВ=ЦСВ+Зн;
//        Состояние(""+Ном+":"+Ном1);
//    КонецЦикла;
//КонецЦикла;
//Сообщить(СтрДлина(ЦСВ));
//Сообщить(ТекущееВремя());
//КонецПроцедуры


Базовый вариант: 2 минуты 46 сек.
Улучшенный: 17 сек.

Улучшенный правда немного косячит, в конце, но это мелочь... И конечно же чем больше данных, тем больше разница, и тем больше понадобится разбивки для поддержания быстродействия.
32 dimzon
 
12.07.07
18:00
(31) у меня 1С нет, можешь то-же самое на Stream померять?
спасибо !
33 dimzon
 
12.07.07
18:03
кстати, а кто-нить знает как можно дополнить и подредактировать уже отправленное сообщение на этом форумном движке? или никак?
34 dimzon
 
12.07.07
19:53
(32) вопрос снят, похоже действительно быстрее чем Stream даже при увеличенном в 10 раз объёме, поздравляю ;)
блин, жалко в классик обернуть этот код нельзя... хотя можно наверно сделать структурку содержащую в качестве полей Буфер1, ЦСВ и Сч плюс процедуру добавления ДобавитьСтроку(структурка, строка) плюс функцию получения строки ПолучитьРезультат(структурка)...
35 PR
 
12.07.07
20:47
(8) +1
LOL
36 dimzon
 
12.07.07
20:53
(35)
читай (10) и (15)
37 Torquader
 
13.07.07
17:26
Вопрос мной просматривался при сборке TXT файла.
Выяснилось, что
t=CreateObject("Text");
t.AddLine("abcd");
работает прекрасно.
При этом для решения кодирования base 64 вполне подойдёт.
Также есть мнение, что строки меньше 256 символов работают без выделения памяти, но копирования всё равно не избежать.
38 Эльниньо
 
13.07.07
18:15
Процедура Сформировать()
   Сп = СоздатьОбъект("СписокЗначений");
   Б = "";
   А = "Аждлэ лдэждэ жд эж дэждэждэждэ ждэжд эжд эждоьлдьт лщтжлдо тджл от джло т джло т ждлотждлотджлотдлотдлотлдЯ";
   Н = _GetPerformanceCounter();
   Для х = 1 По 10000 Цикл
       //Б = Б + А;
       Сп.ДобавитьЗначение(А);
   КонецЦикла;
   Б = ЗначениеВСтроку(Сп);
   Форма.Обновить();
   Б = СтрЗаменить(Б, СокрП(СтрЗам1), "");
   Б = СтрЗаменить(Б, СокрП(СтрЗам2), "");
   Сообщить("Секунд=" + ((_GetPerformanceCounter() - Н) / 1000));
КонецПроцедуры

Результат: 7 сек против 19
39 Эльниньо
 
13.07.07
18:18
+(38) Про значения СтрЗам1 и СтрЗам2 не говорю, несложно догадаться.
40 Torquader
 
13.07.07
20:33
Вообще, проблема намного глубже 1С.
Я давно говорил, что, например, в Си++ есть операторы new и delete, но нет оператора redim, который позволял бы увеличить длину массива.
Поэтому и получаем - как в глубине, так и наруже.
41 Ковычки
 
14.07.07
09:42
Фигня какая-то...(с)
...
Есть метод быстрее всех этих...
42 dimzon
 
14.07.07
13:17
(37) а что, для строк меньше 256 символов память не нужна? ;)

(40) уважаемый, в С есть функция realloc это и есть redim... работает именно так как описано в (0)

(38) задача не просто получить большую строку из abcd (это только пример)... для примеров из реальной жизни, например (10) и (15) данный способ не годится...

(37) по поводу объекта "Text" - посмотрю подробнее (первый раз такой объект вижу)
43 Torquader
 
16.07.07
19:03
В справке 1С есть работа с текстом, который можно прочитать из текстового файла. (По русски вроде бы Текст).
Замечено, что добавление строки в текст работает быстрее.
Причём, анализ показал, что строка
s=s+a
работает медленно из-за того, что сначала создаётся строка в которую копируется s, потом копируется a, а потом результат снова копируется в s, то есть проблема не в выделении памяти, а в выполнении операции memcpy несколько лишних раз.
Для команды s+=a выполняется только одно копирование строки а, когда строка маленькая, и два копирования s и а, когда идёт выделение памяти (и блок в памяти переезжает с места на место).
Вота.
Есть два вида языков, одни постоянно ругают, а вторыми никто не пользуется.