Если Вы не пишите
программы для обработки сложной графики, то
Ваш код вряд ли когда-то испытывал
ограничения по скорости исполнения. Но,
бывают случаи, когда каждая, даже
незначительная, задержка может сильно
повлиять на скорость работы Вашего кода. В
первую очередь это относится к функциям,
которые часто вызываются в Вашей программе,
да ещё "упакованы" в большой цикл.
Существует несколько принципов
оптимизации кода в Visual Basic, которые мы
сегодня рассмотрим.
Введение
Для начала я бы хотел
сделать оговорку, что всё о чем Вы здесь
прочитаете не является только моими "мыслями".
Все основные принципы оптимизации VB кода
давно известны и подробно описаны в толстых
руководствах к программным продуктам Microsoft,
но я бы хотел рассказать Вам о том же, но, как
бы, с позиции практики и на русском (более
понятным для Вас) языке.
Общие принципы оптимизации
Компания Microsoft
рекомендует использовать в своих
программах следующие основные принципы,
помогающие (по их мнению) получить более
быстрый код:
- Избегайте использования переменных
типа Variant
- Используйте целые переменные Long и
целочисленную математику
- Кэшируйте часто используемые свойства в
переменных.
- Используйте переменные уровня модуля вместо переменных Static
- Замените вызов процедуры линейным
кодированием
- Используйте константы,
когда это возможно
- Передавайте в процедуры параметры ByVal вместо
ByRef
- Использование объявленных необязательных параметров
- Воспользуйтесь преимуществом коллекций
Методика исследования и "оборудование"
Ну что? Давайте
рассмотрим все эти принципы или правила с
точки зрения практического использования.
Для этого я буду использовать самый точный
метод оценки практической ценности этих
принципов - замеры производительности. Итак
для работы нам будут нужны: несколько
процедур, которые помогут измерять
скорость выполнения той или иной операции,
две головы (моя и Ваша), четыре руки .... шутка.
Теперь серьезно, я буду измерять
производительность операции с
помощью функции timeGetTime из мультимедиа
библиотеки winmm.dll, входящей с состав WinAPI, так
считаю её наиболее точной и легкой в
использовании. Всё это "упаковано" в
отдельный модуль, я им всегда пользуюсь и
Вам советую. Процедуры измерения
производительности имеют вид:
'запускает таймер для измерения
Public Sub ProfileStart(lStart As Long)
lStart = timeGetTime
End Sub
'измеряет время выполнения операции
Public Sub ProfileStop(lStart As Long, lLenth As Long)
lLenth = timeGetTime - lStart
End Sub
Я намеренно не буду
приводить в тексте полные исходные тексты
процедур, которые буду тестировать на
производительность, так как они
значительного размера и статья бы
получилась очень большого объема. В общем,
кому интересно, то смотрите все исходные
тексты в
примере к статье.
Все замеры я буду
делать компьютере: Celeron 333
с системой Windows 98. Я хотел
было провести тестирование и в Windows 2000, но
прогнав несколько тестов в очередной раз
убедился, что это с практической точки
зрения бесполезно. Дело в том, что
реализация многозадачности в этой системе позволяет
её без труда прерывать любую
задачу для "профилактических" и иных
работ, а также для нужд других процессов,
поэтому Вы можете получить для одной и той
же операции результаты, отличающиеся друг
от друга в несколько раз. В общем, это
практически не имеет смысла.
Переменные различных типов
Тип необъявленных данных в Visual Basic - Variant. Это удобно для начинающихся программистов. Если Вы
хотите, чтобы Вас код выполнялся быстрее,
то нет переменным Variant. Microsoft утверждает, что
самый быстрый тип данных - long, а самый
медленный - variant. Ну что ж, за удобство не
объявления переменных нужно платить, ниже
Вы увидите какова реальная цена этого
удобства. Хороший способ избегать
Variant состоит в том, чтобы использовать инструкцию Option Explicit, которая вынуждает Вас объявлять все переменные. Чтобы использовать Option Explicit,
отметьте переключатель Require Variable Declaration на вкладке Editor диалогового окна Options, доступного от меню Tools.
Будьте внимательным при объявлении множественных переменных:
Например, в следующем объявлении, X и Y - Variant:
Dim X, Y, Z As Long
Чтобы все при переменные были Long, нужно
объявить данные так:
Dim X As Long, Y As Long, Z As Long
Теперь давайте
приступим непосредственно к тестированию. Напишем
процедуру, в которой будем измерять
производительность. В общем то, мы будем
заниматься ерундой, прибавлять и отнимать
единицу в цикле for.
For i = 1 To 100000
iType = iType + 1
iType = iType - 1
Next i
Для тестирования
различных типов переменных все
использующиеся в цикле переменные (кроме
счетчика цикла) сделаем одинакового типа, а
именно того типа, производительность
которого будем исследовать. После окончания каждого цикла
мы будем выводить результаты в отдельном
окне. Кому интересен полный код этой процедуры -
смотрите пример к статье. Итак, посмотрим на
результаты:
Время выполнения цикла (р -код):
long - 32 ms
integer - 35 ms
byte - 69 ms
single - 37 ms
double - 36 ms
currency - 44 ms
variant - 78 ms
|
Время выполнения цикла (компилятор):
long - 2 ms
integer - 5 ms
byte - 13 ms
single - 9 ms
double - 9 ms
currency - 17 ms
variant - 47 ms
|
Да.... Ну что можно
сказать, тип long, конечно, самый быстрый, а
variant самый медленный. Но, особой разницы,
между long и integer, практически нет, особенно
при компиляции в р -код. Разница
между long и variant, лично меня, сильно
впечатляет. А Вас?
Технические подробности.
Огромное влияние на скорость работы той или
иной переменной оказывает её размер,
занимаемый в памяти Вашего компьютера: long - 4
байта, integer - 2, byte - 1, single - 4, double - 8, currency - 8, variant
- 16. Кроме этого процессор Вашего компьютера
по разному работает с различными типами
переменных. Так, например на обработку Long и
Integer процессор тратит гораздо меньше тактов,
чем для других типов данных, более того,
самый быстрый тип данных тот, длина
которого совпадает с разрядностью
операционной системы, в которой он работает
(в нашем случае Windows 98). Поэтому Long, имеющий 32-бит,
работает быстрее, чем Integer, который, как
известно 16-битный. Более того, у каждого
компилятора есть некоторые особенности,
сильно влияющие на скорость работы
полученного кода. Так, например, строки
фиксированной длины в Visual Basic работают гораздо медленнее,
чем такие же строки переменной длины, хотя
теоретически должно быть наоборот. Так в
чем же дело? Дело в том, что практически все
внутренние операторы Visual Basic для работы со
строками используют строки переменной
длинны, поэтому перед вызовом такого
оператора происходит преобразование
строки, а после строка ещё раз
преобразовывается в строку постоянной
длинны. Ну а, как известно, преобразование
требует времени.
Кэширование свойств объектов в переменных
Если в цикле Вы
используете свойство какого либо объекта,
то такой код выполняется медленнее, чем
если бы Вы использовали вместо этого
свойства его значение предварительно
прокэшированное в переменной.
Допустим, что у Вас
есть такой код:
For i = 1 To 100000
l = frmMain.cmbVariant.Width
Next i
Конечно, :-) такой код в
своей программе трудно представить, но
смысл, думаю, понятен. Если свойство frmMain.cmbCashe.Width
не изменяются в процессе
выполнения цикла, то лучше всего записать
этот код так:
lCashe = frmMain.cmbVariant.Width
For i = 1 To 100000
l = lCashe
Next i
Время выполнения цикла (р -код):
без кэширования - 394 ms
с кэшированием - 8 ms
|
Время выполнения цикла (компилятор):
без кэширования - 337 ms
с кэшированием - 1 ms
|
Ну что, результат ясен,
переменные значительно быстрее, чем
свойства объектов.
Технические подробности.
Переменные всегда быстрее, чем свойства
объектов. Давайте разберемся с основной
причиной такого различия. Для этого
посмотрим, что будет делать компилятор,
встретив конструкцию типа frmMain.cmbVariant.Width.
Вначале он найдет объект frmMain, затем,
просмотрев его объекты, найдет cmbVariant и
только после этого ему станет доступно
свойство Width. Но, если Вы хоть раз писали и в
пошаговом режиме выполняли код класса, то
наверняка знаете, что для того, чтобы
получить значение любого свойства, нужно
выполнить код процедуры Property Get, отвечающей
за возврат значения этого свойства. Говоря
иными словами, для выполнения, казалось бы
одного "оператора", требуется
выполнить несколько достаточно больших
операций, а при предварительном
кэшировании свойства в переменной, этого не
происходит.
Используйте переменные уровня модуля вместо переменных Static
Microsoft утверждает, что
самые быстрые переменные - локальные, кроме,
конечно, Static переменных. Я, если следовать
логике, должен бы с ними согласиться, но,
исследовав работу переменных, я получил
очень интересные результаты.
Время выполнения цикла (р -код):
переменная Variant уровня модуля - 86 ms
местная Variant переменная - 84 ms
переменная Long уровня модуля - 50 ms
местная Long переменная - 49 ms
Static Long переменная - 57 ms
|
Время выполнения цикла (компилятор):
переменная Variant уровня модуля - 55 ms
местная Variant переменная - 55 ms
переменная Long уровня модуля - 20 ms
местная Long переменная - 20 ms
Static Long переменная - 19 ms
|
Вот так то, в р-коде все
как и обещали, а вот компилятору, похоже, все
равно, какая переменная, локальная или на
уровне модуля, а Static вообще отработала
быстрее всех. Я вначале подумал, что разные
типы переменных могут по-разному на это
реагировать, но потом, добавив ещё и Variant
переменную убедился в том, что тип
переменной не играет здесь никакой роли. Но,
почему же Static при компиляции в "естественный"
код работает не хуже, чем и обычная
переменная? Это же идет "в разрез" с
рекомендациями Microsoft. В общем, я бы сказал
так: если р-код, то Static работает хуже, если
компилятор, то можно использовать любые
переменные.
Технические подробности.
Такие переменные различаются своим местом
расположения при выполнении программы.
Переменные уровня процедуры расположены в
стеке данных процедуры, переменные уровня
модуля - в стеке этого модуля. Отсюда и
некоторое различие в скорости доступа к ним.
Замените вызов процедуры линейным
кодированием
Линейное кодирование
всегда было наиболее быстрым, но код,
написанный таким образом, теряет самое
главное - универсальность. Поэтому, если Вам
действительно необходим очень быстрый код,
то откажитесь от внутренних процедур, а в
любом другом случае не стоит отказываться
от процедурного программирования. Кроме
того Microsoft утверждает, что вызов процедуры
из другого модуля медленнее, чем из одного и
того-же модуля. Да, и ещё,
если Вы думаете, что классы Вас спасут, то
заблуждаетесь, я уже расматривал свойства
объектов, так вот классы работают с той же
скоростью, что и объекты. Это всё
рассуждения, основанные на рекомендациях
Microsoft, а вот результаты:
Время выполнения цикла (р -код):
линейное кодирование - 156 ms
процедура в том же модуле- 392 ms
процедура в другом модуле- 412 ms
класс - 598 ms
|
Время выполнения цикла (компилятор):
линейное кодирование - 11 ms
процедура в том же модуле- 11 ms
процедура в другом модуле- 11 ms
класс - 146 ms
|
Что я могу сказать, всё,
в принципе, правильно, линейное кодирование
действительно быстрее, чем вызов процедур,
но в "естественном" коде все выглядит
не так уж плачевно, как представляет нам
Microsoft, по крайней мере я не заметил никаких
различий в скорости. Но, если Вы компилируете в р-код, то
линейное кодирование работает гораздо
быстрее. Теперь что касается классов, они
действительно гораздо медленнее, чем
процедуры, но, если Вы хотите сделать свой
код универсальный, то не стоит отказываться
от классов в пользу процедур, а тем более в
пользу линейного кодирования. Хотя разница
в скорости работы, конечно, очень
существенная. Что же касается размещения
процедур в одном модуле, то я бы стал этого
делать, так как различия настолько
несущественны, то не стоит обращать на них
внимание.
Технические подробности.
Линейное кодирование изначально быстрее,
чем процедуры по одной простой причине: для
того, чтобы вызвать процедуру (функцию)
нужно найти её адрес и ещё передать ей все
используемые в ней переменные, а на это, как
ни оптимизируй, нужно время.
Используйте константы,
когда это возможно
Ну вот насчет констант
ничего сказать не могу, дело в том, что я
просто не представляю себе как можно
оценить их производительность.
Единственное, что могу сказать, это то, что
константы занимают меньший объем памяти, а
вот по скорости давайте поверим Microsoft.
Технические
подробности. Если Вы в своей программе 1000
раз напишите "", то наплодите 1000
совершенно разных пустых строк, каждая из
которых будет требовать память и время. А
вот константа vbNullString одна, сколько бы раз Вы
её не использовали.
Передавайте в процедуры параметры ByVal вместо
ByRef
В случае с различным
типом передачи параметров в процедуры,
можно утверждать, что передача по ссылке
работает медленнее, чем передача по
значению. В принципе, теоретически, здесь я полностью
согласен с Microsoft, хотя результаты
тестирования не такие уж и "красноречивые".
Время выполнения цикла (р -код):
передача как ByVal - 132 ms
передача как ByRef - 139 ms
|
Время выполнения цикла (компилятор):
передача как ByVal - 44 ms
передача как ByRef - 50 ms
|
Вы можете сэкономить
несколько миллисекунд, передав значения
как ByVal , но в реальном приложении получить
хороший выигрыш в производительности Вам
вряд ли удастся.
Технические подробности.
Чем же отличаются эти два способа передачи
переменных. При передаче ByVal, в процедуру
передается значение переменной, которое
можно сразу использовать, а вот если
переменная передается как ByRef, то вместо
значения в процедуру будет передан адрес
переменной, а вот её значение ещё нужно
будет получить, обратившись по этому адресу.
Тогда для чего нужен способ передачи ByRef,
если он медленнее? Дело в том, что передав
переменную как ByVal и изменив её значение в
этой процедуре, мы, изменим не ту, старую
переменную, а совершенно новую. Более того,
если мы позже, после выхода из процедуры,
захотим узнать новое значение этой
переменной, то получим её старое значение.
Вот поэтому Visual Basic передает все переменные
как ByRef, если Вы явно не указали иной способ
передачи. Но, если Вы не изменяете значения
некоторых переменных, передаваемых в
процедуру, то есть смысл передать их как ByVal.
Использование объявленных
необязательных параметров
Ну здесь даже и
проверять ничего не надо. Optional параметры
надо объявлять, иначе они будут как Variant, со
всеми вытекающими последствиями. Хотя
результаты здесь:
Время выполнения цикла (р -код):
передача необъявленного Optional - 175 ms
передача объявленного (как Long) Optional - 136 ms
|
Время выполнения цикла (компилятор):
передача необъявленного Optional - 70 ms
передача объявленного (как Long) Optional - 49 ms
|
Ну что, вывод очевиден,
Optional параметры надо объявлять, причем
наибольший прирост производительности я
наблюдал при компиляции в "естественный"
код. Думаю, что тут всем понятно, что
переменную типа Long, с её 4 байтами, можно
гораздо быстрее передать, чем то же
значение, но в переменной Variant, имеющей
размер 16 байт.
Воспользуйтесь преимуществом коллекций
Вначале я бы хотел процитировать
Microsoft. Способность использовать
коллекции объектов - отличная возможность Visual Basic. В то время как
коллекции могут быть очень полезны, для
лучшей производительности Вы должны использовать их правильно:
- Конструкция For Each...Next быстрее, чем For...Next.
- Избегите использовать параметры Before и After при добавлении объектов(целей) к совокупности.
- Используйте коллекции вместо массивов для групп объектов того же самого типа.
Коллекции позволяют Вам выполнять итерации
, используя For...Next цикл, однако
конструкция For Each...Next создает более
читаемый и в многих случаях более быстрый
код.
Использование параметров Before и After при добавлении
объектов в коллекции очень нежелательно,
так как это работает очень медленно. Если Вы имеете группу объектов
одного типа, то Вы можете выбирать что
использовать: массив или коллекцию, если они имеют
различные типы, то коллекция - Ваш единственный выбор. Со скоростной точки зрения, Вы должны
смотреть на то, как Вы планируете обращаться к объектам. Если Вы можете связывать
уникальный ключ с каждым объектом, то
коллекция - самый лучший выбор. Использование
ключа, для поиска объекта в коллекции быстрее, чем
поиск в массиве. Однако, если Вы не
используете ключи, то массив - лучший выбор. Матрицы быстрее
выполняют перебор данных, чем коллекции. Для
небольшого количества объектов, массивы используют меньшее количество памяти и могут
делать перебор данных более быстро.
Фактическое количество значений, где
коллекции станут более эффективными чем
массивы -
это более 100 объектов; однако, это значение может
изменяться в зависимости от скорости процессора и доступной памяти.
Вот здесь нам с Вами,
скорее всего, придется поверить Microsoft,
потому что такие исследования провести
сложно. Но я попытаюсь. Возьмем размерность
10000 элементов и два типа массивов (Variant и Long). Перебор будем делать
конструкциями For...Next и For Each...Next. Массивы
придется сделать динамическими (увеличивать
размерность по мере необходимости) и
обычными, а вот про поиск элементов
я помолчу, так как здесь столько разных
методов (я про массивы), что лучше и не надо
начинать.
Время выполнения цикла (р -код):
заполнение динамического массива Long - 22 ms
заполнение обычного массива Long - 2 ms
заполнение динамического массива Variant - 23 ms
заполнение обычного массива Variant - 4 ms
заполнение коллекции - 26 ms
For Next перебор массива Long - 3 ms
For Next перебор массива Variant - 4 ms
For Next перебор коллекции - 11259 ms
For Each перебор массива Long - 9 ms
For Each перебор массива Variant - 12 ms
For Each перебор коллекции - 11 ms
|
Время выполнения цикла (компилятор):
заполнение динамического массива Long - 22 ms
заполнение обычного массива Long - 1 ms
заполнение динамического массива Variant - 21 ms
заполнение обычного массива Variant - 2 ms
заполнение коллекции - 24 ms
For Next перебор массива Long - 1 ms
For Next перебор массива Variant - 3 ms
For Next перебор коллекции - 11169 ms
For Each перебор массива Long - 5 ms
For Each перебор массива Variant - 9 ms
For Each перебор коллекции - 13 ms
|
Вот это да! For Next перебор коллекции
работает просто с со скоростью черепахи, но For Each
в норме. В общем даже и не знаю, что Вам
посоветовать. В общем, если Вам не нужны
ключи и прочие преимущества коллекций, то я
за массивы, причем забудьте о For Each в
массивах, лучше используйте For Next. Что
касается коллекций, то они работают гораздо
медленнее, чем массивы. Но если Вам всё же
нужна коллекция, то и не вздумайте
использовать для работы с ними циклы,
основанные на For Next - это очень медленная
операция.
Заключение
Когда я начинал писать
эту работу, то прекрасно осознавал, что могу
вызвать огромное количество возражений со
стороны программистов. И дело тут вовсе не в
методике исследования и не в "правильности"
моих кодов, дело здесь в общем подходе к
проблеме. Начнем с того, что по моим данным,
например классы, работают гораздо
медленнее, чем вызов процедуры, но это же не
повод, чтобы полностью отказаться от их использования,
просто нужно помнить об этом и в тех
программах, где не возможен компромисс в
скорости, отказаться от них в пользу
процедур. Теперь насчет коллекций. Да,
действительно коллекции работают
медленнее чем массивы, но существуют такие
объекты, которые просто невозможно
использовать в массивах, например объекты
различных типов, кроме того в реальных
программах Вам не часто нужен простой
перебор всех элементов, гораздо чаще Вам
нужно найти какой-то определенный элемент,
вот здесь коллекции могут оказаться в выигрыше, так как позволяют искать элементы
по ключу.
В общем, я бы хотел,
чтобы Вы, прочитав эту статью, иногда
задумывались над тем, что не стоит
принимать все рекомендации других "специалистов",
в том числе и мои, как какие то постулаты в
своих работах. И если Вам действительно
нужна оптимизация кода по скорости работы,
то проверяйте все сами, тем более, что это не
сложно.
И напоследок, я сделал
ещё замеры производительности на другом
компьютере на базе Athlon. Честно говоря, я был
в шоке и, чтобы не вызвать "гнев" Intel или
AMD, не хочу их разглашать, кроме того я не
знаю, что сильнее повлияло на результаты,
большой 512 кБ кэш AMD или другая операционная
система в виде Microsoft Me, но факты говорят сами
за себя. Короче, вот Вам несколько
интересных результатов:
For Next перебор коллекции на Athlon "отработал"
в пять раз быстрее, тестирование класса
показало чуть меньшую скорость работы (!!!),
передача как ByRef и ByVal прошла примерно с
одним результатом и была ровно в десять раз
быстрее чем у Intel и т.д. В общем, практически
все тесты, за исключением некоторых (например,
классы) были гораздо быстрее, но прямой
зависимости в увеличении скорости нет.
Перепечатка этой статьи в любой форме запрещена. Все права на нее принадлежат журналу "Программист"
Статья опубликована в ноябрьском номере журнала
http://www.programme.ru