Введение
Переход на платформу .NET потребовал от разработчиков Visual Basic титанических усилий по его приведению в соответствие со спецификацией CLS. Изменения коснулись практически всех средств языка, но наиболее существенные из них произошли в типах данных и работе с ними. Если кто-то из вас еще сомневается в новизне VB .NET, то после прочтения этой статьи все сомнения должны рассеяться: VB .NET - это действительно новый язык, позаимствовавший от дедушки Basic синтаксис, но не более того. В этой статье я попытаюсь рассказать об основных изменениях в работе с данными и, кроме того, сравнить скорость работы старого VB6 и нового VB .NET при обработке переменных различных типов.
Типы данных в VB .NET
Каждый язык, входящий в "сообщество .NET", полностью поддерживает все типы данных .NET Framework, иначе этот язык не может считаться .NET-языком.
На что хочется обратить ваше внимание? Я считаю, что одним из наиболее существенных изменений в работе с переменными в VB .NET является возможность включения жесткой проверки типов оператором Option Strict. При включении такого режима компилятор требует, чтобы все преобразования типов, которые могут привести к потере данных, выполнялись явно, т.е. с помощью функций явного преобразования CBool, CByte, CDate и т.п. Например, если вы преобразуете число типа Long в число типа Integer, то компилятор выдаст предупреждение, а если наоборот, т.е. Integer в Long, то преобразование может быть выполнено автоматически.
Существует множество других изменений в работе с данными различных типов. VB .NET поддерживает 13 типов данных.
Логические величины Boolean
Основное отличие в переменных этого типа - то, что теперь они занимают в два раза больше памяти, чем раньше: 4 байта, а не 2. Честно говоря, после изучения всех изменений в размерах переменных, я начал понимать, куда новые версии Windows, а также новые версии большинства программ тратят память моего компьютера. Именно из-за увеличения размеров практически всех типов данных и возрастают потребности в оперативной памяти. Однако, может быть, в будущем это позволит с наименьшими затратами расширять возможности языка.
Что же еще изменилось в работе с логическими данными? Да, пожалуй, больше ничего, хотя многие программисты ожидали изменений в работе "логических" операторов (And, Or и т.д). Более того, в первой бета-версии языка его разработчики попытались сделать то, что уже давно необходимо. Что же нам нужно? А было нужно, на мой взгляд, немного.
Давайте вспомним, как работает VB с логическими величинами. Представим себе, что вы проверяете результат выполнения функции, например InStr, которая возвращает 0 (по логике это False) , если подстрока не найдена, или ее позицию в строке, если она найдена, т.е. любое число больше 0 (по логике это True). Но, посмотрите на следующий пример:
If InStr(1, "Журнал
Программист", "Программист")
And_
InStr(1, "Журнал
Программист", "Журнал")
Then
Console.WriteLine("True")
Else
Console.WriteLine("False")
End If |
Рассуждая логически, если первое выражение "истина" и второе тоже "истина", то операция And также должна давать "истину"... Но только не в Visual Basic! Вы никогда не получите логически предсказуемый результат в данном случае. Первый вызов InStr вернет 8, второй 1 и при выполнении операции And вы получите не что иное, как False! Почему? Все очень просто, оператор And (и Or, кстати, также) выполняют не логическое сравнение, а поразрядную операцию, а результат в данном случае получается 0, т.е.
False.
В первой бета-версии был сделан шаг навстречу логике: операторы были переписаны и стали логическими. Кроме того, появились новые операторы для поразрядных действий, например BitAnd. Однако уже во второй бете все в спешном порядке убрали, и в результате вместо логических операторов мы вновь получили поразрядные.
Вот как нужно писать вышеприведенный код в Visual
Basic:
If CBool(InStr(1,
"Журнал Программист",
"Программист")) And_
CBool(InStr(1,
"Журнал Программист",
"Журнал")) Then
Console.WriteLine("True")
Else
Console.WriteLine("False")
End If |
Вывод: при работе с переменными Boolean по-прежнему нужно очень осторожно проводить их сравнение, а точнее, никогда не применять сравнение, не преобразовав полученный результат в переменную типа Boolean, например, с помощью функции CBool. Только так вы можете застраховаться от "нелогического" мышления Visual
Basic.
Целочисленные типы
В новой версии языка есть четыре целочисленных типа данных: Integer, Short, Long и Byte. Следует внимательно отнестись к тому, что теперь тип Integer - это 4-байтовое число, т.е. он соответствует старому типу Long, а вот Long становится 8-байтовым. Что это означает на практике? Дело в том, что если раньше при вызове функции API вы писали Long, то теперь в тех же ситуациях нужно применять
Integer.
Очень существенным изменением является то, что все типы данных обладают методами и свойствами. Например, содержат метод ToString, который можно использовать для преобразования числа в строковую переменную с одновременным ее форматированием в соответствии с заданным форматом. Но вот вопрос: нужно ли? Что это может дать программисту? Конечно, неоспоримо то, что это позволяет сделать ваш код более читабельным, однако меня интересует другой вопрос - не скажется ли это на производительности программы? Если вы читали мою статью "Оптимизация кода в Visual
Basic" 1, то видели, насколько объектно-ориентированный код снижал производительность в VB6. Так вот давайте проверим, как изменится производительность при использовании функции Format и метода ToString. Более того, я позволил себе некоторое пижонство и написал подобные тесты для компилятора VB6. Результаты тестов приведены в таблице 1.
Компилятор |
Метод преобразования |
Быстродействие, % |
VB .NET |
метод ToString |
0 |
VB .NET |
функция Format |
- 40 ... - 50 |
VB 6 (компилятор) |
функция Format |
- 100 ... - 110 |
VB 6 (псевдокод) |
функция Format |
- 110 ... - 120 |
Таблица 1.
Можно сделать следующие выводы. Во-первых, наблюдается заметный рост производительности при преобразовании (форматировании) данных в VB .NET, по сравнению с VB 6. Кроме того, при использовании внутренних методов библиотеки классов .NET Framework можно получить код, работающий более чем в 2 раза быстрее, чем подобный код компилятора VB 6, однако для этого нужно хорошо знать .NET Framework и использовать все ее преимущества. Для тех, кто в этом еще сомневается, ниже я приведу результаты работы VB .NET со строками.
Типы данных с плавающей запятой
Для работы с вещественными числами в VB .NET имеется три типа: Single, Double и Decimal. Первые два вы должны хорошо знать еще со времен VB, а вот Decimal - новый тип, и по заверениям Microsoft, именно этот тип должен заменить старый Currency. Я пока еще не совсем представляю, зачем нужно было увеличивать "денежный" тип до 12 байт, но полагаю, что доходы Microsoft уже не поддаются исчислению в 8 байтах.
Итак, теперь деньги нужно считать в 12 байтах, а вот Single и Double какими были, такими и остались. Отмечу, что эти типы данных также имеют свойства и методы, как и целочисленные типы. Например, вам могут быть полезны такие их свойства, как MaxValue и MinValue, позволяющие узнать верхнюю и нижнюю границы диапазона допустимых значений.
Обратите внимание на тип данных Double. Это нужный и полезный во всех отношениях тип данных. Почему? Все очень просто. Библиотека классов .NET Framework устроена таким образом, что практически все математические функции класса Math возвращают значения именно этого типа. Это означает, что, используя этот тип в своей программе, вам не придется тратить время на преобразование типов из Double в тот, который используете вы. Более того, многие математические функции VB .NET также возвращают результаты работы именно этого типа.
Строки String и Char
Насчет строк у меня две новости, одна плохая, а вторая еще хуже. И это не шутка. Но начнем по порядку. VB 6 поддерживал особый тип строк, которые назывались строками фиксированной длины. Такие строки в отдельных случаях обладали целым рядом преимуществ по сравнению с обычными строками. Одним из главных преимуществ было то, что компилятор сразу выделял под них нужный нам объем памяти, который к тому же брался из стека. При изменениях самой строки память под нее не перераспределялась, что в некоторых случаях, хотя и крайне редких, давало преимущество в скорости работы. Теперь в VB отсутствует понятие строк фиксированной длины, и все строки являются динамическими. Но даже не это главное, гораздо важнее другое изменение - теперь строки "неизменны". Иными словами, если вы хотите изменить значение строковой переменной, то VB .NET вместо модификации старого экземпляра строки создает совершенно новую строку, а старая удаляется при следующем вызове сборщика мусора. Кроме типа String существуют переменные типа Char, которые используются для работы с Unicode-символами. Переменная Char представляет собой 16-битовый код Unicode-символа.
Итак, в новой версии Visual Basic .NET вы можете для работы со строками использовать обычные переменные типа String, либо воспользоваться классом System.Text.StringBuilder из .NET Framework для создания и модификации строки текста. Чем они отличаются? Как раз тем, что StringBuilder при модификации строки не создает новую, а модифицирует исходную.
Технические подробности. Если углубиться в техническую сторону дела, то процесс выделения памяти для строки выглядит примерно так. При объявлении переменной, например так: Dim sText As New
System.Text.StringBuilder() система выделяет для нее память в размере, достаточном для хранения 16 символов. Как только вам потребуется строка, состоящая из 17 символов, размер выделенной памяти автоматически увеличится на ту же величину. Класс System.Text.StringBuilder имеет также в своем арсенале замечательное свойство
Capacity, с помощью которого вы можете переопределить шаг увеличения памяти. Например, присвоив этому свойству, значение 64, вы заставите систему увеличивать размер памяти под строку на величину, достаточную для хранения не 16, а 64 символов. Иногда это очень важно, поскольку позволяет сэкономить время, но не следует злоупотреблять этим свойством - это может привести к нерациональному использованию памяти. |
Естественно, поэтому StringBuilder работает гораздо быстрее, но вы даже не представляете, насколько. Кроме того, вас, конечно, интересует, как изменилась скорость работы VB со строками при переходе на платформу .NET, поэтому я все проверил и собрал полученные данные в таблице 2.
Компилятор |
Тип строки |
Метод |
Быстродействие, мс на 10 000
операций |
VB .NET |
класс System.Text.StringBuilder с предварительным
выделением памяти |
Append |
6,7 |
VB .NET |
класс System.Text.StringBuilder |
Append |
9,6 |
VB .NET |
простая строка типа String |
& |
3413 |
VB 6 (компилятор) |
простая строка типа String |
& |
766 |
VB 6 (псевдокод) |
простая строка типа String |
& |
832 |
Таблица 2.
Думаю, что таблица требует некоторых пояснений. При тестировании я решил использовать простейшую операцию работы со строками: присоединение нового символа в конец строки. System.Text.StringBuilder с предварительным выделением памяти означает, что мы при вызове конструктора строки заранее указываем ее размер и, естественно, память под нее больше не перераспределяется. Как видно из таблицы, это заметно повышает быстородействие.
Результаты меня удивили. Класс System.Text.StringBuilder работает с потрясающей скоростью, а вот обычный строковый тип String, которым мы так привыкли пользоваться... Хотя если подумать, то примерно так и должно быть: создавать при любом изменении переменной новую ее копию - не лучший выбор, даже если это позволяет более эффективно расходовать память.
Вывод здесь может быть только один: если вы собираетесь часто изменять какую-то строку, используйте для этого класс System.Text.StringBuilder, он в сотни раз быстрее обычной переменной типа String, причем, уверяю вас, преимущество в скорости с увеличением размера строки только возрастает.
Date и Object
Увеличение разрядности произошло и у типа Date, хотя и 4 байтов раньше вполне хватало. Но теперь дата представлена в 8 байтах. Кроме того, изменение разрядности повлекло невозможность автоматического преобразования типа Date в Double, как это было раньше. Поэтому вычислить завтрашнюю дату, прибавив к переменной единицу, не получится. Но не стоит расстраиваться, так как в .NET Framework существует класс Convert, с помощью которого вы без труда выполните необходимые преобразования.
Бывалых VB-программистов уже пугали, что Visual Basic больше не поддерживает тип данных Variant. Ну и пусть, потому что теперь есть новый тип данных, который еще лучше и гибче. Он называется Object. По заверениям Microsoft, новый тип обладает рядом преимуществ, одним из которых является улучшенная производительность. За счет чего это стало возможным? По их мнению, это связано с тем, что теперь с технической точки зрения Object - это ссылка на область памяти, в которой содержатся произвольные данные, включая и тип самих данных для идентификации. В случае если Object меняет свой тип, VB .NET просто переставляет ссылку на другую область памяти. Казалось бы, все просто и правильно, но... я никому не верю и всегда проверяю теоретические рассуждения на практике. По моему мнению, тип Object работает еще медленнее, чем старый Variant! И дальше я это докажу по крайней мере на математических операциях.
Производительность
Переменные различных типов
Мне было очень интересно узнать, переменные каких типов работают наиболее эффективно с точки зрения производительности. Я не считаю себя большим специалистом в области тестирования, но все же написал тесты для подобной проверки. Кроме того, мне было интересно, насколько производительность в VB .NET отличается от производительности в VB6.
Для тестирования я написал по три теста для каждого типа переменных, это работа переменных в цикле в качестве счетчика, математические операции сложение/вычитание/умножение/деление и операция преобразования переменной в тип String (лучше ничего не придумал). Измерение скорости я производил с помощью API функции timeGetTime из мультимедиа-библиотеки winmm.dll. При компилировании тестов я отключал оптимизацию и в VB 6, и в VB .NET. Тестирование провел в операционной системе Windows 2000 SP2. Тесты для VB6 и VB .NET были аналогичны, поэтому допускают непосредственное сравнение результатов. Все тесты проводились 5 раз, и их средние результаты (в миллисекундах) приведены в таблице 3 (чем меньше значение, тем быстрее выполнен тест). Нужно сразу отметить, что некоторые результаты расходятся с теорией и ответа на вопрос "Почему?" у меня нет.
Переменная/действие |
VB .NET |
VB 6 p-код |
VB 6 компилятор |
Работа в цикле |
|
|
|
Integer |
140 |
9643 |
9273 |
Long |
280 |
9464 |
9033 |
Byte |
160 |
9544 |
9102 |
Variant |
- |
11015 |
13583 |
Currency |
- |
9433 |
12918 |
Double |
1953 |
10475 |
9604 |
Single |
1031 |
10505 |
9528 |
Short |
171 |
- |
- |
Object |
25757 |
- |
- |
Decimal |
13409 |
- |
- |
Математические операции |
|
|
|
Integer |
2580 |
9430 |
2884 |
Long |
6935 |
7090 |
2433 |
Byte |
2782 |
14962 |
3615 |
Variant |
- |
15071 |
10125 |
Currency |
- |
10275 |
5368 |
Double |
90 |
6159 |
2003 |
Single |
80 |
6329 |
2002 |
Short |
2664 |
- |
- |
Object |
33688 |
- |
- |
Decimal |
9153 |
- |
- |
Преобразование в String |
|
|
|
Integer |
2093 |
10182 |
9982 |
Long |
2037 |
10723 |
9805 |
Byte |
2043 |
10026 |
9786 |
Variant |
|
17702 |
15702 |
Currency |
|
8230 |
8158 |
Double |
3690 |
10207 |
10158 |
Single |
3355 |
10346 |
10180 |
Short |
2068 |
- |
- |
Object |
3059 |
- |
- |
Decimal |
3515 |
- |
- |
Таблица 3.
По результатам тестирования однозначно ответить на вопрос "что быстрее" нельзя. Дело в том, что на одних операциях VB .NET намного быстрее своего предшественника, а на других, например, в операциях умножения/деления, немного отстает, хотя это отставание очень небольшое. В целом VB .NET все же гораздо быстрее, чем VB 6. Я бы сказал так: "VB .NET опережает по скорости работы не только p-код VB 6, но и компилятор шестой версии, причем увеличение быстродействия на некоторых операциях достигает десятков раз".
Если посмотреть, какие типы переменных имеют наиболее быструю математику, то по логике такими переменными должны быть Integer, как соответствующие разрядности операционной системы, но в моих тестах на первое место по скорости работы вышли (кто бы мог подумать) Single и Double. Честно говоря, объяснения этому факту у меня нет. А вот при работе в цикле в качестве счетчика самые быстрые переменные типа
Integer.
Я намерено привел результаты только комплексного математического теста, а не отдельно по каждой из его составляющих, потому, что заметил очень большое различие в производительности переменных в разных операциях. Так, например, умножение/деление переменные типа Single и Double выполняют гораздо быстрее, чем другие типы переменных, а вот на операциях сложение/вычитание такого преимущества они уже не имеют и скорость их работы практически не отличается от Integer. Если кому-то интересны подобные результаты, провести необходимые тесты нетрудно.
Я же хочу сказать еще вот что: при оптимизации критичного по времени кода не нужно верить другим программистам и следует проверять работу своего кода, используя переменные разных типов. Только так вы сможете выбрать наиболее быстрые переменные для своего случая. И, поверьте, теория и практика здесь сильно расходятся.
Проводя тестирование, я использовал простейшие математические операции, потому что, используя математические функции VB .NET, возвращающие результаты в Double, я бы сразу поставил в неравные условия другие типы данных, поскольку результат бы еще преобразовывался из Double в используемый тип.
Говоря о переменных типа Object, я могу сделать такой вывод: математика у него стала еще хуже, чем была раньше у Variant. Если на математических операциях Variant в VB 6 в среднем отставал совсем немного (причем основное отставание было на операциях сложения/вычитания), то теперь Object отстает в десять и более раз, и это грустно.
Общие проблемы производительности в VB .NET
Проблема производительности в VB .NET выходит далеко за рамки этой статьи, но я немного расскажу о самой большой проблеме - это "компиляция по требованию". Что это означает? Вы уже знаете, что JIT-компилятор не выполняет компиляцию всего MSIL-кода вашей программы при ее запуске. Вместо этого каждый метод компилируется при первом обращении к нему, на что, естественно, требуется время. Откомпилированный код хранится в памяти, а последующие обращения к нему выполняют уже откомпилированный код. Более того, первые обращения к методам, функциям и свойствам классов .NET Framework, выполняются также значительно медленнее, чем их последующие вызовы.
Вам, конечно, хочется узнать, насколько это все медленнее? Так вот, например, первый вызов метода ToString на моей тестовой системе прошел почти за 100 миллисекунд, тогда как последующие вызовы просто не поддаются измерению! Но если вы думаете, что первое обращение к VB .NET функции Format пройдет, как и в VB 6, практически мгновенно, то вы глубоко заблуждаетесь, первое обращение к этой, встроенной в VB .NET функции, занимает не меньше, а, иногда, несколько больше времени, чем к подобному методу из .NET Framework! Однако и здесь не все так плохо, обратившись однажды в программе, например, к классу TimeOfDay и "прочитав" одно из его свойств, при следующем вызове любого метода этого класса или обращению к любому из его многочисленных свойств вы больше не потеряете ни одной миллисекунды. Это я прошел не по учебникам.
Есть ли выход? Конечно! Microsoft, вместе с .NET Framework, поставляет специальный компилятор, с помощью которого вы можете обработать свою сборку и получить машинный код, производительность которого значительно выше. Кроме того, этот компилятор загружает вашу откомпилированную сборку в специальный системный кэш. Например, я после обработки этим компилятором своего кода получил результаты, при которых первые вызовы функций VB .NET и .NET Framework прошли практически мгновенно и ничем не отличались от их последующих вызовов. Так что если вас сильно огорчает "компиляция по требованию", то этот компилятор должен стать вашим другом.
Но не стоит злоупотреблять этим методом, так как сильно загрузив системный кэш, можно даже немного снизить общее быстродействие системы. Стоит также отметить, что такого рода предварительная обработка сборки должна проводиться на компьютере клиента, т.е. заранее откомпилировать код для другого компьютера, скорее всего, не получится, так как компилятор генерирует код, зависящий от конкретной платформы. Иными словами (по крайней мере, именно в этом убеждает нас Microsoft) ваш код должен работать не только на процессорах Intel, но и других, а откомпилировав его, вы потеряете такую возможность.
Заключение
В общем, можно заключить, что изменения языка явно пошли ему на пользу и принесли не только новые возможности, но сделали его более быстродействующим, чем предыдущая версия. Однако максимальный выигрыш в производительности в ваших программах может быть получен только при "умном" использовании всей мощи библиотеки классов .NET Framework, иначе можно получить код, который будет работать с черепашьей скоростью.
Однако многие усовершенствования языка принесли ему не только новые возможности, но и новые проблемы, которые еще очень долго будут оставаться неразрешенными. Пройдет немало времени, прежде чем "пытливые умы" программистов на все 100% поймут внутреннюю структуру .NET Framework и напишут подробные рекомендации по оптимизации кода. Если же у кого-то имеется другое мнение по этому вопросу, то я готов с ним поспорить.
1 Журнал "Программист" №11/2001.
Статья опубликована в журнале "Программист"
№7 за 2002 год.