Скачиваний:
83
Добавлен:
01.05.2014
Размер:
3.82 Mб
Скачать

Original pages: 343-377 201

как она делает logT−1 S + 1 проходов по данным. Если S оказывается степенью T − 1, то это самое лучшее, что можно получить при любом методе с T лентами, так как здесь достигается нижняя оценка из соотношения (5.4.4-9). С другой стороны, если S равно (T −1)m−1 +1, т. е. ровно на единицу больше степени T − 1, то этот метод теряет почти целый проход.

В упр. 2 показано, как устранить часть этой лишней работы, используя специальную программу окончания. Еще одно усовершенствование было предложено в 1966 г. Денисом Л. Бэнчером, который назвал свою процедуру перекрестным слиянием. [См. H. Wedekind, Datenorganisation (Berlin W. de Gruyter, 1970). 164–166, и U. S. Patent 3540000 (10 ноября 1970).] Основная идея состоит в том, чтобы отложить слияние до тех пор, пока не будет накоплено больше сведений об S. Мы обсудим несколько измененную форму первоначальной схемы Бэнчера.

Эта улучшенная осциллирующая сортировка действует следующим образом:

 

Операция

T1

T2

T3

T4

T5

Стоимость

Фаза 1.

Распределение

A1

A1

A1

A1

4

Фаза 2.

Распределение

A1

A1A1

A1A1

A1A1

3

Фаза 3.

Слияние

D4

A1

A1

A1

4

Фаза 4.

Распределение D4A1

A1

A1A1

A1A1

3

Фаза 5.

Слияние

D4

D4

A1

A1

4

Фаза 6.

Распределение D4A1

D4A1

A1

A1A1

3

Фаза 7.

Слияние

D4

D4

D4

A1

4

Фаза 8.

Распределение D4A1

D4A1

D4A1

A1

3

Фаза 9.

Слияние

D4

D4

D4

D4

4

В этот момент мы не сливаем все D4 в A16, (если только не окажется, что исходные данные исчерпаны);

лишь после того, как закончится

 

 

 

 

Фаза 15.

Слияние

D4D4

D4D4

D4D4

D4

4

будет получен первый отрезок A16:

 

 

 

 

Фаза 16.

Слияние

D4

D4

D4

A16

16

 

Второй отрезок A16 появится после создания еще трех D4:

 

Фаза 22.

Слияние

D4D4

D4D4

D4

A16D4

4

Фаза 23.

Слияние

D4

D4

A16

A16

16

и т. д. (ср. с фазами 1–5). Преимущества схемы Бэнчера можно видеть, если имеется, например, только пять начальных отрезков: осциллирующая сортировка (ее модификация из упр. 2) выполняла бы четырехпутевое слияние (на фазе 2), за которым следовало бы двухпутевое слияние с общей стоимостью 4 + 4 + 4 + 1 + 5 = 14, тогда как схема Бэнчера выполняла бы двухпутевое слияние (на фазе 3), за которым следовало бы четырехпутевое слияние с общей стоимостью 4 + 1+ 2 + 5 = 12. (Оба метода также требуют небольших дополнительных затрат, именно однократной перемотки перед окончательным слиянием.)

Точное описание метода Бэнчера содержится ниже в алгоритме B. К сожалению, соответствующую процедуру, по-видимому, трудней понять, чем запрограммировать; легче объяснить этот метод ЭВМ, чем программисту! Частично это происходит по той причине, что рекурсивный метод выражен в итеративном виде и затем подвергнут некоторой оптимизации; читатель, возможно, обнаружит, что необходимо несколько раз проследить за работой алгоритма, чтобы действительно осознать, что же происходит.

Алгоритм B. (Осциллирующая сортировка с перекрестным распределением.) Этот алгоритм берет первоначальные отрезки и распределяет их но лентам, время от времени прерывая процесс распределения, чтобы слить содержимое некоторых лент.

Picture: Рис. 77. Осциллирующая сортировка с перекрестным распределением.

В алгоритме используется P-путевое слияние и предполагается, что есть T = P + 1 3 ленточных устройств (не считая устройства, которое может быть необходимо для хранения исходных данных). Ленточные устройства должны допускать чтение как в прямом, так и в обратном направлении; они обозначены числами 0, 1, : : : , P. Используются следующие массивы:

D[j],

0

j P—число фиктивных отрезков, наличие которых предполагается в конце лен-

 

 

 

ты j.

 

 

 

 

 

 

 

A

l; j

,

 

 

l

 

L, 0

 

j

 

P. Здесь L—достаточно большое число, такое, что будет введено

[

]

 

0

 

 

L+1

 

 

 

 

 

не более P

 

начальных отрезков. Если A[l; j] = k 0, то на ленте j имеется отрезок

202 Original pages: 343-377

номинальной длины Pk, соответствующий ”уровню l” работы алгоритма. Этот отрезок возрастающий, если k четно, и убывающий, если k нечетно. A[l; j] = −1 означает, что на уровне l лента j не используется.

Инструкция ”записать начальный отрезок на ленту j” является сокращенным обозначением следующих действий:

Установить A[l; j] 0. Если исходные данные исчерпаны, то увеличить D[j] на 1; в противном случае записать отрезок на ленту j (в возрастающем порядке).

Инструкция ”слить на ленту j” используется как краткое обозначение следующих действий:

Если D[i] > 0 для всех i 6= j, то уменьшить D[i] на 1 при всех i 6= j и увеличить D[j] на 1. В противном случае слить один отрезок на ленту j со всех лент i 6= j, таких, что D[i] = 0, и уменьшить D[i] на 1 для всех остальных i 6= j.

