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

Дисклеймер. Всегда важно понимать, что за задача стоит перед программистом. Порой важнее сделать задачу быстрее, чем качественнее. А если заказчик — это микропредприятие, то они вероятно даже не заметят отличия в работе оптимального кода от неоптимального.

Правило №1. Старайтесь не получать значения реквизитов через точку.

Плохо:


Наименование = Объект.Номенеклатура.Наименование;

Хорошо:


Наименование = ОбщегоНазначения.ЗначениеРеквизитаОбъекта(Объект.Номенеклатура, "Наименование");
// или
ЗначенияРеквизитов = ОбщегоНазначения.ЗначенияРеквизитовОбъекта(Объект.Номенеклатура, "Код, Наименование");
Код = ЗначенияРеквизитов.Код;
Наименование = ЗначенияРеквизитов.Наименование;

При обращении к ссылке через точку, выполняется неявный запрос к базе данных, который получает все реквизиты и табличные части и временно помещает полученные значения в кэш СУБД. А методы БСП модуля ОбщегоНазначения. ЗначенияРеквизитовОбъекта(), . ЗначениеРеквизитаОбъекта(), .ЗначенияРеквизитовОбъектов(), . ЗначениеРеквизитаОбъектов() помогают получить данных только конкретных реквизитов объекта, тем самым уменьшая нагрузку на СУБД.

Для того, чтобы посмотреть на это более наглядно, проведем эксперимент с замером по методике APDEX


#Область ПолучениеРеквизитаЧерезТочку

&НаКлиенте
Процедура ПолучитьРеквизитЧерезТочку(Команда)
	
	УИДЗамера = ОценкаПроизводительностиКлиент.ЗамерВремени("ЧерезТочку");  
	ЧерезТочку(Объект.Номенеклатура);
	ОценкаПроизводительностиКлиент.ЗавершитьЗамерВремени(УИДЗамера);
	
	НазначитьСлучайнуюНоменклатуру();
	
КонецПроцедуры

&НаСервереБезКонтекста
Процедура ЧерезТочку(Номенеклатура)
	Код = Номенеклатура.Код;
	Наименование = Номенеклатура.Наименование;
КонецПроцедуры

#КонецОбласти

#Область ПолучениеРеквизитаЧерезЗначениеРеквизита

&НаКлиенте
Процедура ПолучитьРеквизитЧерезЗначениеРеквизита(Команда)
	
	УИДЗамера = ОценкаПроизводительностиКлиент.ЗамерВремени("ЧерезЗначениеРеквизитов");
	ЧерезЗначениеРеквизитов(Объект.Номенеклатура);
	ОценкаПроизводительностиКлиент.ЗавершитьЗамерВремени(УИДЗамера);
	
	НазначитьСлучайнуюНоменклатуру();
	
КонецПроцедуры

&НаСервереБезКонтекста
Процедура ЧерезЗначениеРеквизитов(Номенеклатура)
	
	РеквизитыОбъекта = ОбщегоНазначения.ЗначенияРеквизитовОбъекта(Номенеклатура, "Код, Наименование");
	Код = РеквизитыОбъекта.Код;
	Наименование = РеквизитыОбъекта.Наименование;
	
КонецПроцедуры

#КонецОбласти

&НаСервере
Функция НазначитьСлучайнуюНоменклатуру()
	
	Если КэшНоменклатуры.Количество() = 0 Тогда
		
		Запрос = Новый Запрос;
		Запрос.Текст = 
		"ВЫБРАТЬ
		|	Номенклатура.Ссылка КАК Ссылка
		|ИЗ
		|	Справочник.Номенклатура КАК Номенклатура
		|ГДЕ
		|	Номенклатура.ЭтоГруппа = ЛОЖЬ";
		
		РезультатЗапроса = Запрос.Выполнить();
		КэшНоменклатуры.Загрузить(РезультатЗапроса.Выгрузить()); 
		
	КонецЕсли;
	
	КоличествоРезультатов = КэшНоменклатуры.Количество();
	
	ГСЧ = Новый ГенераторСлучайныхЧисел;
	СлучайныйИндекс = ГСЧ.СлучайноеЧисло(0, КоличествоРезультатов - 1);
	
	Объект.Номенеклатура = КэшНоменклатуры[СлучайныйИндекс].Ссылка;	
		
КонецФункции

Дополнительно, после каждого замера будем менять номенклатуру случайным образом. Произведем замер по методике APDEX и…. не сказать, что разница сколько-нибудь существенная…

Базовые приемы оптимизации кода. Часть №1. — иллюстрация

Но не будем торопиться с выводами, в правиле №2 я докажу, что пользоваться функциями БСП намного лучше.

Правило №2. Если нужно получить сразу много значений, то получай их одним запросом.

