Блог → Оптимизация макросов VBA. Часть 2

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

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

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

1. Ищите многократно повторяющийся код и выделяйте его в процедуры. Выделяйте повторяющиеся строки в константы.
2. Если цикл короткий и выполняется 2-3 раза, разверните его (просто напишите тело эти 2-3 раза). Разверните небольшие подпрограммы и подпрограммы, вызываемые один раз, то есть, те, которые служат только для абстракции от сущности выполняемых действий. Выносите всё, что можно, из циклов, подсчитывая промежуточные значения переменных вне цикла.
3. Объединяйте циклы For с одинаковым числом повторений.
4. Избегайте глубокой рекурсии. Рекурсия перегружает стек, да и вызовы процедуры выполняются медленнее, чем цикл.
5. Избегайте ненужной рекурсии, при которой одно и то же значение вычисляется многократно, порождая растущее дерево рекурсивных вызовов. Классический пример: рекурсивное вычисление чисел Фибоначчи, когда процедура дважды вызывает себя для вычисления предыдущих членов ряда, вызванные процедуры в свою очередь делают по два рекурсивных вызова и так далее. При этом вычисляются одни и те же значения, но они не сохраняются.

А теперь - перечислю те рекомендации, которые относятся лишь к VBA. Как и любой другой язык программирования, VBA имеет свои "изюминки" и на эти особенности стоит обращать внимание, если вы хотите писать эффективный код.

1. Не злоупотребляйте функциями Choose, If и Switch, заменяя ими If и Select Case, особенно в многократно повторяющихся циклах. Почему? Они гораздо медленнее обычных If и Case (примерно в 2-3 раза), хотя зачастую, и удобнее, в ряде случаев. Кроме того, при вложении этих функций сильно снижается производительность, так как для передачи в функцию будут вычислены все значения, а не только те, которые нужны.
2. Разбейте условие с And на два вложенных условия. Причем внешнее условие должно не выполнятся чаще, чем внутреннее, и/или быть гораздо проще него. Если внешнее условие неверно, программа не будет проверять внутреннее, а сразу перейдет к следующему участку программы:

'Вот так плохо
If х And у then
s = s & " "
End If

'А так - хорошо
If x then
If у Then s = s & " "
End If


Компилятор VBA не выполняет такой оптимизации автоматически, как это делают, например, Delphi, VC++ и целый ряд компиляторов Basic.

Кроме того, рационально выбирайте значения флагов. Выполнение перехода происходит немного быстрее, когда условие If...Then...Else ложно, а не истинно, что, по всей видимости, связано с внутренними особенностями реализации p-кода. Старайтесь проектировать часто проверяемые условия так, чтобы чаще выполнялась "ложная", а не "истинная" ветвь (это относится в основном к выбору значений флагов). К примеру, следующие два макроса почти одинаковы, но при этом первый выполняется на 18 секунд быстрее второго:

'выполняется 129,8 сек.
Dim S Аs Boolean
S = False
X! = Timer
For j& = 1 To 50000000
If S Then g$ = " " Else R$ = " "
Next j
MsgBox Str$(Timer - X)

'выполняется 148,1 сек.
Dim S As Boolean
S = True
X! = Timer
For j& = 1 To 50000000
If S Then R$ = " " Else g$ = " "
Next j
MsgBox Str$(Timer - X)


При этом замечу, что разницы в скорости для выполняемого или не выполняемого условия без Else практически нет.

Дальше переходим к переменным и типам данных. На что следует обращать внимание? По возможности избегайте типа данных Variant. Переменные Variant занимают больше места в памяти и сильно "тормозят" (особенно это касается счетчиков циклов). Объявляйте все переменные явно, включив в начало каждого модуля Option Explicit. Так вы можете легко найти неиспользуемые переменные и избежать ошибок в написании имен переменных. Эти ошибки бывает трудно обнаружить в большом макросе. Производительность разных типов переменных можно расположить в такой последовательности:
• локальные переменные уровня процедуры (100);
• глобальные переменные уровня модуля (112);
• общие (Public) для нескольких модулей переменные (113);
• статические переменные уровня процедуры (122).

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

