Оптимизация программы на VB6
Кто сказал, что VB – тормоз?
Сравнение VB и
Delphi 7.
Существует мнение, что VB –
редкостный тормоз. Я хочу опровергнуть это мнение. Произведем сравнение VB6 и Delphi на
вычисление факториала.
Тест 1. Целочисленная арифметика
В тесте
производится 10 000 000-кратное
вычисление факториала 12-ти с помощью программы на Delphi и на VB. Причем для последнего
произведено несколько тестов: для неоткомпилированной программы (F5), и для компилированной с различными оптимизациями.
Для начала
приведу тексты программ.
VB:
Проект: форма
Form1 с кнопкой Command1, и модуль Module1
Form1:
Option
Explicit
Private
Sub Command1_Click()
Dim
i As Long
Dim
t As Long
Dim
tmr As Long
tmr
= GetTickCount
For
i = 0 To 10000000
t = Factorial(12)
Next
i
MsgBox
GetTickCount - tmr
End
Sub
|
Module1:
Public
Declare Function GetTickCount Lib "kernel32" () As Long
Option
Explicit
Public
Function Factorial(ByVal x As Long) As Long
Dim
i As Long
Dim
t As Long
t
= 1
For
i = 2 To x
t = t * i
Next
i
Factorial
= t
End
Function
|
Delphi:
Проект: Форма, на ней Button1: TButton.
Код формы (начало и конец опущены):
…
function
Factorial(x:integer):integer;
var
i:integer;
t:integer;
begin
t:=1;
for i:=2 to x do t:=t*i;
factorial:=t;
end;
procedure
TForm1.Button1Click(Sender: TObject);
var
tmr:integer;
i:integer;
t:integer;
begin
tmr:=gettickcount;
for i:=0 to 10000000 do begin
t:=Factorial(12);
end;
messagedlg(inttostr(gettickcount-tmr),mtwarning,[mbok],0);
end;
…
|
И вот что вышло в результате тестов:
Обозначения:
Все, что не Delphi относится
к VB.
Debug – неоткомпилированная.
Т.е. просто F5.
P-Code: программа,
откомпилированная в псевдо-код. О том, что это такое, будет сказано дальше.
Все остальное
– Native Code.
Применено
следующее сокращение:
O P abifud
O:
optimization
F=Optimize for fast code
S=Optimize for small code
N=No optimization
P: Favor Pentium
Pro
+=on
-=off
a=Assume
No Aliasing (+-)
b=Remove
Array Bounds Checks
i=Remove
Integer Overflow Checks
f=Remove Floating
Point Error Checks
u=Allow
Unrounded Floating Point Operation
d=Remove
Safe Pentium(tm) FDIV Checks
Несколько
слов о результатах. Как видно, показатели Delphi совсем не плохи, но VB все же может работать быстрее. И видно, что основной вклад
дает флаг отключения проверки переполнения.
Примечания: 1.
все времена измерялись функцией GetTickCount, которая имеет дискретность в
15-16 миллисекунд (она изменяет свое значение иногда с шагом 15, а иногда 16).
Возможно, это специфика конкретно моей конфигурации, но я не встречал
отклонений от этого свойства. Если захочется измерять реально с точностью до
1-й миллисекунды, используйте мультимедиа-таймеры (TimeBeginPeriod, TimeGetTime и TimeEndPeriod из библиотеки WinMM).
2. В Delphi были установлены следующие флаги:
Optimization +
Stack Frames –
Pentium-safe FDIV –
Range Checking –
I/O Checking +
Overflow Checking –
Тест 2. Вещественная арифметика
Сейчас
начинается самое интересное. Я очень удивился, увидев результаты, и сразу стал
искать, что же я не так сделал. Но не нашел. Привожу программы. Единственное
что там поменялось – тип данных. Отличия от целочисленных версий подчеркнуты.
VB:
Form1:
Option Explicit
Private Sub Command1_Click()
Dim i As Long
Dim t As Double
Dim tmr As Long
tmr = GetTickCount
For
i = 0 To 10000000
t = Factorial(12)
Next
i
MsgBox
GetTickCount - tmr
End
Sub
|
Module1:
Public
Declare Function GetTickCount Lib "kernel32" () As Long
Option
Explicit
Public
Function Factorial(ByVal x As Long) As Double
Dim
i As Long
Dim
t As Double
t
= 1
For
i = 2 To x
t = t * i
Next
i
Factorial
= t
End
Function
|
Delphi:
Код формы (начало и конец опущены):
…
function
Factorial(x:integer):extended;
var
i:integer;
t:extended;
begin
t:=1;
for i:=2 to x do t:=t*i;
factorial:=t;
end;
procedure
TForm1.Button1Click(Sender: TObject);
var
tmr:integer;
i:integer;
t:extended;
begin
tmr:=gettickcount;
for i:=0 to 10000000 do begin
t:=Factorial(12);
end;
messagedlg(inttostr(gettickcount-tmr),mtwarning,[mbok],0);
end;
…
|
Вот результаты:
Обозначения:
Delphi (extended) – результаты для
приведенной программы.
Delphi (double) – результаты работы программы с
типом Double вместо Extended.
Все, что не Delphi относится
к VB.
Debug – неоткомпилированная.
Т.е. просто F5.
P-Code:
программа, откомпилированная в псевдо-код. О том, что это такое, будет сказано
дальше.
Все остальное
– Native Code.
Применено
следующее сокращение:
O P abifud
O:
optimization
F=Optimize for fast code
S=Optimize for small code
N=No optimization
P: Favor Pentium
Pro
+=on
-=off
a=Assume
No Aliasing (+-)
b=Remove
Array Bounds Checks
i=Remove
Integer Overflow Checks
f=Remove
Floating Point Error Checks
u=Allow
Unrounded Floating Point Operation
d=Remove
Safe Pentium(tm) FDIV Checks
Как и
ожидалось, время выполнения на Delphi увеличилось.
Все-таки, вещественные числа обязаны работать медленнее. А вот VB выдает поразительные результаты.
Мало того, что все режимы Native-кода кроме
неоптимизированного на VB работают
быстрее, так еще и подсчет с полной оптимизацией вообще даже быстрее чем
целочисленный метод почти в 2 раза! Я сам не могу понять, в чем здесь дело.
Единственная догадка – компилятор VB догадался
заменить вещественный тип на целый. Но как-то не верится в это. К тому же, это
не дало бы ускорения по сравнению с целочисленным методом…
Надеюсь, у вас
больше не будет мнения о VB как
о тормозе. Но я приведу еще аргументы.
Тест 3. Запуск среды разработки
Здесь все
лаконично. Измеряется время второго подряд запуска среды разработки. (Второй по
тому, что при первом Windows’у требуется считать все в
оперативную память, время чего непредсказуемо. А при втором запуске Windows уже все имеет в оперативке, и
время запуска стабилизируется.) Запуск производился открытием проектов,
использованных в тесте 1.
Delphi 7: 13.5sec
VB6: 1.8sec
Разница
налицо.
Тест 4. Запуск программы на исполнение
Опять
измеряется время второго запуска. Но на этот раз программы.
Delphi 7: 1sec
VB6: меньше чем 0.19sec
Здесь время
пришлось замерять секундомером. По сему погрешность измерений велика. А для VB вообще не удалось замерить время
запуска – не успевал нажать на кнопку. Но все равно, разница налицо.
Оптимизация программ для VB
Все до этого
было просто лирическим отступлением. А теперь к собственно оптимизации
программ.
Дизайн проекта
Здесь все
естественно. Использовать поменьше дополнительных компонентов. Они могут долго
грузиться. Также использовать как можно меньше рисунков на формах, чтоб они
грузились быстрее.
Могу сказать,
что многие OLE-объекты работают медленно. Старайтесь
обойтись без них. Если OLE-объект является лишь элементом
дизайна, то его лучше заменить картинкой.
Старайтесь
сделать в вашем проекте как можно меньше форм. Если их достаточно мало, то не
потребуется выгружать форму после закрытия, что еще позволит при следующем
показе ее не загружать. Если диалоговое окно долго грузится, это откладывает
довольно сильное впечатление о медленности программы.
Чем легче
ваша программа в смысле размера, тем быстрее она будет работать. Это связано с
тем, что если Windows’у потребуется освободить память,
и он скинет вашу программу на винчестер, ему потом будет легче ее оттуда
выгружать обратно, когда юзер снова обратится к ней.
Кодирование
Самое важное
для скорости программы – правильно продумать архитектуру. Это и упростит
программу, и ускорит ее. Однако обсуждать вопрос архитектуры в общем случае
трудно. Могу лишь дать один совет: старайтесь придерживаться стандартных
форматов. Я к тому, что я сам один раз напоролся на несоответствие моего
формата с форматом для вывода данных. Это было в графическом редакторе.
Поначалу я писал его, не зная, что такое WinAPI. И
выводил рисунок на экран методами Line и PSet.
И формат цвета пикселя соответствовал тому, что используется в VB. А в Bitmap-ах, как я позже
выяснил, используется формат, где синий и красный поменяны местами по сравнению
с тем, что я использовал. Кроме того, порядок индексов у меня был неверен: у
меня получалось, что данные записываются по столбикам, а все стандартные WinAPI хотят развертку по строкам.
Последнее я исправил без труда. А вот с первым пришлось возиться очень долго, и
я до сих пор ощущаю последствия этого перехода (кое-где до сих пор синий вместо
красного вылезает). Вообще, я не понимаю, почему MS так сделали: в Bitmap-ах цвета
хранятся в формате BGR0, а всем функциям рисования (SetPixel к
примеру) надо подавать цвет в формате RGB0.
Ну и конечно,
надо составить эффективные алгоритмы для наиболее длительных операций. Тут уже
думайте сами.
Когда дело
доходит до кодирования, здесь важно помнить следующее:
А)
обязательно объявлять все переменные. И каждой переменной указывать тип. Это
поможет не только компилятору создать более эффективный код, но и вам не
запутаться в вашей программе. Поясню подробнее.
Рассмотрим
пример. На функции вычисления факториала. Предположим я написал ее так:
Function
Factorial(x)
accum
= 1
For
i = 1 To x
accum = acuum * i
Next
i
Factorial
= accum
End
Function
|
Если эту
функцию протестировать, то она будет все время давать ноль. В чем дело?
Дело в том,
что я сделал опечатку. Внутри цикла я написал acuum вместо accum. Здесь, конечно опечатка
довольно искусственная, но поверьте мне, такого рода опечатки случаются
довольно часто, и иногда их очень сложно найти.
Чтобы
избежать таких сложных поисков ошибок, поместите Option Explicit в самое начало модуля. Эта фраза скажет Visual Basic’у, что вы обязуетесь объявлять
все переменные. Тогда вам придется переписать функцию следующим образом.
Function
Factorial(x)
Dim
i,
accum
accum
= 1
For
i = 1 To x
accum = acuum * i
Next
i
Factorial
= accum
End
Function
|
Теперь если вы
попробуете выполнить эту функцию, VB укажет
вам на вышеупомянутую опечатку, и вы исправите.
Function
Factorial(x)
Dim
i, accum
accum
= 1
For
i = 1 To x
accum = acсum * i
Next
i
Factorial
= accum
End
Function
|
Но этот
алгоритм не оптимален. Можно заметить, что в первом проходе цикла производится
умножение на единицу. А зачем нам умножать на единицу? Поправим.
Function
Factorial(x)
Dim
i, accum
accum
= 1
For
i = 2 To x
accum = acсum * i
Next
i
Factorial
= accum
End
Function
|
Правда,
повышение производительности от этого вы вряд ли заметите. По тому, что удалена
всего одна операция, и она ничто по сравнению с 15-ю к примеру.
Но есть
случаи, когда такого рода поправки очень критичны. Напимер, вам надо нарисовать
холмик параболической формы. И можно написать:
Sub
Holmik(h)
Dim
x, y
For
x = 0 To 100
For y = 0 To 200
If y <= (1 - ((x / 100) * 2 - 1) ^ 2) * h Then
Form1.PSet (x, 200 - y)
End If
Next y
Next
x
End
Sub
|
Но зачем
перебирать точки, которые не нужно изменять? Давайте не будем их перебирать.
Для этого установим правильные границы для цикла по y.
Sub
Holmik(h)
Dim
x, y
For
x = 0 To 100
For y = 0 To (1 - ((x / 100) * 2 - 1) ^ 2) * h
Form1.PSet (x, 200 - y)
Next y
Next
x
End
Sub
|
Это работает
заметно быстрее, даже не смотря на то, что метод PSet
жутко медленный. А теперь давайте ускорим наш холмик еще сильнее.
Работа с
типом данных Variant очень тяжела. А в данном случае я нигде не указал тип
данных. И он стал Variant,
как по умолчанию. Давайте объявим типы:
Sub
Holmik(h As Long)
Dim
x, y As Long
For
x = 0 To 100
For y = 0 To (1 - ((x / 100) * 2 - 1) ^ 2) * h
Form1.PSet (x, 200 - y)
Next y
Next
x
End
Sub
|
ВНИМАНИЕ.
Здесь я напоролся на подводный камень, на который налетают многие, особенно те,
кто знает, к примеру, паскаль. Написав это:
Я объявил
переменную x как Variant (не указал тип переменной). По этому, такое
объявление надо писать так:
Или так:
Dim
x As Long
Dim
y As Long
|
Последняя
запись предпочтительнее, так как она не порождает желания опустить тип для x.
Так, перепишу
Holmik правильно:
Sub
Holmik(h As Long)
Dim
x As Long
Dim
y As Long
For
x = 0 To 100
For y = 0 To (1 - ((x / 100) * 2 - 1) ^ 2) * h
Form1.PSet (x, 200 - y)
Next y
Next
x
End
Sub
|
Вот этот
алгоритм достаточно быстр. Конечно, его можно еще немного оптимизировать.
Возведение в степень – операция медленная. По этому можно расписать его как ((x / 100) * 2 - 1) * ((x / 100) * 2 - 1). Не
знаю, будет ли это быстрее. Но однозначно будет, если записать выражение ((x / 100) * 2 - 1) в
отдельную переменную t, и потом
вместо ((x / 100) * 2 - 1) * ((x / 100) * 2 - 1) написать t * t. Но все это мелочи,
так как происходит вне цикла и по времени выполнения несравнимо быстро по
отношению к циклу. По этому об этом не стоит сильно беспокоиться. Как правило,
достаточно обработать самый глубокий цикл (но в то же время иногда бывают
случаи, когда это не совсем так).
А код
факториала стоит написать так:
Function
Factorial(x As Long) As Long
Dim
i As Long
Dim
accum As Long
accum
= 1
For
i = 2 To x
accum = accum * i
Next
i
Factorial
= accum
End
Function
|
Как ни
странно, этот код еще можно оптимизировать. А именно – передавать значение x не по ссылке, а по значению. Так
как ссылка есть 32-битное число (адрес в памяти), и Long
тоже 32-битное число. И по ссылке, и по значению данных передается
столько же, но при передаче по ссылке придется делать дополнительную операцию –
читать значение x из памяти.
Так что лучше использовать передачу по значению. Это немного более важно в
функциях, которые вы будете вызывать в цикле и если они состоят из одной-трех
строчек. Например:
Function
Min(x As Long, y As Long) As Long
If
x > y Then Min = y Else Min = x
End
Function
|
Вообще для
повышения производительности рекомендуется использовать в выражении только один
тип данных. Если, к примеру, вы делите два числа нацело, пишите w = w \ 2 вместо w = Int(w / 2). Вообще последнее должно рассматриваться
так: w =
CInt(Int(CDbl(w) / 2)). А целочисленное деление есть команда процессора,
которая должна выполняться как одна операция, то есть быстро. Но однако
оператор •\•
(делить нацело) работает не совсем так как Int(•/•). А именно, он работает как Fix(•/•).Напомню,
разница между Int
и Fix
в том, что Int возвращает наибольшее целое, меньшее аргумента, а Fix возвращает ближайшее меньшее по модулю целое к аргументу.
Пример:
Immediate
window
----------------
?
int(0.5)
0
?
fix(0.5)
0
?
int(-0.5)
-1
?
fix(-0.5)
0
?
int(-1/2)=-1\2
False
?
fix(-1/2)=-1\2
True
|
Типы с плавающей точкой
На самом деле
оказывается, что VB абсолютно
до лампочки (в смысле производительности), Single или Double –
время исполнения не зависит то выбора типов. Но однако настоятельно рекомендую
использовать только один из них (Double), чтобы у VB не было трудностей со сравнением. И крайне не рекомендуется
использовать тип Variant,
так как он жутко медленный. Например, подсчет факториала в предыдущем тесте,
произведенный с помощью Variant, занял 12.2 сек, тогда как Double в той же
программе обсчитывался 0.55 сек. Разница очевидна.
Работа с графикой
Графические
методы VB неэффективны. По
этому, рекомендую осваивать GDI. Например для рисования
сразу большого количества линий можно воспользоваться функцией PolyPolyLine,
которая нарисует их все разом. С этим, пожалуй, все.
Работа со строками
Я часто
сталкиваюсь с надобностью перебора и преобразования строк. Здесь важно
следующее.
Для поиска
фрагмента в строке не пишите свою функцию – используйте встроенные функции VB InStr и InStrRev.
Для поиска и
замены есть функция Replace.
Если все же
пришлось делать перебор, помните следующее.
Вам наверняка
придется строить новую строчку из символов старой. И вы наверняка это сделаете
вот так:
Не делайте
так, Если строчка St может стать длинной. Получается, что при каждом добавлении VB копирует строчку St саму
в себя, параллельно приписывая Char. Когда строчка становится большой, эта
штука начинает жутко тормозить.
Предлагается
следующее решение проблемы. Место для строчки выделяется изначально – как
правило, перед перебором известно, какую длину не может превзойти строчка,
которую надо склеивать. Тогда можно сделать так:
Sub
…
Dim
i As Long
Dim
cp As Long
Dim
St As String
Dim
Char As String
cp
= 1
St
= Space$(Len(InSt))
For
i = 1 To Len(InSt)
…
GoSub AddChar
…
Next
i
…
St
= Left$(St, cp-1)
Exit
Sub
AddChar:
Mid$(St,
cp, Len(Char)) = Char
cp
= cp + Len(Char)
Return
End
Sub
|
Т.е. надо
заполнить Char тем, что надо приклеить, и написать GoSub AddChar.
Это будет работать сильно быстрее, особенно с большими строчками.
Компиляция проекта
Правильно
скомпилировать проект тоже надо уметь. Расскажу о том, какие настройки имеет
компилятор VB6.
Заходите в
“File: Make <filename>.exe”. Появляется окно сохранения. Жмете Options.
Появляется диалог, где вы можете написать информацию о своей программе. Но нам
это сейчас не интересно, по этому щелкайте вкладку Make. Там вы увидите
следующее:
Поясню все
настройки.
Compile to P-Code
– компилировать в П-код. Это значит, ваша программа будет переведена на
некоторый промежуточный язык и записана в exe. Этот exe будет
интерпретироваться Runtime-машиной (MSVBVM60.dll). Откомпилированная так
программа будет работать наиболее медленно, почти как в отладке VB. Но зато она
будет очень маленькой. Заметно меньше, чем в Native-коде. Теоретически
программа может быть перенесена на другую платформу (если вы, конечно, не
использовали WinAPI), если найдется виртуальная машина для новой платформы.
Лично я не рекомендую компилировать в П-код.
Compile to Native
code – компилировать в машинные инструкции. Код программы переводится в
машинные инструкции, и они записываются в exe-шник. Такая программа будет
привязана к архитектуре машин Intel и к платформе Windows, что не позволит
переносить ее на другие платформы. Но так как я никогда не слышал о попытках
перенести VB-программу на другую платформу, а по сему лучше компилировать в Native-код,
так как он дает преимущество в скорости вплоть до двадцатикратного (см. тест
производительности, плавающая точка – Native код может быть быстрее в 20.5 раз).
Программы,
откомпилированные в Native Code, тяжелее программ на П-коде. Но ради скорости
все же я всегда компилирую в Native код, чего и вам советую.
Об
оптимизациях:
Optimize for Fast
Code – оптимизировать программу для скорости. Как написано в документации, если
компилятор найдет два способа перевода в машинных код, он переведет в тот, что
будет быстрее. Но это не совсем так. Смотрите результаты тестов с плавающей
точкой – там по неизвестным мне причинам выиграла оптимизация на маленькую
программу, а не на скорость.
Короче,
рекомендую для большинства случаев.
Optimize for Small
Code – оптимизировать размер. Опять, если компилятор найдет несколько вариантов
реализации, он выберет ту из них, что меньше по размеру. Но реально эти две
оптимизации мало чем отличаются, и размер, и производительность меняются не
сильно.
No Optimization
– отключить оптимизацию. Не рекомендую. Включайте эту опцию, если
неоткомпилированная программа работает, а откомпилированная – нет. Но у меня
еще не было случаев, что это помогало. Как правило, причины другие.
Favor Pentium
Pro(tm) – оптимизация вашей программы для работы на процессоре Pentium Pro. Был
когда-то такой процессор. Но я пробовал и с ней, и без нее – почти никакой
разницы.
Create Symbolic
Debug Info – не делает ничего с собственно программой, а лишь создает
дополнительный файл, содержащий информацию для отладки вашей программы во
внешнем отладчике (файл .pdb).
Теперь нажмите
Advanced Optimizations – посмотрим, что у нас там.
В первой
строке написано, что если вы включите эти оптимизации, ваша программа может
перестать работать. Эта чистая правда. Чтобы иметь возможность включить эти
оптимизаторы, вам необходимо соблюдать некоторые требования при кодировании.
Assume No Aliasing
– дает вашей программе возможность передавать аргументы функций через регистры
процессора. Однако если вы включаете этот оптимизатор, вам необходимо
убедиться, что у вас нет конструкций такого рода:
Sub Foo(x As Integer, y As Integer)
x = 5 'доступ к одной и той же переменной
y = 6 'через разные имена
End Sub
Sub Main
Dim z As Integer
Foo z,z
End Sub
|
Как правило,
включение этого оптимизатора к неправильным результатам не приводит, но следует
иметь это в виду. А выражение Aliasing в данном случае означает доступ к одной
области памяти через разные имена. Никакой связи с графикой.
Remove Array Bounds
Checks – не включать в код программы логику автоматической проверки границ
массивов. Если этот флаг не отмечен, VB перед каждой операцией с данными в
массиве проверяет, не выходят ли индексы за границы массива. Если ваша
программа использует ошибку Subscript Out Of Range, не включайте этот
оптимизатор. Если вы это сделаете, у вас вместо нормальной ошибки получится Access
Violation, и программа будет снята со счета. Тут на помогут никакие On Error… Но
зато этот оптимизатор жутко ускоряет работу с массивами. Подробнее о том, что
можно и чего нельзя, будет сказано дальше, в Работе с массивами.
Remove Integer
Overflow Checks – не проверять переполнения при целочисленных операциях. Здесь
все просто – если вы выйдите за границы допустимых значений, вы получите
неправильный результат. Никакого Overflow-а. Естественно, это относится только
к целочисленным операциям, плавающая точка будет работать так же, как раньше.
Remove Floating
Point Error Checks – аналогичная опция, но для плавающей точки. Если не
отмечена, VB проверяет, не вышел ли результат за границы и не пытается ли ваша
программа делить на ноль. Так он выдаст ошибку (переполнение или деление на
ноль). Но если опция включена, никакой ошибки не будет, а результат окажется
неверным.
Allow Unrounded
Floating Point Operation – разрешить сравнение двух чисел разных типов, не
округлив их предварительно. Это может привести к тому, что условие, которое
должно бы выполняться, не выполнится.
Вообще говоря,
сравнивать числа с плавающей точкой вот так: If a = b Then … не следует, так как
все операции с плавающей точкой производятся приближенно и вероятность того,
что два результата совпадут точно, не велика. По этому, для проверки равенства
рекомендуется сравнивать их, введя некоторый допуск. Вот так: If Abs(a - b) < Delta
Then … Здесь Delta
– какая-либо константа, определяемая той точностью, которая вам необходима в
вашей задаче. Таким образом, если a отличается от b меньше чем на Delta, они принимаются равными.
Этот
оптимизатор влияет только на сравнение, где сравниваются типы разной точности (Single и Double).
Если флаг не отмечен, оба значения сначала округляются до Single, а потом
сравниваются. Если этот флаг отмечен, то сравнение производится без округления.
Remove Safe Pentium
FDIV Checks – убирает специальную реализацию вещественного деления из
программы. У некоторых процессоров Pentium есть ошибка в инструкции FDIV, в
результате чего при вещественном делении может получиться немного неправильный результат.
Если эта опция включена, компилятор использует для вашей программы инструкцию FDIV,
в результате чего производительность программы повысится, но на процессорах с
ошибкой FDIV результаты могут быть немного другими.
Работа с массивами
Здесь я опишу,
чего следует остерегаться, когда включен флаг Remove Array Bounds Checks (далее
RABC).
Остерегаться
стоит обращение к неправильному индексу в массиве. Опишу некоторые случаи.
- Обращение к любому элементу пустого массива.
- Обращение к индексам за границами.
- Использована неправильная размерность.
При этом к
немедленному Access Violation привели только последние два.
Попытка
считывания из пустого массива в режиме отладки вызывает VB ошибку Subscript out
of range. А откомпилированная версия (я под откомпилированной дальше буду иметь
в виду откомпилированную в Native-код с отмеченным Remove Array Bounds Checks)
дала ошибку Object variable or
With block variable not set. Причем в связи с тем, что
эта ошибка возникала нерегулярно, можно прийти к выводу, что полагаться на эту
ошибку нельзя.
И вот,
кстати, возникает вопрос, как определить, пуст ли массив. Можно, в принципе
использовать функцию UBound,
но она тоже возвращает Object variable or With block variable not set в
скомпилированной программе при действии на пустой массив. И хотя я не наткнулся
на отсутствие возникновения ошибки, судя по ее поведению, она тоже может быть
нерегулярной. По этому, я не рекомендую полагаться на эту ошибку. Но как же
все-таки определить, пуст ли массив?
Я так и не
нашел способа это сделать стандартными способами VB. И предлагаю два решения
этой проблемы.
Первое
приходит сразу. Вместе с массивом таскать за ним его длину. Но этот метод не
надежен, так как вы спокойно можете забыть изменить этот номер.
Второе –
решение, основанное на знании того, как VB хранит массивы. Предлагаю процедурку
для определения размерности массива.
Private Declare Sub CopyMemory Lib " kernel32" Alias " RtlMoveMemory" _
( Destination As Any, _
Source As Any, _
ByVal Length As Long)
Private Declare Function AryPtr Lib "msvbvm60.dll" Alias "VarPtr" (Ary() As Any) As Long
Private Type SafeArrayBound '8 bytes
cElems As Long '4
lBound As Long '4
End Type
Private Type SafeArray
' length offset
cDims As Integer '2 0
fFeatures As Integer '2 2
ElemSize As Long '4 4
cLocks As Long '4 8
PtrData As Long '4 12
Bounds(0 To 1) As SafeArrayBound '16 bytes 16
End Type
Private Function AryDims(ByVal ptrAry As Long) As Long
Dim PtrStruc As Long
Dim SA As SafeArray
If ptrAry = 0 Then
Err.Raise 1111, "AryDims", "Некорректный указатель на массив . Используйте AryDims(AryPtr(ary))"
End If
CopyMemory PtrStruc, ByVal ptrAry, 4
If PtrStruc = 0 Then
AryDims = 0
Else
CopyMemory SA, ByVal PtrStruc, 2
AryDims = SA.cDims
End If
End Function
|
Поясню, что
делает эта процедурка и как организованы массивы в VB.
Функция AryPtr возвращает указатель на то место, где хранится указатель на
заголовок массива. То есть, если понимать идентификатор массива как переменную,
то в ней записан адрес, по которому надо искать заголовок массива. А вызвав
функцию VarPtr(массив),
я получил указатель на указатель на заголовок массива. В заголовке массива
хранятся размеры массива, указатель на данные и еще кое-какая информация. Для
того, чтобы узнать немного о том, что есть в заголовке массива, поищите в MSDN в индексе слово «SAFEARRAY».
Таким
образом, определение, пуст ли массив, выглядит так:
If AryDims(AryPtr(MyAry())) = 0 Then
‘Массив пуст
End If
|
У этой
функции есть один большой недостаток – она не работает с массивами строк. А
опция RABC влияет в том числе и на массивы строк. Что в данном случае делать с
массивами строк, я не знаю, по этому мне приходится таскать размер массива.
Итак, обобщу
все, что так сложно написал:
Если вы включили
флаг Remove Array Bounds Checks, вы должны следить:
А) при работе с элементами:
- за тем, что массив не пуст.
- за размерностью.
- за границами.
Б) при использовании LBound/UBound:
- за тем, что массив не пуст.
- за размерностью.
За всем
остальным следить не надо – все случаи ReDim работают
хорошо. Копирование массивов также не вызывает никаких проблем.
А чтобы
определить, пуст ли массив, нужно либо таскать за каждым массивом его длину,
либо использовать функцию AryDims, код которой приведен выше.
Примечание.
Флаг RABC не влияет на работу массивов элементов управления. Т.е. на то, что
называется Control Array. Отмечу еще, что массив объектов не есть Control Array,
а просто обычный массив, и на него тоже действует флаг RABC.
Некоторые
полезные советы.
Если вам надо
откопировать массив, не пишите цикл, а просто напишите так:
Это хоть и
будет копировать данные из массива в массив, но все же быстрее, чем в обычном
цикле.
Заключение
В общем, по
большому счету производительность программы очень сильно зависит от того,
насколько грамотно написан код. Но все же не стоит забывать, что во многих
случаях даже с некрасивым и неграмотным кодом можно достигнуть приемлемой
производительности просто за счет алгоритма.
Еще очень
сильное впечатление пользователя может отложиться по тому, как программа
докладывает о своем состоянии. То есть, если программа задумалась, она должна
нарисовать прогресс бар, или сделать указатель часиками. Еще не плохо
сопроводить эти действие информацией о том, в какой стадии оно находится и что
сейчас происходит. Если уж действие длится совсем долго, обязательно сделать
возможность отмены. Можно еще показывать полезные советы, чтобы пользователь не
скучал.