ТЕСТИРОВАНИЕ ДИНАМИЧЕСКИХ МАССИВОВ
              С АВТОМАТИЧЕСКОЙ ПРОВЕРКОЙ ИНДЕКСА
                               
                         Рауль Шакиров
                 Институт машиноведения УрО РАН
                       raul@imach.uran.ru
                               
     Проходящее десятилетие,  без преувеличения, можно назвать
декадой  новых инструментов  программирования. Эта особенность
в  полной  мере отражается на  страницах массовой компьютерной
печати. Относительно меньшее внимание при этом уделяется таким
вопросам  теории  и технологии  программирования, как проблемы
создания  корректно  работающих  и  мобильных  программ. Такое
распределение  приоритетов  создает   "социальный  заказ"  для
разработчиков инструментария - ориентироваться прежде всего на
реализацию  новых  функциональных   возможностей,  а  проблемы
корректности и мобильности оставлять на  усмотрение прикладных
программистов.  Разумно это или нет?  Не вдаваясь в  детальное
обсуждение  этого   вопроса,   отметим,   что  даже  частичная
инструментальная  поддержка  могла  бы  существенно  облегчить
разработку  корректных  программ.   Один   из   методов  такой
поддержки заключается  в  реализации  динамических  массивов с
автоматической проверкой индекса.
     
     1. Автоматическая проверка индекса
     
     Возможность автоматической проверки индекса при обращении
к элементу  массива  предусмотрена  в  ряде  языков программи-
рования,  из которых  наиболее известным является Java. Важным
исключением  являются  языки  С  и  С++  -  там автоматической
проверки  индекса нет,  хотя  в  С++  ее  можно  реализовать с
помощью   механизма    шаблонов.   Отсутствие   автоматической
проверки   индекса   означает,    что    программист    должен
гарантировать корректность  индекса.  Если такой гарантии нет,
то  программа содержит  скрытую ошибку,  например,  такую, как
знаменитая ошибка в программе sendmail или  сбой при обработке
длинных MIME-имен в программе Microsoft Outlook.
     Считается, что отсутствие автоматической проверки индекса
имеет  и свои  положительные стороны  -  большую  свободу  для
профессионального программиста и более  высокую производитель-
ность программы.  Справедливость этой точки зрения нуждается в
экспериментальной  проверке,  методика  и  результаты  которой
будут обсуждаться в этой статье.
     
     2. Динамические массивы
     
     В отличие от обычного массива, размер которого фиксирует-
ся при сборке  программы или при создании  массива  оператором
динамического  распределения   памяти,   размер  динамического
массива может меняться в процессе выполнения программы.
     Динамические массивы могут  быть  реализованы,  как часть
языка программирования или в виде класса -  надстройки. Первый
вариант  реализован,   например,  в  системе  программирования
Borland Delphi 4 [1]. Но наиболее известной и хорошо изученной
является реализация динамического массива в классе Vector.
     Класс Vector  предусмотрен  в языке Java  [2]  и  в новом
стандарте  языка С++,  где он  декларирован составе библиотеки
шаблонов  STL  (Standard  template  library)  [3].  Реализация
динамического массива  в  классе  Vector  основана  на технике
хранения   элементов   в   непрерывной   области   памяти,   с
последовательным  удвоением   ее  размеров  по мере заполнения
массива   данными.   В   Табл.1   сравнивается   эффективность
непрерывных  динамических  массивов  и  динамических структур,
использующих размещение элементов в  несмежных областях памяти
- таких, как связный список и двоичное дерево.
-----------------T-----------------T-------------------------¬
¦      Структуры ¦ Динамический    ¦ Динамические структуры  ¦
¦ Свойства       ¦ массив          ¦ с несмежным размещением ¦
+----------------+-----------------+-------------------------+
¦ Время доступа  ¦ Не зависит от   ¦ Пропорционально числу   ¦
¦ к элементу по  ¦ числа элементов ¦ элементов или логарифму ¦
¦ его индексу    ¦                 ¦ числа элементов         ¦
+----------------+-----------------+-------------------------+
¦ Число операций ¦ Пропорционально ¦ Пропорционально числу   ¦
¦ распределения  ¦ логарифму числа ¦ элементов               ¦
¦ памяти         ¦ элементов       ¦                         ¦
L----------------+-----------------+--------------------------
      Табл. 1. Сравнение различных динамических структур
     Реализация   класса Vector  в языке  Java  страдает из-за