B1 [Начальная установка.] Установить D[j]

0 при 0 j P. Затем записать начальный отрезок на

ленту j при 1 j P. Установить A[0; 0]

−1, l 0, q 0.

B2 [Ввод завершен?] (В этот момент лента q пуста и всякая другая лента содержит самое большее один отрезок.) Если еще есть исходные данные, перейти к шагу B3. Однако если ввод исчерпан, то перемотать все ленты j 6= q, такие, что A[0; j] четно; затем слить на ленту q, читая все только что перемотанные ленты в прямом направлении, а остальные ленты—в обратном. Этим завершается

сортировка; результат находится на ленте q в возрастающем порядке.

 

B3 [Начать новый уровень.] Установить l

l + 1, r

q, s

0 и q

(q + 1) mod T. Записать

начальный отрезок на ленту (q + j) mod T при 1 j T − 2. (Таким образом, начальные отрезки

 

записываются на все ленты, кроме лент q и r.) Установить A[l; q]

−1 и A[l; r]

−1.

B4

[Можно ли сливать?] Если A[l − 1; q] 6= s, вернуться к шагу B3.

 

 

B5

[Слияние.] (В этот момент A[l − 1; q] = A[l; j] = s при всех j 6= q, j 6= r.) Слить на ленту r. (См.

 

выше определение этой операции.) Затем установить s s + 1, l

l − 1, A[l; r]

s и A[l; q] −1.

 

Установить r

(2q − r) mod T. (В общем случае мы имеем r

= (q − 1) mod T, если s четно,

и r = (q + 1) mod T, если s нечетно.)

B6 [Закончен ли уровень?] Если l = 0, перейти к B2. В противном случае, если A[l; j] = s для всех j 6= q и j 6= r, то перейти к B4. В противном случае вернуться к B3.

Чтобы показать правильность этого алгоритма, мы можем использовать доказательство типа ”рекурсивной индукции”, так же как мы делали для алгоритма 2.3.1T. Предположим, что мы начинаем с шага B3 с l = l0, q = q0, s+ = A[l0; (q0 + 1) mod T] и s= A[l0; (q0 − 1) mod T], и допустим, кроме того, что либо s+ = 0, либо s= 1, либо s+ = 2, либо s= 3, либо : : : . Можно проверить по индукции, что алгоритм в конце концов придет к шагу B5, не изменив с нулевой по l-ю строки A и со значениями переменных l = l0 + 1, q = q0 1, r = q0 и s = s+ или s, причем мы выбираем знак +, если s+ = 0 или (s+ = 2 и s6= 1) или (s+ = 4 и s6= 1; 3) или : : :, и мы выбираем знак −, если (s= 1 и s+ = 0) или (s= 3 и s+ 6= 0; 2) или : : : . Приведенный здесь набросок доказательства не очень элегантен, но и сам алгоритм сформулирован в виде, который больше годится для реализации, чем для проверки правильности.

Picture: Рис. 78 Эффективность осциллирующей сортировки, использующей метод алгоритма B и упр. 3.

На рис. 78 показана эффективность алгоритма B, выраженная средним числом слияний каждой записи в зависимости от S—числа начальных отрезков, причем предполагается, что начальные отрезки приблизительно равны по длине. (Соответствующие графики для многофазной и каскадной сортировки приведены на рис. 70 и 74.) При подготовке рис. 78 учтено небольшое усовершенствование, упомянутое в упр. 3.

Прямое чтение. Схема осциллирующей сортировки, по-видимому, требует возможности обратного чтения, поскольку приходится где-то накапливать длинные отрезки по мере того, как мы сливаем вновь введенные короткие отрезки. Тем не менее М. А. Готц [Proc. AFIPS Spring Jt. Сотр. Conf.; 25 (1964), 599–607] нашел способ выполнить осциллирующую сортировку, используя только прямое чтение и простую перемотку. Его метод в корне отличается от остальных схем, которые мы видели в этой главе, поскольку

a)данные иногда записываются в начало ленты, причем предполагается, что данные, находящиеся в середине этой ленты, не разрушаются;

b)все начальные строки имеют фиксированную максимальную длину. Условие (a) нарушает свойство ”первым включается—первым исключается”, которое, как мы предположили, является характеристикой прямого чтения, однако оно может быть надежно реализовано, если между отрезками

Original pages: 378-399 203

оставлять достаточное количество чистой ленты и если в нужные моменты пренебречь ”ошибками четности”. Условие (b) оказывается до некоторой степени противоречащим эффективному использованию выбора с замещением.

Осциллирующая сортировка Готца с прямым чтением имеет одно темное пятно—это один из первых алгоритмов, который был запатентован как алгоритм, а не как физическое устройство [U. S. Patent 3380029 (23 апреля 1968)]. Если положение не изменится, то это означает, что алгоритм нельзя использовать в программе без разрешения владельца патента. Метод Бэнчера (осциллирующая сортировка с обратным чтением) был запатентован IBM несколькими годами позже. [Таким образом, наступил конец эры, когда удовольствие от открытия нового алгоритма считалось достаточным вознаграждением! Так как программирование неотделимо от создания машины, а программы для ЭВМ теперь стоят денег, то патентование алгоритмов является неизбежным. Конечно, действия недальновидных людей, сохраняющих новые алгоритмы в строгом секрете, значительно хуже, чем широкая доступность алгоритмов, которые являются собственностью в течение лишь ограниченного времени.]

