Быстродействие программ на VFP

Важное замечание
Пользовательский интерфейс
Rushmore
Навигационный и реляционный подходы
Оптимизация запросов
Нормализация БД
Алгоритм
Массивы (Arrays)
Работа с таблицами
Скорость выполнения отдельных команд
Пример
Как успокоить пользователя
Тестирование
Благодарности

Здесь вы можете скачать тексты демонстрационных программ.

Важное замечание

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

Пользовательский интерфейс

Если пользователи жалуются на слишком медленную работу форм, вы можете предпринять следующие действия:

  • Загружайте элементы интерфейса только тогда, когда в них появится необходимость. Это могут быть элементы на неактивных закладках PageFrame, скрытые узлы Treeview, или невидимые формы в FormSet. Практически любой набор объектов можно поместить в контейнер, сохранить его, как класс, а в нужный момент создать экземпляр этого класса.
  • Можно поступить "с точностью до наоборот" - при загрузке программы создать самые тяжелые и часто используемые объекты, и сделать их невидимыми до определенного момента. Обращали внимание, как долго загружается программа 1С? Зато потом скорость работы вполне приемлемая. Это пример такого подхода.
  • Если в большой таблице наложить фильтр, установить активный индекс, и отобразить результат через Browse или Grid, то переход между строками будет происходить очень медленно. Более подходящий источник для отфильтрованных и отсортированных данных - Local View.
  • Если вам не нужны все возможности Grid-а, замените его на Listbox. Этот контрол намного легче и быстрее. Кроме того, в нем нет проблем с подсветкой строки, которые имели место до 8-й версии VFP.
  • Установите последние обновления для сети и операционной системы. В моей практике было, когда для первоначального открытия десяти таблиц на сервере Novell требовалось полминуты. После установки на рабочих станциях патча для работы с Novell открытие таблиц стало происходить мгновенно.

Rushmore

Как использовать индексы для оптимизации Rushmore, описано в разделе HELP "Using Rushmore Query Optimization to Speed Data Access". В этой же статье вы найдете список команд, которые могут быть оптимизированы.

Оптимизация не будет работать, если:

  • Не совпадает Set Collate (в индексах таблицы используется Russian, а системная установка - Machine)
  • Индекс содержит условие FOR ... или ключевое слово UNIQUE
  • Индексное выражение включает условие .NOT., например Index on .NOT. DELETED()
  • Выражение для поиска не совпадает с индексным. Часто допускают такую ошибку. Предположим, у таблицы есть индекс по UPPER(cField1). Для поиска пустых полей используют условие EMPTY(cField1) = .T., что не совпадает с индексным выражением. Для того, чтобы поиск был оптимизированным, выражение должно выглядеть как UPPER(cField1) = SPACE(N), где N - длина поля cField1.

Навигационный и реляционный подходы

В FoxPro сосуществуют два подхода к обработке данных - навигационный и реляционный.Навигационные команды - это Locate, Seek, Set Relation, Set Order, GoTo. Основная реляционная команда - Select SQL. Скорость выполнения запросов SQL весьма высока, однако во многих случаях навигационный подход работает еще быстрее.

Пример поиска максимального значения поля различными способами:

 *** поиск с помощью Select SQL
Select MAX(Field1) as MaxField From Table1 Into Cursor _Temp
? _Temp.MaxField

*** а так выполнится значительно быстрее (необходимо наличие индекса по полю Field1)
Select Table1
Set Order To Field1
Go Bottom
? Field1

Оптимизация запросов

Запросы на FoxPro могут выполняться долго, если в них участвует больше трех-четырех таблиц. Речь идет именно о запросах к родным БД на dbf, а не о запросах, отправляемых на сервер. Как правило, увеличить скорость путем разбиения большого запроса на несколько последовательно выполняемых подзапросов не удается. Можно выйти из положения, исключая из запроса справочные таблицы (например, ФИО сотрудников), и привязывая их к результату через Set Relation, смешивая таким образом "реляционный" и "навигационный" подходы.

Использование Outer join снижает скорость выполнения запроса. Если нет прямой необходимости в этой опции, лучше переписать запрос без нее. Как вариант - если у вас есть строки, не имеющие соответствия в связанной таблице, и потому содержащие в поле для связи .NULL., добавьте в связанную таблицу строку с нулевым кодом, а в основной - замените пустые значения на этот нулевой код.