ограничений,  свойственных  этому  языку.  Из-за  отсутствия в
языке Java шаблонов  базовый массив класса Vector  содержит не
значения,  а  ссылки на объекты,  расположенные в динамической
памяти,  что снижает производительность  массива и  приводит к
повышенному расходу памяти  и увеличивает нагрузку  на процесс
сборки  "мусора".  Для  обращения  к  элементу  массива нельзя
использовать  привычную операцию  [],  т.к.  в  языке Java эту
операцию нельзя  переопределить  -   вместо этого предлагается
использовать методы ElementAt и SetElementAt. При обращении  к
несуществующему  элементу   массива  возбуждается  исключение.
Размер  массива предлагается  устанавливать вручную  с помощью
метода EnsureCapacity.
     Реализация динамических  массивов  в языке  Java выглядит
шагом  назад даже  по сравнению  с тем,  чего можно добиться в
языке C++. Остается только  сожалеть по поводу того, что столь
неполноценное решение  закрепляется  на  десятилетия  в рамках
популярного языка программирования.
     В классе Vector библиотеки STL, в отличие от его тезки из
языка Java,  вообще не  предусмотрена проверка  индексов, т.е.
программист  не  только  должен   вручную  управлять  размером
массива,  но  и даже  не  может рассчитывать  на предсказуемое
поведение программы в случае ошибочной индексации.  К счастью,
эта  особенность  не  связана  с  какими-либо  принципиальными
недостатками языка С++,  а вытекает из установки разработчиков
библиотеки STL на максимальную производительность программы.
     Реализация  класса  Vector  в  библиотеке  STL отличается
довольно  высокой   эффективностью,  интересными  техническими
идеями и внушительным объемом программного  кода  - более 7000
строк только в заголовочных файлах.
     
     3. Динамические массивы с автоматической проверкой индекса
     
     Как упоминалось в  п.1,  автоматическая  проверка индекса
при обращении к элементу массива уже реализована в языке Java,
При выходе индекса за границы массива  виртуальная машина Java
возбуждает исключение.  Возможность автоматического увеличения
размера массива не предусматривается, т.к. предполагается, что
размер массива должен задавать программист.  По мнению автора,
такое решение является половинчатым  -  если уж создатели Java
пошли на накладные расходы, связанные с проверкой индексов, то
почему бы  за  счет  этого  не  облегчить  работу программиста
вместо того,  чтобы просто его контролировать? Так мы приходим
к  понятию динамического  массива  с  автоматической проверкой
индекса.
     Поскольку язык Java не позволяет реализовать динамические
массивы  удобным   и   эффективным   образом   (см.  п.2),  то
соответствующая реализация проводилась автором для  языка C++.
Результаты разработки представлены в виде  заголовочного файла
exarray.h и вспомогательного файла exarray.cpp.
     Динамический   массив  описывается   шаблоном  exarray  и
создается с указанием типа элементов:
          exarray<тип> имя;
     Например, целочисленный массив vector объявляется так:
          exarray<int> vector;
     Размер массива не  задается.  Вместо этого постулируется,
что  массив  имеет  неограниченное  число  элементов,  которым
первоначально  присвоены  нулевые   значения,  либо  значения,
заданные конструктором класса по умолчанию. Фактически, массив
первоначально имеет нулевой размер,  но при обращении к любому
элементу  размер  массива   автоматически  увеличивается.  При
переполнении  разрядной  сетки  индекса  или  нехватке  памяти
программа завершит работу с выдачей диагностики.  
     Для обращения к элементу массива с целью  его  чтения или