Центральная идея в методе Готца состоит в таком использовании лент, чтобы каждая лента начиналась с отрезка относительной длины 1, за которым следовал бы отрезок относительной длины P, затем P2 и т. д. Например, если T = 5, то сортировка начинается следующим образом (”.” указывает текущее положение головки чтения-записи на каждой ленте):

 

Операция

T1

T2

T3

T4

T5

Стоимость

Примечания

Фаза 1.

Распределение

A1

:A1

:A1

:A1

A1:

5

[T5 не перематывается]

Фаза 2.

Слияние

A1:

A1:

A1:

A1: A1A4:

4

[Перемотка всех лет]

Фаза 3.

Распределение

A1

:A1

:A1

A1: :A1A4

4

[T4 не перематывается]

Фаза 4.

Слияние

A1:

A1:

A1:

A1A4: A1

:A4

4

[Перемотка всех лент]

Фвэа 5.

Распределение

A1

:A1

A1:

:A1A4

:A1A4

4

[T3 не перематывается]

Фаза 6.

Слияние

A1:

A1:

A1A4: A1:A4

A1

:A4

4

[Перемотка всех лент]

Фаза 7.

Распределение

A1

A1:

:A1A4 :A1A4

:A1A4

4

[T2 не перематывается]

Фаза 8.

Слияние

A1:

A1A4: A1:A4 A1:A4

A1

:A4

4

[Перемотка всех лент]

фаза 9.

Распределение

A1:

:A1A4 :A1A4 :A1A4

:A1A4

4

[T1 не перематывается]

Фаза 10.

Слияние

A1A4:

A1:A4 A1:A4 A1:A4

A1

:A4

4

[Нет перемотки]

Фаза 11.

Слияние

A1A4A16: A1A4: A1A4: A1A4: A1A4:

16

[Перемотка всех лент]

И так далее. Во время фазы 1 лента T1 перематывается и одновременно на T2 записываются исходные данные, затем перематывается T2 и одновременно на T3 записываются исходные данные и т. д. В конце концов, когда исходные данные исчерпаны, начинают появляться фиктивные отрезки, и иногда необходимо вообразить, что они записаны явно на ленте полной длины. Например, если S = 18, то отрезки A1 на T4 и T5 будут фиктивными во время фазы 9; нам придется продвинуться вперед по T4

иT5 при слиянии с T2 и T3 на T1 во время фазы 10, так как нам надо добраться до отрезков A4 на T4

иT5 для подготовки к фазе 11. С другой стороны, фиктивный отрезок A1 на T1 не обязательно должен существовать явно. Таким образом, ”конец игры” несколько замысловат. Еще с одним примером применения этого метода мы встретимся в следующем пункте.

Упражнения

1.[22] В тексте имеется иллюстрация осциллирующей сортировки Собеля в ее первозданном виде для T = 5 и S = 16. Дайте точное определение алгоритма, в котором эта процедура обобщается и сортируются S = PL начальных отрезков на T = P +1 3 лентах. Постарайтесь найти алгоритм, который может быть очень просто описан.

2.[24] Если в изначальном методе Собеля S = 6, то мы могли бы заявить, что S = 16 и что имеется 10 фиктивных отрезков. Тогда фаза 3 в примере в тексте поместила бы фиктивные отрезки A0 на T4 и T5; фаза 4 слила бы отрезки A1 на T2 и T3 в D2 на T1; фазы 5–8 не делали бы ничего; фаза 9 породила бы A6 на T4. Лучше бы перемотать T2 и T3 сразу после фазы 3 и затем немедленно

получать A6 на T4 с помощью трехпутевого слияния.

Покажите, как, основываясь на этой идее, улучшить окончание алгоритма из упр. 1, если S не является точной степенью P.

>3. [24] Составьте таблицу, показывающую поведение алгоритма B, если T = 3, предполагая, что имеется 9 начальных отрезков. Покажите, что эта процедура очевидно неэффективна в одном месте, и предложите изменения в алгоритме B, которые исправляют положение.

4.[21] На шаге B3 имеется установка как A[l; q], так и A[l; r]в −1. Покажите, что одна из этих операций всегда лишняя, так как соответствующий элемент массива A никогда не рассматривается.

5.[М25] Пусть S—число начальных отрезков в имеющихся исходных данных для алгоритма B. При каких значениях S не требуется ни одной перемотки на шаге B2?

204 Original pages: 378-399

5.4.6. Практические аспекты слияния на лентах

Здесь всплывают разные мелочи. После того как мы обсудили различные семейства схем слияния, самое время посмотреть, как они отображаются на реальные конфигурации ЭВМ и магнитных лент, и сравнить разумным образом эти схемы. Изучение внутренней сортировки показало, что невозможно адекватно оценить эффективность метода сортировки, просто подсчитывая число выполняемых им сравнений; подобно этому, мы не можем правильно оценить метод внешней сортировки, просто выясняя число выполняемых им проходов по данным.

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

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

MIXT читает или записывает 800 литер на дюйм ленты со скоростью 75 дюймов в секунду. Это означает, что одна литера читается или записывается в течение 1/60 мс, т. е. 1632 мкс, если лента находится в активном состоянии. [Реальные лентопротяжные устройства, широко распространенные в настоящее время, имеют плотность в диапазоне от 200 до 1600 литер на дюйм и скорость в диапазоне от 3721 до 150 дюймов в секунду, так что их эффективная скорость в сравнении с MIXT изменяется в диапазоне от 18 до 4. С практической точки зрения большой объем сортировки выполняется в коммерческих системах на относительно небольшом и недорогом оборудовании, которое медленнее, чем рассматриваемое здесь. С другой стороны, лентопротяжные устройства вскоре могут резко измениться, что сделает настоящие предположения устаревшими. Наша основная цель состоит не в получении конкретных ответов, а в том, чтобы научиться разумно сочетать теорию с практикой.]