1. Избегайте использования ненужных глобальных и общих переменных. Если вам не нужно обращаться к переменной из разных процедур/модулей, объявите ее на уровне одной процедуры или модуля.
2. Вынесите статические переменные на уровень модуля, если процедура с этими переменными вызывается часто.

При этом вы потеряете независимость процедуры от внешних ссылок (если потребуется перенести её в другой проект, нужно будет не забыть скопировать и глобальные переменные тоже). Значения переменных также будут сохраняться между вызовами макросов (то есть пользователь может вызвать ваш макрос, затем поработать в Word, затем слова вызвать макрос, и значение переменной при этом сохранится).

Dim Н&
Sub G()
' При первом вызове - 0, при последующих - 1
MsgBox Н
Н = 1
End Sub


Неявные преобразования всегда быстрее - если вам нужно максимально ускорить программу, пользуйтесь ими. Хотя выигрыш во времени велик, неявные преобразования нарушают стиль вашего кода (в других языках программирования неявные преобразования между строкой и числом запрещены). Другой недостаток первого примера: если вы введёте 233 и потом без пробела амперсанд, то VBA распознает это как значение 233 типа Long. Точно также &233 будет понято неправильно. То есть, до и после амперсанда необходимо ставить пробелы, тогда как для плюса они вставляются редактором автоматически при переходе на другую строку.

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

В частности, If intNumber Then для сравнения переменной с нулём работает немного быстрее, чем If intNumber <> 0 Then (при этом ненулевое значение распознается инструкцией If как True). Жаль, что Not iNumber использовать нельзя, так как Not в языке Basic понимается как битовая, а не логическая операция, и Not 12, к примеру, равен -13. Аналогично можно написать Len(strName) вместо strName <> "" - выигрыш в скорости будет значительно больше, примерно в 1,5 раза!

К сожалению, в VB/VBA нет сдвигов, что не позволяет реализовывать быстрые умножения и деления на степени двойки, но сделать быстрые модули степеней двойки можно с помощью того же And (просто обнуляя старшие биты): вместо X Mod 4 пишем X And 3 (четыре минус один, так как нас интересуют все двоичные разряды до второго включительно, остальные обнуляем), вместо X Mod 2 - X And 1. Выигрыш по времени составит около 30%.

Чтобы один раз вычислить число Пи, можно использовать формулу 4*atn(1). Если формула с Пи не одна, а тем более, если она вычисляется в цикле, обязательно заведите отдельную переменную и в начале работы макроса присвойте ей значение 4*atn(1). В дальнейшем вы будете обращаться к уже вычисленному значению, которое хранится в этой переменной. Другой вариант - объявить Пи как константу (как известно, Пи ~ 3.141592653589793238462643).

Всегда пишите знак операции целочисленного деления (\) для Integer, Long и Byte. Несмотря на то, что документация утверждает равнозначность / и \ для целых переменных, операции с последним знаком выполняются на 25% дольше (12 секунд против 9). Почему это происходит? Всё очень просто, в первом случае числа неявно преобразуются в вещественные, а затем производится деление с плавающей запятой (FDIV на ассемблере). Во втором же случае используется обычное целочисленное деление (DIV).

Никогда не возводите в известную степень, меньшую 27: вместо этого перемножьте числа (A*A *A*A и т.д.). Умножение работает в несколько раз быстрее, особенно на небольших степенях (скажем, для квадрата - A*A выполняется 10 млн. раз за 6,9 секунд, а A^2 - за 49,7 секунд). Причём это относится как к целым, так и к действительным числам.

Деление медленнее умножения примерно на 20%. Замените деление на константу умножением на обратную константу, деление на несколько переменных делением на их произведение. Вместо умножения на два целых и действительных чисел используйте сложение, оно выполняется немного быстрее (как минимум, на 2-3%).