Подзапросы вида Where Field1 in (Select ... ) или Where Exists (Select ... ) могут замедлить скорость выполнения запроса (хотя это зависит от многих параметров). Если такой запрос выполняется слишком медленно - попробуйте переписать его с использованием простого объединения.

Для большего быстродействия располагайте первыми те условия, которые максимально сужают область дальнейшего поиска.

Для анализа оптимальности SQL-запроса воспользуйтесь функцией Sys(3054). Имейте в виду, что если у таблиц нет индексов по Deleted(), эта функция будет показывать частичную оптимизацию, даже если фактически они вносят отрицательный вклад в скорость выполнения запроса. Этот эффект подробно описан Владимиром Максимовым.

Нормализация БД

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

Например, в справочнике клиентов есть поле "Город". Исходя из принципов нормализации, необходимо завести таблицу - справочник городов. Так как названия городов меняются нечасто, в таблице клиентов вместо кода города можно записывать его название. Это позволит уменьшить количество таблиц в запросах, но потребует аккуратности, если название города все-таки изменилось, или в нем была допущена ошибка.

Другой вариант - предварительные расчеты. Например, с помощью триггеров можно заранее пересчитывать суммарные данные о продажах за день в момент добавления или корректировки нового счета. К примеру, пусть счета хранятся в таблице Invoices, а данные о продажах за день - в таблице DailySales. Тогда в триггеры таблицы Invoices нужно добавить примерно такой код:

 *** для триггера Insert
Update DailySales Set TolalSum = TolalSum + Invoices.InvoiceSum Where DailySales.SalesDate = Invoices.InvoiceDate

*** для триггера Update
Update DailySales Set TolalSum = TolalSum + Invoices.InvoicesSum - OldVal(Invoices.InvoiceSum) Where DailySales.SalesDate = Invoices.InvoiceDate

*** для триггера Delete
Update DailySales Set TolalSum = TolalSum - Invoices.InvoiceSum Where DailySales.SalesDate = Invoices.InvoiceDate

Естественно, сам код триггеров будет намного сложнее. Кроме приведенных строк там необходимы проверки, обработка ошибок и т.д.

Если вы решите идти по такому пути, не забудьте написать процедуру синхронизации, если из-за сбоев предварительные расчеты станут отличаться от основных.

Алгоритм

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

Вот несколько общих советов:

  • Вынесите за пределы циклов однократно выполняемые установки типа Set deleted, Set exact, и т.д.
  • Поместите проверки в начало подпрограммы. Это позволит не выполнять лишние действия, если проверки окончатся неудачно.
  • Освобождайте память от объектов и таблиц, которые вам больше не понадобятся.
  • Если позволяют технические возможности, перенесите наиболее тяжелые расчеты на другой компьютер, воспользовавшись технологией DCOM.

Массивы (Arrays)

Иногда можно встретить утверждение, что использование массивов ускоряет работу, так как весь объем информации находится в оперативной памяти. Возможно, это верно для небольших массивов. Если же вам нужно обработать большой объем информации - поместите данные в таблицы. Для работы с ними VFP имеет широкий набор оптимизированных команд и функций, которых вы лишитесь, работая с массивами.

Во многих языках программирования принято обрабатывать данные, находящиеся именно в массивах. На одном из форумов я даже видел тесты, в которых сравнивалась скорость обработки больших массивов в FoxPro и в других языках. IMHO, это сравнение не совсем корректно. Нужно сравнивать обработку массивов в других языках, и обработку таблиц - в FoxPro.

Работа с таблицами