записи применяется стандартная нотация:
          имя [индекс]
     Предусмотрена возможность объявления многомерных массивов
с  неограниченными  размерами  по  всем  измерениям. Например,
двумерная целочисленная матрица matrix объявляется так:
          typedef exarray<int> vector;
          exarray<vector> matrix;
     Если требуется,  то можно объявить комбинированный массив
с фиксированными и динамическими измерениями, например:
          typedef int int5 [5];
          exarray<int5> matrix;
     Для  передачи  динамического  массива  в  функцию  в  ней
объявляется параметр вида:
          exarray<тип>& имя;
     Чтобы облегчить применение динамических массивов, следует
избавить  программиста  от  необходимости  переучиваться.  Для
этого класс exarray  объявлен так,  чтобы перевод программы на
использование  динамических  массивов  можно  было   выполнить
путем  простой замены деклараций,  без необходимости коррекции
исполняемого кода.
      Поскольку во многих программах для обращения к элементам
массивов используются указатели, то в файле exarray.h объявлен
шаблон expoint, реализующий концепцию динамического указателя.
Каждый   динамический  указатель   привязан   к  определенному
динамическому  массиву  и  характеризуется  смещением  в  этом
массиве,  которое разрешается менять с  помощью арифметических
операций.  При  обращении  через  указатель  к несуществующему
элементу размер массива автоматически увеличивается.
     Динамический указатель объявляется как
          expoint<тип> имя;
     и может быть инициализирован динамическим массивом.
     Например, указатель на массив vector объявляется так:
          expoint<int> p = vector;
     Далее можно выполнять привычные  операции  над указателем
типа p [3], p += 3, *p++, p = NULL, логические проверки и т.п.
     Динамический  массив  exarray  размещается  в непрерывной
области памяти,  адрес  которой может  меняться  при изменении
размеров  массива.   Поэтому  адреса  элементов  динамического
массива   не  могут   храниться   в   обычных   указателях,  а
использование для этой цели динамических указателей ограничено
адресацией   элементов   одномерных    массивов   и   старшего
динамического измерения многомерных массивов.
     Указанные  ограничения  можно  снять,  если   реализовать
динамический массив в виде непрерывного  массива указателей на
значения элементов, а сами значения размещать по фиксированным
адресам  в  различных  фрагментах  динамической  памяти. Такое
решение   напоминает   схему,   принятую   в   языке   Java  и
характеризуется невысокой производительностью,  т.к. на каждое
обращение  к  элементу  динамического  массива  приходится два
обращения  к оперативной памяти.  С  одной из  реализаций этой
идеи можно познакомиться в работе [4].
     
     
     4. Тестирование динамических массивов
     
     Для  проведения  сравнительных  испытаний  использовались
программы  перемножения  матриц   matrix1.cpp  и  matrix2.cpp.
Программа   matrix1    написана   в   традиционном   стиле   с
использованием   статических   массивов.    Программа  matrix2
использует    динамические   массивы.    В   ней   применяется
заголовочный файл  exarray.h  и  функция  распределения памяти
exmreloc.cpp,  которые  обсуждались выше.   В  остальном текст
matrix2    идентичен   тексту    matrix1.   Чтобы   обеспечить
достоверность результатов сравнения,  при  разработке программ
matrix1  и  matrix2  не проводилась оптимизация под конкретные
трансляторы или вычислительные устройства.
     Сравнивая программы, отметим, что в программе matrix1 нет
проверки индексов,  поэтому при попытке перемножения  матриц с
размером dim более 500  программа  ведет  себя непредсказуемым
образом.   Другой  недостаток  заключается  в  том,   что  для
увеличения  максимального  размера  матриц  следует  увеличить
размер  статических массивов;  при  этом  выделять увеличенный
объем памяти придется даже при перемножении небольших матриц.
     Указанные недостатки  программы matrix1  можно преодолеть