Одно из важных соображений, которое надо иметь в виду, состоит в том, что ленты имеют конечную длину. Каждая бобина содержит 2400 футов ленты или меньше; следовательно, на одной бобине ленты MIXT есть место для самое большее примерно 23 000 000литер, и, чтобы прочесть их все, требуется около 23 000 000=3 600000 6:4 мин. Если требуется сортировать больший файл, то обычно лучше всего сортировать за один раз одну полную бобину и затем слить отдельные отсортированные бобины с целью избежать избыточной работы по установке лент. Это означает, что число начальных отрезков S, реально присутствующих в схемах слияния, которые мы изучали, никогда не будет очень большим. Мы никогда не столкнемся со

Picture:

Рис. 79. Магнитная лента с блоками переменной длины.

случаем S > 5000 даже при очень маленькой внутренней памяти, производящей начальные отрезки длиной только в 5000 литер. Следовательно, формулы, дающие асимптотическую эффективность алгоритмов при S ! 1, имеют главным образом академический интерес.

Данные хранятся на ленте в виде блоков (рис. 79), и каждая инструкция чтения/записи передает один блок. Блоки часто называются ”записями”, но мы будем избегать этого термина, чтобы не путать его с ”записями” файла, которые участвуют в сортировке. Для многих ранних программ сортировки, написанных в 50-х годах, это различие было необязательным, так как в одном блоке хранилась одна запись; но мы увидим, что объединение нескольких записей в одном блоке на ленте обычно дает определенные преимущества.

Соседние блоки разделяются межблочными промежутками длиной по 480 литер, что позволяет ленте остановиться или разогнаться между отдельными командами чтения или записи. Межблочные промежутки приводят к уменьшению числа литер на одной бобине ленты, зависящему от числа литер в одном блоке (рис. 80); в той же степени уменьшается количество литер, передаваемых за секунду, так как лента движется с постоянной скоростью.

Многие ”устаревшие” модели ЭВМ имеют фиксированные и весьма малые размеры блока. Например, MIX, как она описана в гл. 1, всегда читает и пишет блоки по 100 слов, таким образом, это составляет примерно 500 литер на блок и 480 литер на промежуток, т. е. почти половина ленты пропадает! Сейчас большая часть машин допускают переменный размер блока, и поэтому ниже мы обсудим проблему выбора подходящего размера блока.

Picture:

Рис. 80. Число литер на одной бобине ленты MIXT как функция от размера

 

блока.

Original pages: 378-399 205

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

Picture:

Рис. 81. Как вычислить время стартстопной задержки. (Оно добавляется ко

 

времени, используемому при чтении или записи блоков и промежутков.)

то лента остановится и к тому же потребуется некоторое время, чтобы разогнать ее до полной скорости в следующей операции. Суммарная стартстопная задержка составляет 5 мс, 2 для остановки и 3 для разгона (рис. 81). Таким образом, если мы не поддерживаем непрерывного, безостановочного движения ленты с полной скоростью, то эффект во времени счета будет, в сущности, такой же, как если бы в межблочном промежутке было 780 литер вместо 480.

Рассмотрим теперь операцию перемотки. К сожалению, обычно трудно точно охарактеризовать время, требуемое для перемотки ленты на заданное число литер n. На некоторых машинах имеется ”быстрая перемотка”, которая применяется, только если n превышает число порядка 5 миллионов; для меньших значений n перемотка происходит с нормальной скоростью чтения/записи.

Picture:

Рис. 82. Приблизительное время счета при двух обычно используемых мето-

 

дах перемотки.

На других машинах имеется специальный мотор, используемый во всех операциях перемотки; он постепенно ускоряет бобину ленты до определенного числа оборотов в минуту, а затем тормозит ее, когда подходит время остановки; действительная скорость ленты в этом случае зависит от заполненности бобины. Мы примем для простоты, что MIXT требует max(30; n=150) мс для перемотки на n литерных позиций (включая промежутки), т. е. примерно две пятых того, что требуется для их чтения/записи. Это достаточно хорошее приближение к поведению многих реальных устройств, где отношение времени чтения/записи ко времени перемотки обычно заключено между 2 и 3, но оно не дает адекватной модели эффекта комбинированной нормальной и быстрой перемотки, имеющейся на многих других машинах (рис. 82).

При первоначальной установке и/или перемотке к началу лента помещается в точку загрузки, и для любой операции чтения или записи, начинающейся из этого положения, требуется дополнительно 110 мс. Если лента не находится в точке загрузки, она может быть прочитана в обратном направлении; 32 мс добавляется ко времени любой обратной операции, следующей за прямой, или любой прямой операции, следующей за обратной.

Наконец, следует рассмотреть возможность одновременного ввода и вывода. Часто по экономическим причинам несколько ленточных устройств присоединяются к одному ленточному контроллеру (устройству управления лентами), который может одновременно работать только с одной или двумя лентами, поскольку число линий передачи данных между ним и ЭВМ ограниченно. Иногда контроллеры не способны работать более чем с одной лентой одновременно. Однако часто они могут читать одну ленту во время записи другой. Несколько реже встречаются контроллеры, которые могут читать одновременно с двух устройств, и автор никогда не видел контроллера, который мог бы писать на два устройства одновременно. Перемотка—это особый случай: через 30 мс после начала перемотки ленточное устройство MIXT ”отключается” от своего контроллера, который после этого способен выполнять операции с другими устройствами. Таким образом, очень большое число ленточных устройств могут одновременно осуществлять перемотку.