Как уже говорилось, основная задача VFP - работа с таблицами. Здесь перечислены приемы, которыми вы можете воспользоваться для ускорения обработки данных:

  • Поиск записей с помощью команды Seek (или ее аналога функции SEEK( ) ) выполняется примерно в 5-10 раз быстрее, чем с помощью команды Locate.
  • Команда Locate позволяет указывать несколько условий, например Locate for ClientID = X AND InvoiceDate = Y. Для максимально быстрого поиска по нескольким полям заведите сложный индекс, в данном случае - STR(ClientID, 10, 0) + DTOS(InvoiceDate), и осуществляйте поиск с помощью Seek STR(X, 10, 0) + DTOS(Y).
  • "Быстрая" команда Seek может работать с любыми индексами, в том числе содержащими ключевое слово UNIQUE или условие FOR.... Однако имейте ввиду, что сложные индексы затрудняют читабельность и модификацию кода.
  • Если нужно только проверить наличие строк, удовлетворяющих условию, по которому есть индекс, но нет необходимости перемещать указатель на эти строки, воспользуйтесь функцией IndexSeek() (она появилась в VFP начиная с 6-й версии).
  • Команда Goto ... выполняется практически мгновенно. Если вам нужно вернуться к какой-либо записи в таблице - запомните текущее значение RECNO(). Возврат по номеру записи отработает быстрее, чем поиск по идентификатору с помощью Seek или Locate.
  • Цикл Scan...EndScan оптимизирует перебор записей таблицы даже без индекса. Там где это возможно, используйте его вместо конструкции Do While not EOF() ... Skip ... EndDo.
  • Выполнение команд Locate, Scan For.., Set Filter to.. замедлится, если у таблицы установлен активный индекс. Отключите индекс командой Set Order to без параметров, если порядок перебора строк неважен.
  • Команда Set Filter to.. входит в число Rushmore-оптимизируемых команд, и в "лабораторных условиях" скорость ее работы вполне удовлетворительна. Тем не менее, эта команда очень чувствительна к "внешним факторам", например, к установке активного индекса. Поэтому в большинстве случаев использование фильтров негативно сказывается на быстродействии.
  • Если вам необходимо просканировать некоторый диапазон значений в таблице, и по этим значениям есть индекс, вы можете воспользоваться приемом, известным как "Set Order + Seek + Scan while".
     Set Order to Field1
    Seek StartValue && устанавливаем указатель на начало диапазона
    Scan While Field1 =< EndValue && перебираем строки до конца диапазона
    ...
    EndScan

    Этот код работает быстрее, чем Scan for Between( Field1, StartValue, EndValue ) ... EndScan, но проигрывает ему в читабельности, и кроме того, требует установки активного индекса.

  • Если какой-либо индекс в таблице используется чаще других, во время технического обслуживания БД физически отсортируйте записи в таблице в порядке этого индекса. (Я никогда не использовал этот прием на практике, однако такой совет есть в HELP, в статье "Optimization of Tables and Indexes").

Скорость выполнения отдельных команд

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

  • Там, где это возможно, вместо макроподстановки & используйте выражения имени ( ) и функцию EVALUATE(). Выражения имени можно применять там, где Fox ожидает увидеть имя переменной или поля. Функция EVALUATE() может быть использована в операциях присвоения и вычислениях.
     X = "Var1"
    Y = "Var2"
    Store EVALUATE(X) to (Y) && так быстрее
    &Y = &X && эта команда сделает то же, что и предыдущая, но существенно медленнее
  • Если команда состоит из несколько макроподстановок, лучше собрать части команды в одну переменную, и выполнить единой макроподстановкой
     *** медленный вариант
    Select &FieldsList From &TablesList Where &JoinConditions
    *** так быстрее
    cSQL = "Select " + FieldsList + " From " + TablesList + " Where " + JoinConditions
    &cSQL
  • Стандартные функции VFP хорошо оптимизированы и выполняются достаточно быстро. Например, условиями задачи требуется найти цифру, входящую в текстовую строку. Можно по очереди перебирать символы:
     For I = 1 to LEN(cText)
    	If ISDIGIT( SUBSTR(cText, m.I,1) )
    		? m.I
    		Exit
    	EndIf
    EndFor

    Более быстрый вариант с использованием стандартных функций:

     cText1 = STRTRAN(cText, "123456789", "0")
    ? AT(cText1, "0")
  • Скорость обращения к переменным выше, чем к свойствам объекта или полям таблицы. Если значение какого-то свойства или поля используется многократно, присвойте это значение переменной.
    Подавление системных сообщений с помощью команд Set Talk off, Set Notify off (в последних версиях еще и Set Notify Cursor off) может существенно ускорить процесс.
    Сложение текстовых строк происходит быстрее, если к длинной строке добавлять короткую, а не наоборот. То есть

     *** этот код выполнится быстро
    x = ""
    For i = 1 to 10000
    	x = x + Sys(3) && функция Sys(3) играет роль "короткого текстового выражения"
    EndFor
    
    *** а этот - медленно
    x = ""
    For i = 1 to 10000
    	x = Sys(3) + x
    EndFor
  • Для поиска "узких мест" воспользуйтесь командой Set Coverage to. Эта команда создаст текстовый файл, в котором будет записано время выполнения каждой строки. Вы можете сохранить этот файл в таблицу, и проанализировать ее. Либо воспользоваться Coverage profiler из дистрибутива Visual FoxPro.