за  счет  применения   оператора  динамического  распределения
памяти new,  но это  сделает программу более громоздкой,  а ее
алгоритм -  трудно читаемым. По наблюдениям автора, прикладные
программисты    предпочитает    создавать    некорректные    и
расточительные,  но удобочитаемые программы, подобные matrix1.
Интересно  в связи с этим  отметить,  как  стремительно растут
требования к оперативной памяти в современных программах.
     Программа matrix2, при полном сохранении удобочитаемости,
не  обладает  ни  одним  из   недостатков  программы  matrix1.
Оперативная  память  распределяется   автоматически   по  мере
необходимости,  а при ее нехватке программа завершает работу с
выдачей диагностики.
     Программы  транслировались на  Borland C++ Compiler 4.5 с
DOS Power Pack  в  режиме  DPMI  32.  Тесты производительности
проводилось на Pentium-166 MMX,  430TX,  512К L2, 66Mhz SDRAM,
Win 95  OSR2.  Результаты тестов для матриц размером от 100 до
500  приведены в табл.2, где колонки 1 и 2 содержат результаты
для программ matrix1 и matrix2. Колонка 1' содержит результаты
для программы matrix1 после замены  размерной константы DIM на
512.  Колонка 2'  содержит  результаты  для  программы matrix2
после отключения  проверки  индексов во внутреннем  цикле, для
чего  выражение  m1[i][k] * m2[k][j]   заменялось   выражением
m1.item(i).item(k) * m2.item(k).item(j). 
     Для удобства представления результатов время перемножения
матриц  пересчитано  на  однократное  прохождение  внутреннего
цикла (т.е.  поделено на dim в кубе) и указано в наносекундах.
Время ввода-вывода данных не учитывается.
------T--------T---------------------------------------------¬
¦ Раз ¦ Сум-   ¦  Время 1-го прохода внутреннего цикла (нс)  ¦
¦ мер ¦ марный +----------------------T----------------------+
¦(dim)¦ объем  ¦   Без оптимизации    ¦    С оптимизацией    ¦
¦     ¦ матриц ¦      (Default)       ¦    (Fastest Code)    ¦
¦     ¦ (Кбайт)¦   1    1'   2    2'  ¦   1    1'   2    2'  ¦
+-----+--------+----------------------+----------------------+
¦ 100 ¦   120  ¦  235  216  281  132  ¦   97  186  280  128  ¦
¦ 200 ¦   480  ¦  322  326  284  137  ¦  180  288  283  131  ¦
¦ 300 ¦  1080  ¦  328  340  389  239  ¦  183  298  389  235  ¦
¦ 400 ¦  1920  ¦  344  345  425  275  ¦  200  309  424  271  ¦
¦ 500 ¦  3000  ¦  365  351  466  320  ¦  221  315  466  319  ¦
L-----+--------+----------------------+-----------------------         
            Табл.2. Результаты тестов производительности               
                                                                       
     Комментируя  результаты,  следует  прежде  всего отметить         
значительное   влияние  эффектов,  связанных  с  особенностями
архитектуры процессоров Pentium.
      В частности, замена размерного параметра DIM в программе
matrix1 на 512 влияет на производительность двояким образом:
     1) При  обращении  к  элементу  матрицы  вместо медленной
операции умножения выполняется быстрая операция сдвига.
     2) Особенности устройства частично-ассоциативного кэша L1
процессоров Pentium [5] приводят  к тому,  что при обращении к
памяти с шагом 64*n +/- 4 байт эффективная емкость кэша падает
в 2  раза,  при шаге 128*n байт -  в 4 раза и.т.п. Поэтому при
последовательном  обращении  к  m[k][j]   с  шагом  2048  байт
частично-ассоциативный кэш L1  не работает, и значения берутся
из более медленного кэша L2 или даже из оперативной памяти.
     
     Поскольку  два вышеупомянутых  эффекта противоположны, то