Почти все машины допускают выполнение ввода/вывода параллельно с вычислениями, хотя многие ЭВМ, когда происходит ввод/вывод, работают на 20–40% медленнее из-за ”разделения циклов памяти”.

Снова о слиянии. Обратимся вновь к процессу P-путевого слияния с упором на функционирование устройств ввода и вывода. Будем считать, что для вводных и выводного файлов используется P + 1 ленточных устройств. Наша цель—максимально совместить операции ввода/вывода друг с другом и со счетом по программе так, чтобы минимизировать общее время слияния.

Поучительно рассмотреть следующий частный случай, в котором на возможную степень одновременности наложены серьезные ограничения. Предположим, что

a)можно записывать не более чем на одну ленту одновременно;

b)можно читать не более чем с одной ленты одновременно;

c)чтение, запись и вычисления могут происходить одновременно, только если операции чтения и записи были начаты одновременно.

206Original pages: 378-399

Оказывается, что даже при таких ограничениях достаточно иметь2P буферов ввода и 2 буфера вывода, чтобы поддерживать, в сущности, максимальную скорость движения лент, если только мы имеем дело не с очень медленной ЭВМ. Заметим, что (a) не является на самом деле ограничением, так как имеется только одна выводная лента. Кроме того, объем ввода равен объему вывода, так что читается в среднем только одна лента в любой данный момент времени; если условие (b) не выполняется, то обязательно должны быть периоды, когда вообще не происходит ввода. Следовательно, для того чтобы минимизировать время слияния, достаточно поддерживать выводную ленту в состоянии работы.

Важный метод, называемый предсказанием или прогнозированием (forecasting), дает желаемый эффект. Во время выполнения P-путевого слияния обычно имеется P ”текущих буферов ввода”, которые используются как источники данных; некоторые из них заполнены больше других в зависимости от того, какая часть данных в них уже просмотрена. Если все они опустошатся примерно в одно и то же время, то, прежде чем продолжить работу, нам придется выполнить много чтений, если только мы не предпримем нужных мер заранее. К счастью, всегда можно сказать, какой буфер первым станет пустым, просто посмотрев на последнюю запись в каждом буфере. Буфер, последняя запись которого имеет наименьший ключ, обязательно будет первым пустым буфером независимо от значений какихлибо других ключей, так что мы всегда знаем, какой файл послужит причиной нашей следующей команды ввода. Алгоритм F раскрывает этот принцип в деталях.

Алгоритм F. (Прогнозирование с плавающими буферами.) Этот алгоритм управляет буферизацией во время P-путевого слияния длинных вводных файлов при P 2. Допустим, что вводные ленты и файлы занумерованы 1, 2, : : :, P. В алгоритме используются 2P буферов ввода I[1], : : : , I[2P], два буфера вывода O[0] и O[1] и следующие вспомогательные массивы:

A[j], 1 j 2P: 0 если в буфер I[j] можно вводить данные; 1 в противном случае.

B[i], 1 i P: Буфер, содержащий последний прочитанный блок из файла i. C[i], 1 i P: Буфер, используемый в настоящий момент для ввода из файла i. L[i], 1 i P: Последний ключ, прочитанный из файла i.

S[j], 1 j 2P: Буфер, который следует использовать, когда I[j] опустошится.

В описываемом виде алгоритм никогда не остановится; подходящий способ его ”выключения” обсуждается ниже.

F1

[Начальная установка.] Прочитать первый блок с ленты i в буфер I[i], установить A[i] 1, A[P +

 

i] 0, B[i]

i, C[i] i и установить L[i]равным ключу последней записи в I[i] при 1 i P. Затем

 

найти m, такое, что L(m) = minfL[1]; : : : ; L[P]g, и установить t 0, k P + 1. Начать читать с

 

ленты от в буфер I[k].

 

 

 

F2

[Слияние.] Сливать записи из буферов I[C[1]], : : : , I[C[P]] в O[t], пока буфер O[t] не заполнится. Если

 

во время этого процесса какой-нибудь буфер ввода, скажем I[C[i]], станет пустым, а O[t] еще не

 

заполнен, то установить A[C[i]] 0, C[i]

S[C[i]] и продолжать слияние.

F3

[Ввод/вывод завершен.] Ждать, пока не завершится предыдущая операция чтения (или чте-

 

ния/записи). Затем установить A[k]

1, S[B[m]] k, B[m] k и установить L[m] равным ключу

 

последней записи в I[k].

 

 

 

F4

[Прогнозирование.] Найти m, такое, что L[m] = minfL[1]; : : : ; L[P]g, и найти k, такое, что A[k] = 0.

F5

[Чтение/запись.] Начать читать с ленты m в буфер I[k] и писать из буфера O[t] на выводную ленту,

 

затем положить t 1 − t и вернуться к F2.

 

 

 

 

 

Picture:

Рис. 83. Прогнозирование с плавающими буферами.