Рассмотрим случай, когда нам нужно вывести наименования по всему справочнику номенклатура. И сделаем это тремя способами:

  1. Через точку по ссылке,
  2. Через значение реквизита объекта,
  3. Получение одним запросом.

#Область СообщениеЧерезТочку

&НаКлиенте
Процедура СообщитьЧерезТочку(Команда)
	
	УИДЗамераЧерезТочку = ОценкаПроизводительностиКлиент.ЗамерВремени("СообщитьЧерезТочку");
	СообщитьЧерезТочкуНаСервере();
	ОценкаПроизводительностиКлиент.ЗавершитьЗамерВремени(УИДЗамераЧерезТочку);
	
КонецПроцедуры

&НаСервереБезКонтекста
Процедура СообщитьЧерезТочкуНаСервере()
	
	Запрос = Новый Запрос;
	Запрос.Текст = "ВЫБРАТЬ
	|	Номенклатура.Ссылка КАК Ссылка
	|ИЗ
	|	Справочник.Номенклатура КАК Номенклатура";
	
	Выборка = Запрос.Выполнить().Выбрать();
	
	Пока Выборка.Следующий() Цикл
		Наименование = Выборка.Ссылка.Наименование;
	КонецЦикла;
	
КонецПроцедуры

#КонецОбласти

#Область СообщениеЧерезЗначениеРеквизита

&НаКлиенте
Процедура СообщитьЧерезЗначениеРеквизита(Команда)
	
	УИДЗамераЧерезЗначениеРеквизита = ОценкаПроизводительностиКлиент.ЗамерВремени("СообщитьЧерезЗначениеРеквизита");
	СообщитьЧерезЗначениеРеквизитаНаСервере();
	ОценкаПроизводительностиКлиент.ЗавершитьЗамерВремени(УИДЗамераЧерезЗначениеРеквизита); 
	
КонецПроцедуры

&НаСервереБезКонтекста
Процедура СообщитьЧерезЗначениеРеквизитаНаСервере()
	
	Запрос = Новый Запрос;
	Запрос.Текст = "ВЫБРАТЬ
	|	Номенклатура.Ссылка КАК Ссылка
	|ИЗ
	|	Справочник.Номенклатура КАК Номенклатура";
	
	Выборка = Запрос.Выполнить().Выбрать();
	
	Пока Выборка.Следующий() Цикл
		Наименование = ОбщегоНазначения.ЗначенияРеквизитовОбъекта(Выборка.Ссылка, "Наименование").Наименование;
	КонецЦикла;
	
КонецПроцедуры

#КонецОбласти

#Область СообщениеНапрямую   

&НаКлиенте
Процедура СообщитьНапрямую(Команда)
	
	УИДЗамераНапрямую = ОценкаПроизводительностиКлиент.ЗамерВремени("СообщитьНапрямую");
	СообщитьНапрямуюНаСервере();
	ОценкаПроизводительностиКлиент.ЗавершитьЗамерВремени(УИДЗамераНапрямую);

КонецПроцедуры

&НаСервереБезКонтекста
Процедура СообщитьНапрямуюНаСервере()
	
	Запрос = Новый Запрос;
	Запрос.Текст = "ВЫБРАТЬ
	|	Номенклатура.Наименование КАК Наименование
	|ИЗ
	|	Справочник.Номенклатура КАК Номенклатура";
	
	Выборка = Запрос.Выполнить().Выбрать();
	
	Пока Выборка.Следующий() Цикл
		Наименование = Выборка.Наименование;
	КонецЦикла;
	
КонецПроцедуры

#КонецОбласти

Но здесь нужно быть очень аккуратным с замерами, чтобы не попасть в заблуждение о том, что обращение через точку может быть быстрее. Начнем с того, что стоит дождаться очистки кэша СУБД, чтобы СУБД производила физическое, а не логическое считывание данных.

Так как при быстром нажатие кнопки для инициирования алгоритма и замера времени выполнения, приведет к тому, что данные произойдет логическое считывание данных, временно помещенной таблицы в оперативную память. Поэтому между нажатиями делаем паузу в 10-15 секунд.

Вот кстати интересная выдержка из официальной документации к MS SQL

The I/O from an instance of the SQL Server Database Engine includes logical and physical reads. A logical read occurs every time the Database Engine requests a page from the buffer cache. If the page is not currently in the buffer cache, a physical read first copies the page from disk into the cache.

То есть, мы делаем вывод, что MS SQL копирует страницу (часть или всю таблицу) в оперативную память и до тех пор, пока считает данные в оперативной памяти валидными, считывает их из оперативной памяти.

Базовые приемы оптимизации кода. Часть №1. — иллюстрация

И вот здесь мы уже видим, что при хоть сколько-нибудь значительных объемах данных, получение реквизита через точку, нервно покуривает в сторонке.

Но получение нужных нам данных напрямую сразу из множества объектов, может дать огромный прирост в производительности. В нашем случае в 67 раз.