результаты тестов 1  и 1' противоречивы. В основном, программа
matrix1  с DIM=500  работает быстрее,  чем  c  DIM=512,  и это
особенно  заметно  в  режиме  с  оптимизацией,  когда операция
умножения заменяется приращением рабочего указателя.
     Динамические  массивы в программе  matrix2  не используют
операцию  умножения,   а  для  оптимизации  загрузки  кэша  L1
применяется оригинальный  алгоритм  распределения  памяти. Еще
один фактор ускорения заключается в том, что подсистема памяти
типа EDO или SDRAM  работает  быстрее  при  плотном размещении
данных   в   оперативной  памяти.   Этот  эффект  заметен  при
перемножении матриц размерности 200.
     Перемножение   матриц  является   хорошим   примером  для
исследования    автоматической    оптимизации   кода.   Эффект
автоматической оптимизации наблюдается в  программе  matrix1 и
отсутствует  в программе matrix2.  Причина заключается  в том,
что  современные  трансляторы  языка  С++  не  приспособлены к
оптимизации кода для динамических  массивов. Чтобы исследовать
эффект  оптимизации  кода  в   случае  динамических  массивов,
проверка  индексов  была  отключена  вручную;  соответствующие
результаты приведены в колонке 2'.
     Подведем итоги тестирования.  Для целей сравнения обычных
и динамических  массивов  в  режиме  без  оптимизации подходят
колонки 1 и 2, а в режиме с оптимизацией по скорости - колонки
1  и  2'.  Судя  по  выбранным  колонкам,  время  перемножения
динамических  матриц  на  процессоре  Pentium  MMX  меняется в
пределах  73-144%  от времени перемножения  обычных матриц при
среднем  соотношении  119%.  Эти  результаты  подтверждаются и
другими тестами.  В частности,  для случая одномерных массивов
было получено  соотношение 120%.  Следует  особо отметить, что
обнаружить  разницу  в  производительности  удается  только на
специально  подобранных примерах  при тщательном тестировании,
иначе  разница  будет  скрыта  более  существенными эффектами,
связанными с архитектурными особенностями процессора и памяти.
     Полученные  результаты  следует считать  специфичными для
компьютеров на базе процессоров Pentuim и Pentium MMX, которые
характеризуются  быстрым вычислительным  ядром  и относительно
медленной   подсистемой    оперативной    памяти.   Результаты
тестирования на компьютерах других типов и  тестовые программы
публикуются на странице http://www.imach.uran.ru/exarray.
     
     5. Возможности ручной оптимизации кода
     
     Реализация    динамических    массивов    позволяет   при
необходимости  отключить  проверку  индексов   и  организовать
доступ к  массиву через обычный указатель.  Поэтому результаты
ручной   оптимизации   кода   зависит   главным   образом   от
квалификации  программиста,  а  не  от  того,  какие массивы -
обычные или динамические - он применяет.
     После  ручной  оптимизации  кода  время  работы  программ
matrix1  и matrix2  уменьшается до 80-100 нс/проход при  любой
размерности  матриц  в   пределах  от  100   до   1000.  Текст
оптимизированного   варианта  matrix2   приведен  на  листинге
matrix3.cpp.  В  этом  варианте  применяется транспонированная
матрица m2,  а критический участок кода выделен в подпрограмму
scalar, использующую обычные указатели.
     
     6. О применении динамических массивов
     
     Одна из областей применения динамических массивов связана
с переводом существующих 16-разрядных  программ в 32-разрядный
режим для увеличения размера обрабатываемых  данных. Во многих
16-разрядных программах используется статическое распределение
памяти  в  пределах   640   КБайт,  доступных  для  MS-DOS.  В
32-разрядном варианте программы желательно  иметь динамическое
распределение  памяти,   зависящее  от  объема  обрабатываемых
данных.  Для  этого  программа  переводится  на  использование
динамических массивов:
     1) Если в тексте программы имеются операторы & или sizeof