Пример

Предположим, вам необходимо всем клиентам, у которых есть заказ в предыдущем месяце, предоставить скидку 10% на будущие заказы. Данные о заказах хранятся в таблице Invoices, а информация о скидке - в таблице Clients.

 New_Discount = 0.10 && скидка, которую мы хотим предоставить
PrevDate = GoMonth(Date(), -1)
PrevMonth = Month(PrevDate) && предыдущий месяц
PrevYear = Year(PrevDate) && год, в котором был предыдущий месяц

Go top in Clients
Do while not EOF("Clients") && перебор записей в таблице клиентов
	Go top in Invoices
	Do while not EOF("Invoices") && перебор записей в таблице счетов
		If Invoices.ClientID = Clients.ClientID AND ;
		Year(Invoices.InvoiceDate) = PrevYear AND ;
		Month(Invoices.InvoiceDate) = PrevMonth
			Replace Clients.Discount with New_Discount
			Exit
		EndIf
		Skip in Invoices
	EndDo
	Skip in Clients
EndDo

Приведенный код логически верен, но абсолютно не оптимизируем.

Попробуем переписать этот код:

 Month_End = Date() - Day(Date()) && последний день предыдущего месяца
Month_Start = Month_End - Day(Month_End) + 1 && первый день предыдущего месяца
Select Clients
Scan
	Select Invoices
	Locate for ClientID = Clients.ClientID AND Between(InvoiceDate, Month_Start, Month_End)
	If Found("Invoices")
		Replace Clients.Discount with New_Discount
	Endif
EndScan

Перебор записей таблицы Clients осуществляется командой Scan. Поиск в таблице Invoices может быть оптимизирован с помощью индексов по полям ClientID и InvoiceDate. Этот вариант будет выполняться существенно быстрее.

Для реализации следующего варианта необходимо, чтобы у таблицы Invoices существовал особый индекс:

 Index on STR(ClientID, 10,0) + DTOS(InvoiceDate) tag MIXED
...
...
...
Set Exact OFF
LastMonth  = LEFT(DTOS( GOMONTH(DATE(), -1) ), 6) && год и месяц в формате "YYYYMM"
Select Clients
Replace Clients.Discount with New_Discount For IndexSeek( STR(ClientID, 10,0) + m.LastMonth, .F., "Invoices", "MIXED" )

Поиск в таблице Invoices осуществляется по индексу, поэтому этот пример отработает еще быстрее. Но индекс весьма специфический, и не может быть применен для других вычислений. Кроме того, даже автору это кода этого потребуется немало времени, чтобы спустя полгода разобраться, что именно он делает 🙂

Решение с использованием команд SQL:

 Update Clients Set Discount = New_Discount Where Exists ;
(Select * From Invoices Where ;
Invoices.ClientID = Clients.ClientID AND ;
Invoices.InvoiceDate BETWEEN Month_Start AND Month_End)

Возможно, данный вариант не выиграет с точки зрения скорости по сравнению с предыдущим, но он гораздо понятнее. Если время выполнения запроса находится в разумных пределах, я бы остановился именно на нем. К сожалению, такой синтаксис команды SQL-Update возможен только в VFP9.

По этой ссылке вы можете скачать исходные тексты примеров

Как успокоить пользователя, пока выполняется программа

Если программа в течение минуты не будет подавать признаков жизни, у пользователей возникает жгучее желание нажать Ctrl+Alt+Del, и прервать процесс. Вам необходимо информировать пользователя о состоянии дел. Вот варианты.

Если расчеты многоступенчатые, перед каждым этапом выводите либо в статус-бар, либо в окно сообщение

 Wait window "Этап 1: Проверка данных" nowait
...
Wait window "Этап 5: Суммирование данных за год" nowait
...
Wait window "Этап 10: Окончательное форматирование" nowait
...
Wait clear

