Какой цикл быстрее? Тестируем 1С

Занимаюсь программированием 1С уже несколько лет, и тут посетила мысль — «А не пройти ли какой-нибудь обучающий курс, вдруг в знаниях есть какие-то пробелы, о которых раньше даже и не подозревал»? Сказано-сделано. Сижу, слушаю курс, дохожу до циклических операторов и тут вторая мысль (да, не часто они у меня появляются)«А какой цикл быстрее»? Надо бы проверить.
Итак, я нашел пять способов, как можно организовать цикл средствами 1С.

Первый вид цикла, назовем его условно «ДляПо» выглядит так:

Для н = 0 по КоличествоИтераций Цикл 
	КакиеТоДействия();
КонецЦикла;

Второй вид «ДляКаждого»:

Для Каждого ЭлементКоллекции из Коллекция Цикл 
	КакиеТоДействия();
КонецЦикла;

Третий «Пока»:

Пока н <> КоличествоИтераций Цикл 
	КакиеТоДействия();
	н = н + 1;
КонецЦикла;

Далее вспомнил ассемблерную молодость — цикл «Если»:

~НачалоЦикла:
Если н <> КоличествоИтераций Тогда 
	КакиеТоДействия();
	н = н + 1;
	Перейти ~НачалоЦикла;
КонецЕсли;

Ну и напоследок «Рекурсия»

Процедура РекурсивныйЦикл(н, КоличествоИтераций)
	КакиеТоДействия();
	Если н <> КоличествоИтераций Тогда 
		РекурсивныйЦикл(н+1, КоличествоИтераций);
	КонецЕсли;
КонецПроцедуры

Естественно, что относить рекурсию к циклам не совсем корректно, но тем ни менее с её помощью можно добиться похожих результатов. Сразу оговорюсь, что в дальнейшем тестировании рекурсия не участвовала. Во первых все тесты проводились при 1 000 000 итераций, а рекурсия выпадает уже при 2 000. Во вторых скорость рекурсии в десятки раз меньше, чем скорость остальных циклов.

Последнее отступление. Одним из условий было выполнение в цикле каких-либо действий. Во первых пустой цикл используется очень редко. Во вторых цикл «ДляКаждого» используется для какой-либо коллекции, а значит и остальные циклы должны работать с коллекцией, чтобы тестирование проходило в одинаковых условиях.

Ну что ж, поехали. В качестве тела цикла использовалось чтение из заранее заполненного массива.

ПриемникТестовогоЗначения = ТестовыйМассив.Получить(н);

или, при использовании цикла «ДляКаждого»

ПриемникТестовогоЗначения = Элем;


Тестирование проводилось на платформе 8.3.5.1231 для трех видов интерфейса (Обычное приложение, Управляемое приложение и Такси).
Результаты для 8.3.5.1231
Интерфейс ДляПо ДляКаждого Пока Если
Обычное приложение 5734,2 4680,4 7235,4 7263,0
Управляемое приложение 5962,4 4882,6 7497,4 7553,6
Такси 5937,2 4854,6 7500,8 7513,0
Числа это время в миллисекундах полученное с помощью функции ТекущаяУниверсальнаяДатаВМиллисекундах(), которую я вызывал до цикла и после его завершения. Числа дробные, потому что я использовал среднее арифметическое пяти замеров. Почему я не использовал Замер производительности? У меня не было цели замерить скорость каждой строчки кода, только скорость циклов с одинаковым результатом работы.

Казалось бы и все, но — тестировать так тестировать!
Результат для платформы 8.2.19.106
Результаты для 8.2.19.106
Интерфейс ДляПо ДляКаждого Пока Если
Обычное приложение 4411,8 3497,2 5432,0 5454,0
Управляемое приложение 4470,8 3584,8 5522,6 5541,0
В среднем платформа 8.2 на 25% быстрее, чем 8.3. Я немножко не ожидал такой разницы и решил провести тест на другой машине. Результаты приводить не буду, в можете сами нагенерировать их с помощью вот этой конфигурации. Скажу только, что там 8.2 была быстрее процентов на 20.