применительно к массивам,  то &  удаляется (это тавтология), а
sizeof заменяются размерными переменными или константами. Если
в программе применяются функции calloc,  malloc и free, то они
заменяются на new и delete.
     2) Параметры-массивы  всех   функций   подразделяются  на
входные  и  выходные.   К указателям, соответствующим  входным
массивам,  добавляется модификатор  const (использование этого
модификатора входит в понятие  грамотного программирования, но
многие реальные программисты этим пренебрегают).
     3) Там,  где  это требуется,  обычные массивы и указатели
заменяются на динамические массивы и указатели.
     При  передаче  динамического  массива   или  указателя  в
качестве входного параметра функции выполняется автоматическое
преобразование   к   обычному   const-указателю.    Доступ   к
динамическому  массиву  по  const-указателю   выполняется  без
проверки  индексов  в  пределах  заполненной   части  массива.
Существенный  момент  заключается   в  том,   что  в  процессе
выполнения  функции  входной  динамический  массив  не  должен
менять свой размер -  для этого следует  обеспечить, чтобы при
вызове функции этот же массив не передавался в качестве одного
из ее выходных параметров.
     Если динамический массив передается  в качестве выходного
параметра функции, то соответствующий параметр объявляется как
exarray<тип>&  или expoint<тип>. Первый вариант предпочтителен
с точки зрения производительности.  Для передачи динамического
указателя следует использовать параметр вида expoint<тип>.
     Если  динамический  массив  или  указатель  передается  в
качестве  выходного   параметра   стандартной  функции,  текст
которой недоступен, то создается функция-оболочка, управляющая
размером массива (примеры - strcpy и strcat в exarray.h).
     Если это требуется,  то  с  помощью  механизма перегрузки
можно задать  несколько вариантов каждой  функции с различными
способами передачи параметров.
     Описанная методика была успешно использована для перевода
в 32-разрядный режим ряда небольших прикладных программ.
     
     
     Выводы
     
     Динамические    массивы    избавляют    программиста   от
необходимости заниматься распределением  памяти и обеспечивают
полную  защиту  от  любых  ошибок  индексации,   при  этом  их
эффективность  сопоставима с эффективностью  обычных массивов,
не защищенных от ошибок индексации. Эксперименты на компьютере
с  процессором  Pentuim  MMX  показывают,  что  после перевода
32-разрядной программы на использование  динамических массивов
производительность программы  уменьшается  в  среднем  на 20%,
причем  в  некоторых  случаях  наблюдается  обратный  эффект -
увеличение производительности.
     Предложенную  реализацию  динамических  массивов  следует
считать   предварительной,   т.к.   в  ней  не  обеспечивается
автоматическая   оптимизация   программы,   а   также   выдача
транслятором  удобочитаемых   диагностических  сообщений.  Для
полноценной реализации динамических  массивов требуется, чтобы
их поддержка была  включена  в трансляторы языка C++.  Судя по
тому, что динамические массивы (без автоматического увеличения
размеров)  уже предусмотрены в трансляторе  Borland  Delphi 4,
такое развитие событий представляется весьма вероятным.
     
     Ссылки
     1. Delphi 4 Unleashed - Chapter 2. http://www.inprise.com
/delphi/books/del4unleashed/chapter2/
     2. Патрик  Нотон.   Java.   Справочное  руководство.  M.:
Восточная книжная компания, 1996. 448с.
     3. Rogue Wave. Standard C++ Library User's Guide, Tutorial
and Class Reference. Rogue Wave Software Corvallis, Oregon USA.
     4. Tom Cargill. C++ Gadfly. C++ Report Reprints: Jan 1994
http://www.inprise.com/borlandcpp/news/report/CR9401cargill.html
     5. Intel Architecture Tutorials.
http://www.intel.ru/ contents/design/perftool/cbts