Фундаментальные алгоритмы и структуры данных в Delphi
Шрифт:
Автоматическое выравнивание переменных иногда может ввести программиста в заблуждение. Если, например, объявлен следующий тип record в 32-разрядной версии Delphi, каким будет результат выполнения операции sizeof(TMyRecord)?
type
TMyRecord = record
aByte : byte;
aLong : longint;
end;
Многие без сомнения ответят, что, дескать, 5 байт (и это было бы правильно для Delphi1). Однако верным ответом будет 8 байт. Компилятор автоматически вставит три байта между полем aByte и along, просто чтобы выровнять
Если тип record объявить следующим образом:
type
TMyRecord = packed record
aByte : byte;
aLong : longint;
end;
то функция sizeof(TMyRecord) даст результат 5. Однако в этом случае доступ к полю aLong потребует больше времени, чем в предыдущем примере, поскольку значение поля будет переходить через границу 4 байт. Следовательно, правило можно сформулировать так: если используется ключевое слово packed, поля в записи должны располагаться таким образом, чтобы данные были выровнены по границе 4 байт. Сначала объявите 4-байтные поля, а затем уже все остальные. Это правило применяется во всех кодах, приведенных в настоящей книге. И еще одно, никогда не угадывайте размер записи, а пользуйтесь для этого функцией sizeof.
Кстати, следует знать, что диспетчер динамического распределения памяти Delphi также помогает выполнять выравнивание данных. Он выравнивает не только 4-байтные значения по границе 4 байт, но и 8-байтные значения по границе 8 байт. Это имеет большое значение для переменных типа double: операции с числами с плавающей запятой выполняются быстрее, если переменные типа double выровнены по границе 8 байт. Если задачи по программированию связаны с использованием большого количества числовых переменных, убедитесь, что поля типа double в записях выровнены по границе 8 байт.
Пространство или время
Чем больше мы изучаем, разрабатываем и анализируем алгоритмы, тем чаще мы сталкиваемся с одним универсальным законом вычислительной техники: быстрые алгоритмы, как правило, требуют больше памяти. Таким образом, для использования быстрого алгоритма необходимо располагать большим объемом памяти.
Рассмотрим это на простом примере. Предположим, что требуется разработать алгоритм, который бы определял количество установленных бит в байте. Первый вариант алгоритма показан в листинге 1.3.
Листинг 1.3. Первоначальная функция определения количества установленных битов в байте
function CountBitsl(B : byte):byte;
begin
Result := 0;
while (B <> 0) do
begin
if Odd(B) then
inc(Result);
B := B shr 1;
end;
end;
Как видите, в этой функции не используются промежуточные переменные. Она просто считает установленные биты путем деления значения на 2 (сдвиг целого значения на один бит вправо эквивалентно делению на 2) и определения количества полученных нечетных результатов. Цикл завершается, когда будет получено значение 0, поскольку в этом случае очевидно, что установленных битов больше нет. Значение О большого для приведенного алгоритма зависит от количества установленных битов в параметре, и в худшем случае внутренний цикл будет
Описанный алгоритм вполне очевиден и, если не принимать во внимание возможность его реализации на языке ассемблера, улучшить его практически невозможно.
Тем не менее, давайте рассмотрим назначение алгоритма с другой точки зрения. В качестве входного значения функция принимает 1-байтный параметр, с помощью которого можно передать всего 256 значений. В таком случае, почему бы нам заранее не вычислить все возможные ответы и не записать их в статический массив? Реализация нового алгоритма приведена в листинге 1.4.
Листинг 1.4. Улучшенная функция определения количества установленных битов в байте const
BitCounts : array [0..255] of byte =
(0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8);
function CountBits2(B : byte): byte;
begin
Result := BitCounts[B];
end;
Здесь за счет статического 256-байтного массива значений функция намного упростилась. Более того, приведенный алгоритм не содержит цикла. Независимо от значения входного параметра, количество установленных битов вычисляется за один простой шаг. (Обратите внимание, что значения для статического массива были вычислены автоматически с помощью простой программы, использующей первую функцию.)
На компьютере автора книги последний алгоритм оказался в 10 раз быстрее, чем первый: 10 вызовов второй функции занимает столько же времени, сколько один вызов первой. (Обратите внимание, что здесь речь идет о среднем случае. В лучшем случае для первой функции значение параметра равно 0 и функция практически не будет требовать времени на выполнение.)
Таким образом, за счет введения 256-байтного массива мы разработали алгоритм, который быстрее в 10 раз. Увеличение скорости было достигнуто за счет увеличения требуемого объема памяти: можно получить быструю функцию, использующую большой статический массив (который будет скомпилирован в выполняемый файл, об этом также следует помнить), или более медленную функцию, не требующую больших объемов памяти. (Существует еще одна альтернатива. Можно заполнять массив значениями в процессе выполнения функции, при ее первом вызове. В этом случае массив не будет компилироваться в выполняемый файл, но первый вызов функции займет достаточно длительное время.)