Пример на рис. 84 показывает, как работает метод прогнозирования при P = 2 в предположении, что каждый блок на ленте содержит только две записи. Здесь представлено содержимое буферов ввода во все моменты, когда мы достигаем начала шага F2. Алгоритм F, в сущности, образует P очередей буферов, где C[i] указывает на начало i-й очереди, B[i]—на ее конец, S[j] указывает на преемника буфера I[j]; этим указаниям на рис. 84 соответствуют стрелки. Строка 1 отражает состояние дел после начальной установки. Для каждого вводного файла есть один буфер, и еще один блок читается из файла 1 (так как 03 < 05). Строка 2 показывает положение вещей после того, как слит первый блок: мы выводим блок, содержащий ” 01 02”, и вводим следующий блок из файла 2 (так как 05 < 09). Заметим, что в строке 3 три из четырех буферов ввода, по сути дела, предоставлены файлу 2, так как мы читаем из этого файла и в его очереди уже есть полный буфер и частично заполненный буфер.

Original pages: 378-399 207

Этот механизм ”плавающих буферов” является важной чертой алгоритма F, так как мы не смогли бы продолжить работу в строке 4, если бы в строке 3 выбрали для ввода файл 1 вместо файла 2.

Picture:

Рис. 84. Очереди буферов в соответствии с алгоритмом F.

Чтобы доказать правильность алгоритма F, мы должны установить два факта:

i)всегда имеется свободный буфер (т. е. мы всегда можем найти k на шаге F4);

ii)если буфер ввода исчерпывается во время слияния, то его преемник уже присутствует в памяти (т. е. S[C[i] в шаге F2 имеет осмысленное значение).

Допустим, что (i) не имеет места, т. е. все буферы заняты в некоторый момент, когда мы достигаем шага F4. Каждый раз, когда мы приходим к этому шагу, суммарный объем необработанных данных во всех буферах составляет ровно P емкостей буфера, т. е. данных ровно столько, чтобы, переместив их, заполнить P буферов, ибо данные вводятся и выводятся с одинаковой скоростью. Некоторые буферы заполнены лишь частично, однако для каждого файла частично заполнен самое большее один буфер, так что всего таких буферов не более P. По предположению все 2P буферов заняты, так что по меньшей мере P из них должны быть заполнены целиком. Это может случиться, только если

Pбуферов полны и P пусты, иначе мы бы имели слишком много данных. Но самое большее один буфер может быть одновременно пуст и занят; следовательно, (i) нe может не выполняться.

Допустим, что (ii) не имеет места, т. е. для некоторого файла в памяти нет необработанных записей, но текущий буфер вывода еще не полон. Согласно принципу прогнозирования, нужно иметь не более одного блока данных для всех остальных файлов, так как мы не читаем блок из файла, если этот блок не потребуется прежде, чем будут исчерпаны буферы какого-нибудь другого файла. Таким образом, общее число необработанных записей составляет самое большее P − 1 блоков; добавление неполного буфера вывода дает менее P буферных емкостей данных в памяти; получили противоречие.

Эти рассуждения устанавливают справедливость алгоритма F; они также показывают, что возможны патологические обстоятельства, при которых алгоритм едва-едва избегает крушения. Нами здесь не упомянута некая важная тонкость, касающаяся возможного равенства ключей; это обсуждается в упр. 5.

Один из способов изящно завершить алгоритм F состоит в том, чтобы присвоить L[m] значение 1 на шаге F3, если только что прочитанный блок был последним в отрезке. (Конец отрезка всегда указывается некоторым особым образом.) После того как будут прочитаны все данные во всех файлах, мы в конце концов обнаружим на шаге F4, что все L равны 1; тогда обычно можно начать чтение первых блоков следующих отрезков в каждом файле, выполняя начальную установку для следующей фазы слияния по мере вывода последних P + 1 блоков.

Таким образом, можно поддерживать полную скорость работы выводной ленты, читая в любое время не более одной ленты. Исключение из этого правила встречается на шаге F1, где было бы полезно читать сразу несколько лент, чтобы обеспечить возможность начать работу; но обычно шаг F1 можно устроить так, чтобы он совмещался с предыдущей частью вычислений.

Идея просмотра последней записи каждого блока с целью предсказания, какой буфер первым станет пустым, была высказана в 1953 г. Ф. Э. Гольбертон. Сам метод впервые был опубликован Э. X. Фрэндом [JACM, 3 (1956), 144–145, 165]. Его довольно сложный алгоритм использовал 3P буферов ввода, и каждому файлу ввода предназначалось по три буфера; алгоритм F улучшает положение, используя ”плавающие буферы” и позволяя любому одному файлу потребовать сразу даже

P+ 1 буферов ввода, хотя всего требуется не более 2P буферов.

Слияние с менее чем 2P буферами обсуждается в конце этого пункта. Некоторые ЭВМ имеют возможность ”чтения вразброс—записи со сборкой”, что позволяет осуществлять ввод/вывод из непоследовательных ячеек памяти; использование такой возможности выходит за рамки этой книги.

Сравнительное поведение схем слияния. Используем теперь наши знания о лентах и слиянии, чтобы сравнить эффективность различных схем слияния, изученных нами в п. 5.4.2–5.4.5. Будет весьма поучительно разработать детали каждого метода в применении к конкретному ”беспристрастному” примеру. Рассмотрим поэтому задачу сортировки файла, каждая запись которого содержит 100 литер, причем в памяти для записи данных доступно 100000 литерных позиций (не считая места для программы, ее вспомогательных переменных и сравнительно небольшого пространства, необходимого для ссылок в дереве выбора). Исходные данные расположены на ленте в случайном порядке блоками по 5000 литер каждый, и результат должен получиться в том же формате. Для работы имеется пять рабочих лент в добавление к устройству, на котором находится вводная лента.

Общее число сортируемых записей 100000, но эта информация алгоритму сортировки заранее не известна.

208Original pages: 378-399

Всхему A (здесь и далее см. вкладку) сведены те действия, которые происходят, когда к нашим данным применяется десять различных схем слияния. Обратившись к этой важной иллюстрации, очень полезно вообразить, что вы наблюдаете за тем, как происходит реальная сортировка: медленно просматривайте каждую строку слева направо, мысленно представляя себе шесть лент, осуществляющих чтение, запись, перемотку и/или обратное чтение, как указано на диаграмме. В течение P-путевого слияния вводные ленты будут находиться в движении в P раз реже, чем выводная лента. Заметим, что в схеме A предполагается, что, когда первоначальная вводная лента полностью прочитана (и перемотана, чтобы ее убрать), умелый оператор снимает ее и заменяет рабочей лентой за 30 с. В примерах 2, 3 и 4 это и есть ”время критического пути”, когда ЭВМ в бездействии ожидает оператора. Но в остальных примерах операция снятия и установки лент совмещена с другой работой. (На схеме A по горизонтали указано время в мин.)

Пример 1. Сбалансированное слияние с прямым чтением. Напомним описание задачи: записи имеют длину в 100 литер, внутренней памяти достаточно для одновременного хранения 1000 записей

икаждый блок вводной ленты содержит 5000 литер (50 записей). Всего имеется 100 000 записей (т. е. 10 000 000 литер, или 2000 блоков).

Мы ничем не связаны в выборе размера блоков для промежуточных файлов. Шестиленточное сбалансированное слияние использует трехпутевое слияние, так что техника алгоритма F требует 8 буферов; можно, следовательно, использовать блоки, содержащие каждый по 1000=8 = 125 записей (или 12500 литер).

Проход начального распределения может использовать выбор с замещением (п. 5.4.1), и, чтобы поддерживать непрерывную работу лент, будем использовать два буфера ввода по 50 записей каждый, плюс два буфера вывода по 125 записей каждый. Это оставляет для дерева выбора место в 650 записей. Большая часть начальных отрезков будет, следовательно, иметь длину около 1300 записей (10 или 11 блоков); на схеме A получилось 78 начальных отрезков, причем последний отрезок короткий.

Первый проход слияния, как показано, сливает девять отрезков на ленту 4, а не чередует ленты 4, 5 и 6. Это дает возможность выполнять полезную работу в то время, когда оператор вычислительной машины устанавливает рабочую ленту на устройство 6; так как общее число отрезков S известно сразу после завершения начального распределения, то алгоритм знает, что на ленту 4 должно быть слито dS=9e отрезков, затем d(S − 3)=9e—на ленту 5, затем d(S − 6)=9e—на ленту 6.

Вся процедура сортировки в этом примере может быть следующим образом изображена с использованием обозначений, введенных в п. 5.4.2:

126

126

126

39

39

38

93

93

9261

— — —

271

271

241

781 — — — — —

Пример 2. Многофазное слияние с прямым чтением. Второй пример на схеме A иллюстрирует многофазное слияние в соответствии с алгоритмом 5.4.2D. В этом случае мы выполняем пятипутевое слияние, поэтому память разбита на 12 буферов по 83 записи каждый. В течение первоначального выбора с замещением мы имеем два буфера ввода в 50 записей и два буфера вывода в 83 записи, что оставляет 734 записи в дереве; таким образом, начальные отрезки в этот раз будут иметь длину около 1468 записей (17 или 18 блоков). В данной ситуации получено S = 70 начальных отрезков, причем длины двух последних в действительности равны только четырем блокам и одному блоку соответственно. Схему слияния можно изобразить так:

013118

013117

013115

012112

0811

115

114

112

18

— 08142153

17

16

14

— 48

142153

13

12

— 84

44

2153

11

— 161191

82

42

52

341

191

81

41

51

701

Удивительно, что многофазное слияние занимает на 25 с больше времени, чем значительно более простое сбалансированное слияние! Это объясняется двумя основными причинами:

1)Этот случай особенно удачен для сбалансированного слияния, так как S = 78 очень близко к точной степени 3. Если бы было получено 82 начальных отрезка, то сбалансированное слияние заняло бы еще один проход.