Почему? Не знаю, дезасемблировать ядро в мои планы не входило, но в замер производительности я все же заглянул. Оказалось, что сами циклические операции в 8.3 проходят несколько быстрее, чем в 8.2. Но на строке
ПриемникТестовогоЗначения = ТестовыйМассив.Получить(н);

то есть при считывании элемента коллекции в переменную происходит значительное снижение производительность.

В итоге:
Процентное соотношение скорости циклов
ДляПо ДляКаждого Пока Если
78,97% 64,57% 99,57% 100,00%
К чему всё это? Для себя я сделал несколько выводов:

1. Если есть возможность использовать специализированный цикл — «ДляКаждого», то лучше использовать его. Кстати, сам по себе он отрабатывает дольше чем другие циклы, но скорость доступа к элементу коллекции у него на много выше.
2. Если заранее знаешь количество итераций — используй «ДляПо». «Пока» отработает медленнее.
3. Если использовать цикл «Если» — другие программисты тебя явно не поймут.
Share post

Similar posts

Comments 35

    +3
    Я в молодости тоже грешил тестами производительности на 1С, даже показывал их менеджерам. Потом перестал заниматься фигней, точнее противоестественным способом реализовывать свои желания и теперь пользуюсь профайлерами на Java ))

    А в 1С для удобства читаемости очень подходит ДляКаждого (эдакий for each), грустно, когда вместо него используют что-то другое.
      0
      Еще интереснее на это посмотреть с точки зрения C++ :-)
        –3
        А смешно получилось. Произносится «Си плюс плюс счастливое лицо»
          +1
          Кстати, а есть ли для С++ профайлер такой же информативный как в 1С? Чтобы время выполнения показывал не по функциям, а по каждой строке кода?
            0
            perf top умеет нечто подобное, но не слишком точно.
              0
              Но оно нам не слишком нужно, потому что то что не является вызовом функции обычно транслируется в небольшой набор машинных инструкций, оценить затратность которых на глаз довольно просто, особенно имея перед собой информацию о времени выполнения каждой функции.
                0
                В циклах небольшой набор инструкций может выполняться довольно долго. Ну да ладно, с оптимизацией. Есть еще одно применение — отладка.

                По результатам замера производительности удобно анализировать выполнение кода. Можно сразу увидеть, какие ветки условий сработали, какие блоки кода не выполнялись вообще, и сколько раз прокрутились циклы.

                После такого взгляда на код обычная отладка «скаканием» по каждой строке покажется вам унылым занятием.
                  0
                  Именно для этого perf top умеет определять время занятое каждой отдельной инструкцией, этого хватает. Но все-таки такие вещи, как «исполняется ли ветка кода», обычно отладчиком определяют.
                  Кто мешает поставить точку останова на начало нужной ветки?
                    +1
                    Если б знать где упасть… Эта методика нужна до того, как поставить точку останова. То есть вопрос не в том, исполняется ли одна конкретная ветка. Проблема где-то в коде, но точное место неизвестно. По результатам замера можно одновременно увидеть как исполняются все ветки и сразу увидеть проблемное место. И когда уже проблема локализована, можно поставить точку останова на условие (если оно сложное) и определить, почему условие или цикл работает неправильно. Особенно удобно просматривать замеры на больших участках кода. Не знаю, понятно ли будет на небольшом примере.

                    Вот функция, которая должна найти сумму все чисел от 0 до 99, за исключением числа, переданного как параметр.

                    	int SumExcept(int x)
                    	{
                    1:		int s = 0;
                    100:		for (int i = 0; i < 100; i++)
                    100:		{
                    100:			if (x = i)
                    100:				continue;
                    			s += i;
                    		}
                    1:		return s;
                    	}
                    


                    В функции ошибка — она всегда возвращает 0. Первое, на что можно подумать – цикл не выполнился ни разу. Но прежде, чем ставить точку останова на цикл, давайте посмотрим на замер.

                    Цикл выполнился 100 раз, то есть проблема не в цикле. Условие так же проверилось 100 раз – это правильно. Но почему-то условие всегда истинно – оператор continue выполнился 100 раз. Итак проблема локализована — она в условии! Ой, как стыдно, вместо равенства оператор присваивания!
                    Пробегая отладчиком по коду, мы бы увидели то же самое, но в замере мы сразу видим работу всех циклов и условий в один момент времени. Да и точку останова уже можно не ставить — и так все понятно.
                      0
                      Как я уже сказал, такая проблема будет отловлена поиском аннотации perf top частоты использования инструкций типа add/mov, соответствующих строке кода s += i. Но конкретно в этом случае я поставлю точку останова на s += i, так как это и есть полезная нагрузка, а проверка в начале цикла полезной нагрузкой не является.
                      Я не спорю, что круто иметь возможность посмотреть на статистику по выполнению каждой отдельной строчки, но это не очень хорошо вяжется с C++.
                        0
                        конкретно в этом случае я поставлю точку останова на s += i, так как это и есть полезная нагрузка

                        Это очень интересная тема, т.к. свое пребывание в отладчике хочется оптимизировать по времени.
                        Отладчик на s += i не остановится, т.к. выполнение до этой инструкции попросту не дойдет. И нет никаких предпосылок предположить, почему это происходит. В этом примере можно визуально обратить внимание сначала на цикл, а затем на условие, но в реальном коде до полезной нагрузки может быть пару циклов и десяток-другой условий и визуальная проверка невозможна.
                        Как дальше локализовать место ошибки? Сделать еще одну попытку с точкой останова или начать пошаговую отладку с начала функции?
                        Я не спорю, что круто иметь возможность посмотреть на статистику по выполнению каждой отдельной строчки, но это не очень хорошо вяжется с C++.
                        К сожалению, даже профессионалы 1С не используют профайлер для отладки таким способом. Я случайно обратил внимание на эту возможность. Имеет ли смысл статья на хабре?
          +1
          Действительно с 1С порой складывается очень печальная ситуация. Мне встречались базы, в которых все данные хранились в одном регистре, а вместо ссылок на справочники использовались строки для хранения наименований и прочих данных. Знакомый рассказывал про конфигурацию, использующуюся для начислений в ЖКХ, в которой все было построено на справочниках, без регистров и без документов.
            0
            Visual Studio умеет же.
            +5
            1С и производительность — вещи несовместимые. Поэтому перестаньте заниматься фигней, точнее противестественным образом реализовывать свои нужды, и начните писать так, как вам удобно.
              +2
              Производительность 1с достаточна для задач, для которых она применяется, а скорость разработки при этом — колоссальная. Никто же кризис на 1с писать не просит, или ядерные реакции считать. Как и реализовывать бух. баланс на UnrealEngine. Видел недавно статью «CRM/ERP/еще какие-то модные буквы» на вордпрессе (правда не читал), представляю объем труда, который был вложен, а также, насколько это далеко от тех букв…
              +2
              Замер производительности запускает клиент в режиме отладки, как подчиненный процесс процессу конфигуратора, и соответственно, получает сырые данные о скорости исполнения. Т.е. реальные относительно времени исполнения процесса. Измерение же по времени дает оценку времени исполнения относительно операционной системы, в которой может быть запущено еще множество других процессов с индексом вытесняющей многозадачности более высоким, чем процесс исполнения 1С, соответственно и время будет очень размыто, конечно здесь взято среднее, но выполнено только 5 замеров, и погрешность лучше при таких замерах брать не по средней, а по средней квадратичной. Так значительно точнее.

              То, что рекурсия не сработала более чем на 2000 вполне ожидаемо — ведь в рекурсивном вызове постоянно происходит пополнение стека, ведь аргументы функции передаются через стек, который на 2000 уже закончился, и началась обработка сборщика мусора + куча перебросов в выделении памяти.
                +1
                Про рекурсию вы правильно заметили. Действительно, стек не резиновый, а я почему-то не упомянул об этом в статье. Спасибо за дополнение.

                На счет точности тоже согласен. При замерах я не запускал и не останавливал ни каких процессов. Согласен, что замер не точный, но я преследовал немножко другую цель. Хотелось наглядно показать, что использование специализированных механизмов, в частности цикла «ДляКаждого», не только упрощает написание кода и последующее в нем ковыряние, но и положительно влияет на производительность.
                  –1
                  На самом желе Для Каждого интересно тем, что оно работает не так, как его аналог из .NET for ..in…. for in использут интерфейс IEnumirable, который имплементирован в большинстве коллекций, и реализоация этого интерфейса не позволяет в полученное значение записывать информацию. Т.е.
                  for (var obj in diction)
                  {
                  obj.prop = 1;
                  }
                  выдаст ошибку. Однако в 1с это свободно отрабатывает =) всегда было интересно как это реализовано на уровне платформы, но увы, информация закрыта =(
                  Для Каждого Объект Из Коллекция Цикл
                  Объект.Свойство = 1;
                  КонецЦикла
                    0
                    Действительно, любопытно. Но думаю, что платформу если когда-нибудь и откроют, то это будет очень не скоро. А пока остается пользоваться и радоваться. Механизм очень удобный.
                      +2
                      Мне кажется вы что-то путаете, нельзя удалять или добавлять элементы в коллекцию во время for/foreach, а св-ва объектов менять, конечно, можно.

                      Скажем, в java, код:

                      for (MyClass obj: collection)
                      {
                      obj.prop = 1;
                      }

                      работает прекрасно. В C# тоже, должен работать.
                  0
                  А можно написать свой модуль на нормальном языке программирования и подключить его?
                  Тогда появится возможность писать цикл на брейнфаке под 1С… по-моему, это будет чудесно.
                    0
                    Идея интересная. Думаю её можно реализовать через подключение внешних DLL. Возможно, если будет время, реализую. Устрою гонки между родными и внешними циклами.
                      +2
                      Это не имеет смысла, т.к. библиотеки подключаются через COM, и время обращения к библиотеке будет дольше, чем исполняемая в ней команда.
                        0
                        А если предположить несколько миллиардов итераций? Возможно время подключения будет компенсировано?
                          +1
                          Только в теории, так как такой внешний код не сможет нормально работать с сущностями 1С, а даже гипотетически сложно представить ситуацию, когда в 1С потребуется миллиард итераций, работающих только с числами и строками и никак не использующий объекты самого 1С.

                          Плюс, мерить производительность циклов в высокоуровневых языках в большинстве случаев никакого смысла не имеет, так как один кривой запрос к базе данных или к сети, будет выполняться дольше любого практического числа итераций.
                          +2
                          Начиная с 8.2 есть же NativeAPI. Правда не факт что он по скорости будет намного лучше :)
                            0
                            NativeAPI по сути тот же COM только более красиво подключен и унифицированы вызовы внешних процедур и функций.

                            Сейчас 1С сделали REST интерфейс на основании протокола OData 3.0, так что может быть будет во много раз удобнее рассчет проводить извне обращаясь к 1С только для записи/чтения. Это по сути как OLE, только не запускает толстый клиент, конечно же, чисто на HTTP вызовах. Обещают скоро и поддержку JSON добавить =)
                              0
                              NativeAPI по сути тот же COM только более красиво подключен и унифицированы вызовы внешних процедур и функций.

                              Ни по сути, ни по реализации это ни разу не СОМ.
                              Используются нативные вызовы загруженной в память динамической библиотеки.
                            0
                            Вы немного заблуждаетесь, вот транслятор haxe в 1с, например.
                          0
                          А «для каждого» на разных коллекциях с одной скоростью работает, что ли?
                            0
                            Интересно, что показало бы сравнение на одинаковом железе:
                            1. Выполнение циклов на клиенте и на сервере.
                            2. 32-х разрядный и 64-разрядный сервер.
                            3. Сервер на винде и на линуксе.
                              0
                              Со скорость работы у 1С беда прямо. Недавно сравнивал JSON сериализатор на коде 1С с XML-сериализатором средствами платформы 1С
                              Сериализация примерно 5 тысяч записей таблицы значений:
                              JSON 120 секунд
                              XML 4 секунды
                                0
                                Может кривость реализации сериализации JSON, кто ее писал?
                                  0
                                  Сомневаюсь. Брались разные разработки с Инфостарт. Выбирал с лучшей скоростью. Большие задержки были на СтрЗаменить: в строке менялись одинарные кавычки на двойные.
                                  0

                                Only users with full accounts can post comments. Log in, please.