При сканировании большой таблицы выводите номер обрабатываемой записи. Имейте ввиду, что вывод на экран - очень медленный процесс. Поэтому выводите сообщения только на каждой десятой или даже сотой строке:

 Set Talk Off
...
Scan
	...
	...
	If MOD(RECNO(), 100) = 0
		Application.StatusBar = "Обработано "  + TRANSFORM( RECNO() ) + " строк из " + TRANSFORM( RECCOUNT() )
	EndIf
EndScan

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

 Wait window "В прошлый раз расчет длился " + TRANSFORM(Duration) + " секунд" + CHR(13) + ;
		"Этот расчет начался в " + TIME() + CHR(13) + ;
		"и продлится примерно до " + TTOC( DATETIME() + Duration , 2 ) nowait

Если длительность расчета зависит от каких-то параметров, вы можете экстраполировать старые показания пропорционально новым. Например, если параметрами для вашей процедуры является диапазон дат, разделите длительность процесса на количество дней в диапазоне, и получите примерную длительность в расчете на один день. В следующий раз, когда пользователь укажет другие даты, умножьте сохраненный результат на новое количество дней, и получите экстраполяцию, сколько будут длиться вычисления. Время, естественно, будет очень приблизительным, но это в любом случае поможет пользователю оценить, сколько чашечек кофе он успеет выпить 🙂

Для полноты картины можете вывести прогресс-бар. Однако имейте в виду, что любые украшательства требуют системных ресурсов. Так что не переусердствуйте.

Тестирование

Советы уважаемых гуру, и даже выдержки из HELP не дают гарантий, что именно ваш случай не будет редким, но исключением из общих правил. Скорость работы может зависеть от сервера и сети, от активности пользователей, от объема и частотного распределения данных. Самый хороший способ определить, насколько быстро будет выполняться программа - провести тесты, максимально приближенные к реальности. Использование тестов только кажется сложным. Нет ничего сверхъестественного в том, чтобы сгенерировать табличку, близкую по размерам к максимальному ожидаемому размеру реальной БД, и попробовать поработать. В генерации тестовых данных вам помогут функции RAND()SYS(3) и SYS(2015), а так же любой осмысленный текст достаточно большого размера. В тестах для данной статьи я использовал kladr - классификатор адресов России. Для большей достоверности постарайтесь, чтобы данные не были физически отсортированы по какому-либо полю.

 *** Генерация тестовой таблицы ***
=RAND(-1) &&  HELP рекомендует перед началом работы вызвать RAND() с отрицательным параметром
For nCount = 1 to BigNumber && число добавляемых записей
*** пример генерации случайного числа от 1 до 1000
	nRandomValue = CEILING(1000 * RAND())
	*** а так можно заполнять почтовые индексы и номера телефонов
	cZipCode = PADR(CEILING(999999 * RAND()), 6, "0")
	*** поиск случайной записи в связанной таблице
	Goto CEILING(RECCOUNT("ChildTable") * RAND() ) in ChildTable
	RelatedTableValue = ChildTable.Field1
	*** пример генерации случайного адреса
	Goto CEILING(RECCOUNT("Towns") * RAND() ) in Towns
	Goto CEILING(RECCOUNT("Streets") * RAND() ) in Streets
	cAddress = ALLTRIM(Towns.Name) + ", " + ALLTRIM(Streets.Name) + ", дом " + TRANSFORM( CEILING(200 * RAND()) )
	*** Вставляем полученные данные в таблицу

	Insert into BigTable (...) values (nRandomValue, cZipCode, cAddress, RelatedTableValue)
EndFor

Регулярное проведение тестов имеет еще одну приятную особенность. Пока выполняется тест, вы имеете полное право откинуться на спинку стула, и несколько минут наслаждаться бездельем 😉

Благодарности

В работе над статьей принимали участие члены Фокс-Клуба: Владимир Максимов, Игорь Королев, Alex Shustikov, Соколов Игорь

Игорь Ильин
Последнее обновление: 22.12.05

Автор публикации

не в сети 8 часов

Joys

Комментарии: 2Публикации: 177Регистрация: 25-06-2000
Материалы по теме
Оставить комментарий
//////////////// ///////////////
Авторизация
*
*
Генерация пароля