Original pages: 378-399 209

2)При многофазном слиянии теряется 30 c во время замены вводной ленты и в целом свыше 5 мин проходит в ожидании завершения операций перемотки. В противоположность этому сбалансированное слияние требовало сравнительно небольшого времени перемотки. Во второй фазе многофазного слияния сэкономлено 13 с, так как 8 фиктивных отрезков на ленте 6 можно считать присутствующими даже во время перемотки этой ленты, но дальше не происходит никакого совмещения перемотки. Таким образом, многофазный метод проигрывает, несмотря на то, что он требует значительно меньшего времени чтения/записи.

Пример 3. Каскадное слияние с прямым чтением. Этот случай аналогичен предыдущему, только использует алгоритм 5.4.3С. Слияние изображается так:

114

115

112

114

115

15

19

— 114

115

132336

5163 53

5362 — 11

22

121

61

181 181

161

701

(Просматривая схему A, не забывайте представлять каждый пример в действии.)

Пример 4. Многофазное слияние с расщеплением лент. Эта процедура, описанная в конце п. 5.4.2, позволяет совместить большую часть времени перемотки. Она использует четырехпутевое слияние, так что мы делим память на десять буферов по 100 записей; в дереве выбора имеется 700 записей, и в результате оказывается, что образовано 72 начальных отрезка. Последний отрезок вновь очень короткий. Использована схема распределения, аналогичная алгоритму 5.4.2D, за ней следует простой, но до некоторой степени специальный метод размещения фиктивных отрезков:

121

119

115

18

— 0219

02117

02115

02111

0214

021944

113

111

17

— 0244

021944

110

18

14

02443241

1844

16

14

— 44

02443241

1444

15

13

4431 01443241

1344

12

— 3172

4431

423241

44

11

3172131 4331

413241

43

131

3172131

4231

3241

42

131141

72131

4131

3141

41

181

131141

71131

31

41

181

141

131

271

— — 721 — —

Среди всех примеров на схеме A, которые не читают в обратном направлении, в этом, как оказывается, наилучшее время выполнения. Так как S никогда не бывает очень большим, можно разработать более сложный алгоритм, который размещает фиктивные отрезки еще лучше (см. упр. 5.4.2-26).

Пример 5. Каскадное слияние с совмещением перемоток. Эта процедура работает почти так же быстро, как предыдущая, хотя управляющий ею алгоритм более прост. Мы используем для начального распределения метод каскадной сортировки, как в алгоритме 5.4.3C, но с T = 5, а не T = 6. Затем использование лент в каждой фазе каждого ”каскада” чередуется таким образом, что мы обычно не пишем на ленту, пока она почти наверняка не окажется перемотанной. Короче говоря, схема такова:

121

122

119

110

14

17

— — 122235 410

72

— 83 7282

— 41

261

81

221

161

721

Пример 6. Сбалансированное слияние с обратным чтением. Этот пример похож на пример 1, но все перемотки устранены:

A26

A26

A26

 

1

1

1

 

D9

D9

D8

 

A3

A3

A2A1

3

3

3

— — —

9

9

9

6

D1

D1

D1

 

A1

 

 

 

24

27

27

— — — — —

78

 

 

 

 

 

 

210Original pages: 378-399

Так как в примере 1 было сравнительно мало перемоток, то эта схема не намного лучше, чем в случае прямого чтения. Фактически она оказывается несколько медленней многофазной схемы с расщеплением лент, несмотря на удачное значение S = 78.

Пример 7. Многофазное слияние с обратным чтением. В этом примере используется только пять лент из шести, чтобы устранить время перемотки и смены вводной ленты. Таким образом, используется только четырехпутевое слияние и такая же структура буферов, как в примерах 4 и 5. Используется распределение, аналогичное алгоритму 5.4.2D, но направление отрезков чередуется, и лента 1 зафиксирована, как конечная выводная лента. Первым записывается возрастающий отрезок на ленту 1; затем убывающие отрезки на ленты 2, 3, 4; затем возрастающие отрезки на 2, 3, 4; затем убывающие на 1, 2, 3 и т. д. Всякий раз, как мы переключаем направление, выбор с замещением обычно дает более короткий отрезок, поэтому оказалось образовано 77 начальных отрезков вместо 72 в примерах 4 и 5.

Эта процедура в результате дает распределение (22; 21; 19; 15) отрезков, а ближайшее точное распределение—(29; 56; 52; 44). Упражнение 5.4.4-5 показывает, как построить строки чисел слияния, которые могут быть использованы для размещения фиктивных отрезков ”оптимальным” образом; такая процедура возможна на практике, поскольку конечность бобины гарантирует, что S никогда не будет слишком большим. Поэтому пример на схеме A был построен с использованием такого метода размещения фиктивных отрезков (см. упр. 7). Он оказался самым быстрым из всех представленных примеров.

Пример 8. Каскадное слияние с обратным чтением. Как и в примере 7, здесь участвует только пять лент. Эта процедура следует алгоритму 5.4.3C, используя перемотку и прямое чтение, чтобы избежать однопутевого слияния (так как перемотка более чем в два раза быстрее чтения на устройствах MIXT). Распределение, следовательно, то же, что и в примере 6. Используя символ # для обозначения перемотки, изобразим эту схему так:

A21

A22

A19

A10

1

1

1

1

 

A4

A7

2D2D5

D10

1 #2

12#

4

1 2 3

14

A8A7

A5

A9

D4 #

D17

A9 #

D25

D21

Пример 9. Осциллирующая сортировка с обратным чтением. Осциллирующая сортировка с T = 5 (алгоритм 5.4.5B) может использовать распределение буферов, как в примерах 4, 5, 7 и 8, так как она выполняет четырехпутевое слияние. Однако выбор с замещением действует здесь иначе, поскольку непосредственно перед входом в каждую фазу слияния выводится отрезок длины 700 (а не примерно 1400), чтобы очистить внутреннюю память. Следовательно, здесь порождается 85 отрезков вместо 72. Некоторые ключевые шаги этого процесса таковы:

A1

A1A1

A1A1

A1A1

D4

A1

A1

A1

: : : : : : : :: : : : : : : : : :: : : : : : : : : :: : : : : : : : : :: : : : : : : : : ::

D4D4

D4D4

D4D4

D4

D4

D4

D4

A16

: : : : : : : :: : : : : : : : : :: : : : : : : : : :: : : : : : : : : :: : : : : : : : : ::

D4

A16D4D4

A16D4

A16D4A1

A16

D4

A16D4D4 A16D4D1

A16D4

A16

A16D4

A16D4

A16

A16A13

A16D4

A16

A16A4

A16A13

A16

A16A4

A16A4

A16A13

D37

A16 #

A16 #

A16 #

A85 — — —

Пример 10. Осциллирующая сортировка с прямым чтением. В последнем примере выбор с замещением не используется, так как все начальные отрезки должны быть одной длины. Следовательно, будет происходить внутренняя сортировка 1000 записей (полной емкости памяти) каждый раз, когда

Соседние файлы в папке Дональд Кнут. Искусство программирования. т.3