Тайная жизнь программ. Как создать код, который понравится вашему компьютеру [Джонатан Стейнхарт] (pdf) читать онлайн

-  Тайная жизнь программ. Как создать код, который понравится вашему компьютеру  (пер. Сергей Викторович Черников) (и.с. Для профессионалов) 9.33 Мб, 528с. скачать: (pdf) - (pdf+fbd)  читать: (полностью) - (постранично) - Джонатан Стейнхарт

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]

ТАЙНАЯ ЖИЗНЬ
ПРОГРАММ
КАК СОЗДАТЬ КОД, КОТОРЫЙ
ПОНРАВИТСЯ ВАШЕМУ КОМПЬЮТЕРУ

ДЖОНАТАН

СТЕЙНХАРТ

2023

Джонатан Стейнхарт
Тайная жизнь программ. Как создать код, который
понравится вашему компьютеру
Перевел с английского С. Черников
Научный редактор А. Гаврилов
ББК 32.973.2-018
УДК 004.05

Стейнхарт Джонатан
С79 Тайная жизнь программ. Как создать код, который понравится вашему компью­
теру. — СПб.: Питер, 2023. — 528 с.: ил. — (Серия «Для профессионалов»).
ISBN 978-5-4461-1731-4
Знакомы ли вы с технологиями, лежащими в основе вашей собственной программы? Почему «правильный» код не хочет работать? Истина проста и банальна — нужно сразу создавать код, который будет работать
хорошо и не будет прятать в себе трудноуловимые ошибки.
Для этого Джонатан Стейнхарт исследует фундаментальные концепции, лежащие в основе работы компьютеров. Он рассматривает аппаратное обеспечение, поведение программ на определенных устройствах,
чтобы показать, как на самом деле должен работать ваш код.
Узнайте, что на самом деле происходит, когда вы запускаете код на компьютере, и вы научитесь программировать лучше и эффективнее.

16+ (В соответствии с Федеральным законом от 29 декабря 2010 г. № 436-ФЗ.)
ISBN 978-1593279707 англ.

ISBN 978-5-4461-1731-4

© 2019 by Jonathan E. Steinhart.
The Secret Life of Programs: Understand Computers — Craft Better Code
ISBN 978-1-59327-970-7, published by No Starch Press.
Russian edition published under license by No Starch Press Inc.
© Перевод на русский язык ООО «Прогресс книга», 2023
© Издание на русском языке, оформление ООО «Прогресс книга», 2023
© Серия «Для профессионалов», 2023

Права на издание получены по соглашению с No Starch Press. Все права защищены. Никакая часть данной книги не
может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав.
Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может
гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные
ошибки, связанные с использованием книги. Издательство не несет ответственности за доступность материалов,
ссылки на которые вы можете найти в этой книге. На момент подготовки книги к изданию все ссылки на интернетресурсы были действующими. В книге возможны упоминания организаций, деятельность которых запрещена на
территории Российской Федерации, таких как Meta Platforms Inc., Facebook, Instagram и др.

Изготовлено в России. Изготовитель: ООО «Прогресс книга». Место нахождения и фактический адрес:
194044, Россия, г. Санкт-Петербург, Б. Сампсониевский пр., д. 29А, пом. 52. Тел.: +78127037373.
Дата изготовления: 07.2023. Наименование: книжная продукция. Срок годности: не ограничен.
Налоговая льгота — общероссийский классификатор продукции ОК 034-2014, 58.11.12.000 — Книги печатные
профессиональные, технические и научные.
Импортер в Беларусь: ООО «ПИТЕР М», 220020, РБ, г. Минск, ул. Тимирязева, д. 121/3, к. 214, тел./факс: 208 80 01.
Подписано в печать 23.05.23. Формат 70×100/16. Бумага офсетная. Усл. п. л. 42,570. Тираж 700. Заказ 0000.

Краткое содержание

Благодарности................................................ 18
Предисловие.................................................. 20
Введение..................................................... 24
Глава 1. Внутренний язык компьютеров.............................. 38
Глава 2. Комбинаторная логика................................... 74
Глава 3. Последовательная логика................................ 111
Глава 4. Анатомия компьютера................................... 136
Глава 5. Архитектура компьютера................................. 161
Глава 6. Разбор связей.......................................... 186
Глава 7. Организация данных.................................... 233
Глава 8. Обработка языка....................................... 273
Глава 9. Веб-браузер........................................... 296
Глава 10. Прикладное и системное программирование............... 320
Глава 11. Сокращения и приближения............................. 347
Глава 12. Взаимоблокировки и состояния гонки...................... 400
Глава 13. Безопасность......................................... 418
Глава 14. Машинный интеллект................................... 459
Глава 15. Влияние реальных условий.............................. 491

Оглавление

Об авторе.................................................... 17
О научном редакторе........................................... 17

Благодарности................................................ 18
Предисловие.................................................. 20
От издательства............................................... 23
Введение..................................................... 24
Почему важно программировать хорошо............................ 25
Научиться писать код — только начало.............................. 26
Низкоуровневые знания важны.................................... 27
Кому стоит прочитать эту книгу?................................... 28
Что такое компьютеры?.......................................... 28
Что такое программирование компьютеров?......................... 29
Кодинг, программирование, инженерия и сomputer science.............. 31
Ландшафт.................................................... 33
Структура книги............................................... 36
Глава 1. Внутренний язык компьютеров.............................. 38
Что такое язык?................................................ 38
Письменный язык.............................................. 39
Бит......................................................... 40
Логические операции........................................... 40
Булева алгебра............................................. 41
Закон де Моргана.......................................... 42
Представление целых чисел с помощью битов........................ 43
Представление положительных чисел............................ 43
Сложение двоичных чисел.................................... 46
Представление отрицательных чисел............................ 48

Оглавление   7

Представление действительных чисел............................... 54
Представление с фиксированной точкой......................... 54
Представление с плавающей точкой............................. 55
Стандарт IEEE для чисел с плавающей точкой...................... 57
Двоично-десятичная система счисления............................. 58
Более простые способы работы с двоичными числами.................. 59
Восьмеричное представление.................................. 59
Шестнадцатеричное представление............................. 59
Представление контекста..................................... 60
Именованные группы битов...................................... 61
Представление текста........................................... 63
Американский стандартный код обмена информацией.............. 63
Развитие других стандартов................................... 65
8-битная форма представления Unicode......................... 66
Использование символов для представления чисел.................... 67
Кодировка Quoted-Printable................................... 67
Кодировка Base64.......................................... 68
Кодировка URL............................................. 69
Представление цветов.......................................... 69
Добавление прозрачности.................................... 72
Кодирование цветов......................................... 73
Выводы...................................................... 73

Глава 2. Комбинаторная логика................................... 74
Задача для цифровых компьютеров................................ 75
Разница между аналоговым и цифровым представлением............ 76
Почему для аппаратного обеспечения размер имеет значение........ 78
Цифровые решения для более стабильных устройств................ 79
Цифровые устройства в аналоговом мире........................ 80
Почему вместо цифр используются биты......................... 82
Знакомство с принципами работы электрического тока................. 83
Электрический ток на примере сантехники....................... 83
Электрические переключатели................................. 86
Создание аппаратного обеспечения, работающего с битами............ 90
Реле..................................................... 90
Вакуумные лампы........................................... 93
Транзисторы............................................... 94
Интегральные схемы......................................... 95
Логические вентили............................................ 96
Повышение помехоустойчивости с помощью гистерезиса............ 97
Дифференциальная передача сигналов.......................... 99

8   Оглавление
Задержка распространения.................................. 100
Варианты выходов......................................... 101
Создание более сложных схем................................... 104
Создание сумматора....................................... 104
Построение дешифраторов.................................. 107
Построение демультиплексоров............................... 108
Построение селекторов..................................... 108
Выводы..................................................... 110

Глава 3. Последовательная логика................................ 111
Представление времени........................................ 111
Осцилляторы............................................. 112
Генераторы тактовых сигналов................................ 113
Триггеры-защелки.......................................... 113
Синхронный RS-триггер..................................... 115
Триггеры................................................. 116
Счетчики................................................. 119
Регистры................................................. 121
Организация памяти и обращение к памяти......................... 122
Оперативная память........................................ 125
Постоянное запоминающее устройство......................... 126
Блочные устройства........................................... 129
Флеш-память и твердотельные диски.............................. 132
Обнаружение и исправление ошибок............................. 133
Аппаратное и программное обеспечение........................... 134
Выводы..................................................... 135
Глава 4. Анатомия компьютера................................... 136
Память..................................................... 136
Ввод и вывод................................................. 139
Центральный процессор........................................ 140
Арифметико-логическое устройство............................ 140
Сдвиг................................................... 143
Исполнительное устройство.................................. 144
Набор инструкций............................................ 146
Инструкции............................................... 146
Режимы адресации......................................... 149
Инструкции кода состояния.................................. 150
Ветвление................................................ 150
Итоговый набор инструкций.................................. 151

Оглавление    9

Окончательный проект......................................... 153
Регистр команд............................................ 153
Передача данных и управляющие сигналы....................... 154
Управление движением...................................... 154
Наборы команд RISC и CISC..................................... 158
Графические процессоры....................................... 160
Выводы..................................................... 160

Глава 5. Архитектура компьютера................................. 161
Основные архитектурные элементы............................... 162
Ядра процессора.......................................... 162
Микропроцессоры и микрокомпьютеры......................... 163
Процедуры, подпрограммы и функции............................. 164
Стеки...................................................... 166
Прерывания................................................. 170
Относительная адресация...................................... 173
Блок управления памятью....................................... 175
Виртуальная память........................................... 177
Пространство системы и пользователя............................. 178
Иерархия памяти и производительность............................ 179
Сопроцессоры............................................... 181
Организация данных в памяти................................... 182
Запуск программ............................................. 183
Мощность запоминающих устройств.............................. 184
Выводы..................................................... 185
Глава 6. Разбор связей.......................................... 186
Низкоуровневый ввод/вывод.................................... 187
Порты ввода/вывода....................................... 187
Нажми на кнопку.......................................... 189
Да будет свет............................................. 192
Свет, камера, мотор…...................................... 193
Светлые идеи............................................. 194
2n оттенка серого.......................................... 195
Квадратура.............................................. 196
Параллельная связь........................................ 197
Последовательная связь..................................... 198
Поймать волну............................................ 201
Универсальная последовательная шина......................... 202

10   Оглавление
Сети....................................................... 203
Современные локальные сети................................. 205
Интернет................................................. 206
Аналоговые устройства в цифровом мире.......................... 207
Цифро-аналоговое преобразование........................... 208
Аналого-цифровое преобразование........................... 210
Цифровое аудио.......................................... 214
Цифровые изображения..................................... 222
Видео................................................... 224
Устройства взаимодействия с человеком........................... 226
Терминалы............................................... 226
Графические терминалы..................................... 228
Векторная графика......................................... 228
Растровая графика......................................... 230
Клавиатура и мышь........................................ 232
Выводы..................................................... 232

Глава 7. Организация данных.................................... 233
Базовые типы данных.......................................... 233
Массивы.................................................... 235
Битовые матрицы............................................. 237
Строки..................................................... 238
Составные типы данных........................................ 240
Односвязные списки........................................... 242
Динамическое выделение памяти................................. 247
Более эффективное выделение памяти............................. 248
Сборка мусора............................................... 249
Двусвязные списки............................................ 250
Иерархические структуры данных................................ 251
Хранение данных на дисковых устройствах......................... 255
Базы данных................................................. 258
Индексы.................................................... 259
Перемещение данных.......................................... 260
Векторный ввод/вывод......................................... 264
Подводные камни объектно-ориентированного программирования....... 265
Сортировка................................................. 267
Создание хешей.............................................. 268
Эффективность и производительность............................. 271
Выводы..................................................... 272

Оглавление    11

Глава 8. Обработка языка....................................... 273
Язык ассемблера............................................. 273
Языки высокого уровня......................................... 275
Структурное программирование................................. 276
Лексический анализ........................................... 277
Конечные автоматы......................................... 279
Регулярные выражения...................................... 281
От слов к предложениям........................................ 283
Клуб «Язык дня».............................................. 285
Деревья синтаксического анализа................................ 286
Интерпретаторы.............................................. 289
Компиляторы................................................. 291
Оптимизация................................................ 293
Осторожнее с аппаратной частью!................................ 295
Выводы..................................................... 295
Глава 9. Веб-браузер........................................... 296
Языки разметки............................................... 297
Унифицированные указатели ресурсов............................ 299
HTML-документы.............................................. 300
Объектная модель документа.................................... 302
Словарь древовидных структур данных......................... 303
Интерпретация модели DOM................................. 303
Каскадные таблицы стилей...................................... 304
XML и друзья................................................. 309
JavaScript................................................... 312
jQuery...................................................... 314
SVG....................................................... 316
HTML5..................................................... 317
JSON...................................................... 317
Выводы..................................................... 318
Глава 10. Прикладное и системное программирование............... 320
«Угадай животное», версия 1: HTML и JavaScript...................... 323
Каркас прикладного уровня.................................. 323
Тело веб-страницы......................................... 325
JavaScript................................................ 326
CSS..................................................... 328

12   Оглавление
«Угадай животное», версия 2: С.................................. 329
Терминалы и командная строка............................... 329
Создание программы....................................... 330
Терминалы и драйверы устройств.............................. 330
Переключение контекста.................................... 331
Стандартный ввод/вывод.................................... 333
Кольцевые буферы......................................... 334
Больше абстракций — лучше код.............................. 336
Важные мелочи............................................ 337
Переполнение буфера...................................... 338
Программа на языке С...................................... 339
Тренировка............................................... 345
Выводы..................................................... 346

Глава 11. Сокращения и приближения............................. 347
Поиск по таблице............................................. 347
Преобразование.......................................... 348
Отображение текстур....................................... 349
Классификация символов.................................... 352
Целочисленные методы......................................... 354
Прямые линии............................................. 357
Кривые линии............................................. 362
Многочлены.............................................. 365
Рекурсивное деление.......................................... 366
Спирали................................................. 366
Конструктивная геометрия................................... 369
Сдвиг и наложение масок.................................... 376
Еще меньше математики........................................ 378
Приближения степенного ряда................................ 378
Алгоритм Волдера......................................... 379
Парочка случайностей......................................... 384
Заполняющие пространство кривые............................ 385
L-системы................................................ 387
Стохастические приемы..................................... 389
Квантование.............................................. 390
Выводы..................................................... 399
Глава 12. Взаимоблокировки и состояния гонки...................... 400
Что такое состояние гонки?...................................... 401
Общие ресурсы.............................................. 401

Оглавление   13

Процессы и потоки............................................ 402
Блокировки.................................................. 404
Транзакции и детализация................................... 405
Ожидание захвата ресурсов................................. 406
Взаимоблокировки......................................... 407
Реализация кратковременного захвата ресурсов.................. 408
Реализация долговременного захвата ресурсов................... 409
JavaScript в браузере.......................................... 409
Асинхронные процессы и промисы................................ 413
Выводы..................................................... 417

Глава 13. Безопасность......................................... 418
Обзор безопасности и конфиденциальности........................ 419
Модель угроз............................................. 419
Доверие................................................. 420
Физическая безопасность................................... 423
Безопасность связей........................................ 424
Наше время.............................................. 425
Метаданные и наблюдение.................................. 427
Социальный контекст....................................... 428
Аутентификация и авторизация............................... 430
Криптография................................................ 431
Стеганография............................................ 431
Шифры подстановки........................................ 433
Шифры перестановки....................................... 435
Более сложные шифры...................................... 436
Одноразовые блокноты..................................... 437
Проблема обмена ключами.................................. 438
Криптография с открытым ключом............................. 438
Прямая секретность........................................ 439
Криптографические хеш-функции.............................. 440
Цифровые подписи......................................... 441
Инфраструктура открытых ключей............................. 441
Блокчейн................................................. 442
Управление паролями....................................... 443
Гигиена ПО.................................................. 444
Защищайте только необходимое.............................. 444
Проверьте логику трижды.................................... 444
Поищите ошибки.......................................... 445
Сведите к минимуму поверхности атаки......................... 445

14   Оглавление
Не выходите за пределы..................................... 446
Генерировать хорошие случайные числа — сложно................ 447
Знайте свой код........................................... 449
Чрезвычайная изобретательность — ваш враг.................... 451
Разберитесь с видимостью................................... 451
Не переусердствуйте....................................... 452
Не копите................................................ 452
Не полагайтесь на динамическое выделение памяти............... 452
Не полагайтесь и на сборку мусора............................ 454
Данные как код............................................ 456
Выводы..................................................... 458

Глава 14. Машинный интеллект................................... 459
Обзор...................................................... 460
Машинное обучение.......................................... 463
Байес................................................... 463
Гаусс.................................................... 465
Собель.................................................. 468
Кэнни................................................... 472
Выделение признаков....................................... 475
Нейронные сети........................................... 477
Использование данных машинного обучения..................... 482
Искусственный интеллект (ИИ)................................... 484
Большие данные.............................................. 487
Выводы..................................................... 490
Глава 15. Влияние реальных условий.............................. 491
Повышение ценности.......................................... 492
Как мы до этого дошли......................................... 494
Краткая история........................................... 494
ПО с открытым исходным кодом............................... 497
Creative Commons.......................................... 499
Расцвет переносимости..................................... 500
Управление пакетами....................................... 500
Контейнеры............................................... 501
Java.................................................... 502
Node.js.................................................. 503
Облачные вычисления...................................... 504
Виртуальные машины....................................... 504
Портативные устройства..................................... 505

Оглавление   15

Среда разработки............................................ 505
Есть ли у вас опыт?......................................... 506
Учимся оценивать.......................................... 506
Планируем проекты........................................ 507
Принимаем решения....................................... 508
Работаем с разными людьми................................. 508
Создаем культуру поведения на работе......................... 510
Делаем осознанный выбор................................... 511
Методологии разработки....................................... 511
Проектирование.............................................. 513
Ведение записей........................................... 513
Быстрое прототипирование.................................. 513
Разработка интерфейса..................................... 514
Использовать сторонний код или писать собственный?............. 518
Разработка.................................................. 519
Серьезный разговор........................................ 519
Переносимый код.......................................... 522
Управление версиями....................................... 523
Тестирование............................................. 524
Создание отчетов и отслеживание багов........................ 524
Рефакторинг.............................................. 524
Обслуживание............................................ 525
Позаботьтесь о стиле.......................................... 525
Чините, а не создавайте заново.................................. 527
Выводы..................................................... 527

Джули и Ханалей за то, что научили меня
объяснять принципы работы
сложных технологий простым людям.
Удивительному месту — Bell Telephone Laboratories —
и всем, кто там работал, особенно Карлу, за то,
что он подыскал для меня там местечко,
когда я был еще подростком.

Об авторе
Джонатан Э. Стейнхарт (Jonathan E. Steinhart) занимается разработкой
с 1960-х годов. Он проектировал оборудование, обучаясь в средней школе,
а программное обеспечение — в старших классах, что помогло ему найти подработку на лето в Bell Telephone Laboratories. Он получил степень бакалавра в области электротехники и сomputer science в Университете Кларксона (Clarkson
University) в 1977 году. После выпуска Джонатан работал в Tektronix, а затем
стал пробовать свои силы в компаниях-стартапах. Он стал консультантом
в 1987 году, специализируясь на проектировании систем с повышенными требованиями к безопасности. В 1990-х он немного сбавил обороты — но только
для того, чтобы основать Four Winds Vineyard.

О научном редакторе
Обри Андерсон (Aubrey Anderson) получил степень бакалавра в области электротехники и сomputer science в Университете Тафтса (Tufts University). Во время
учебы он работал ассистентом кафедры и помогал улучшать учебные программы вводных курсов по компьютерным наукам. Он начал программировать
в 14 лет и с тех пор вел проекты в области робототехники, системного дизайна
и веб-программирования. В настоящее время Обри работает инженером-программистом в Google.

Благодарности

К созданию этой книги приложили руку множество людей. Все началось с моих
родителей, Роберта (Robert) и Розалин Стейнхарт (Rosalyn Steinhart), — благодаря им я появился на свет, и они же затем поощряли мой интерес к науке, по
крайней мере, пока он не начал их пугать. Много замечательных учителей не
позволили этому интересу угаснуть, в том числе Беатрис Сигал (Beatrice Seagal),
Уильям Малвахилл (William Mulvahill) и Миллер Бульяри (Miller Bugliari).
Большое спасибо Полу Рубенфилду (Paul Rubenfield) за то, что он рассказал
мне о гражданской обороне и об обществе скаутов Explorer Scout в Bell Labs.
Невозможно отплатить сполна моим помощникам в Explorer Scout, Карлу
Кристиансену (Carl Christiansen) и Хайнцу Ликламе (Heinz Lycklama). Они
изменили мою жизнь. Благодаря им я познакомился со многими удивительными людьми в Bell Telephone Laboratories, включая Джо Кондона (Joe Condon),
Сэнди Фрейзера (Sandy Fraser), Дэйва Хагельбаргера (Dave Hagelbarger), Дика
Хауса (Dick Hause), Джима Кайзера (Jim Kaiser), Хэнка Макдональда (Hank
McDonald), Макса Мэтьюза (Max Mathews), Денниса Ричи (Dennis Ritchie),
Кена Томпсона (Ken Thompson) и Дэйва Веллера (Dave Weller). Я многому
научился у каждого из них.
Спасибо Обри Андерсону (Aubrey Anderson), Клему Коулу (Clem Cole), Ли
Джаловеку (Lee Jalovec), A. C. Мендионесу (A.C. Mendiones), Эду Посту (Ed
Post) и Бетси Зеллер (Betsy Zeller) за то, что они хоть раз прочитали мою книгу.
И особенно Обри за научную редактуру.
Также спасибо Мэтту Блейзу (Matt Blaze), Адаму Чеккетти (Adam Cecchetti),
Сэнди Кларк (Sandy Clark), Тому Даффу (Tom Duff), Натали Фрид (Natalie Freed),
Фрэнку Хайдту (Frank Heidt), Д. В. Хенкель-Уоллесу (DV Henkel-Wallace) (он

Благодарности   19
же Гамби), Лу Кацу (Lou Katz), Саре-Джей Терп (Sara-Jaye Terp), Талин (Talin)
и Полу Викси (Paul Vixie) за отзывы об отдельных главах.
И спасибо всем, кто отвечал на звонки, когда я хотел получить ответы на общие
вопросы, включая Уорда Каннингема (Ward Cunningham), Джона Гилмора (John
Gilmore), Эвелин Мэст (Evelyn Mast), Майка Перри (Mike Perry), Алекса Полви
(Alex Polvi), Алана Вирфс-Брока (Alan Wirfs-Brock) и Майка Зула (Mike Zuhl).
И конечно же, Ракель Хеллберг (Rakel Hellberg), девушке на горнолыжном подъемнике, за то, что она подтолкнула меня к завершению этого проекта.
Эта книга не увидела бы свет без поддержки и поощрения людей из различных
компьютерных сообществ, включая AMW, Hackers и TUHS.
Спасибо Ханалей Стейнхарт (Hanalei Steinhart) за композицию на рис. 6.36
и Джули Доннелли (Julie Donnelly) за шарф на рис. 11.41.
Спасибо коту Тони за то, что позволил использовать его фото, и за шерсть на
моей клавиатуре.

Предисловие

Я родился гиком. Отец рассказывал, что я переключал
воображаемый тумблер, чтобы включить качели, перед
тем как на них сесть, и выключал их, когда переставал
качаться. Механизмы сами рассказывали мне, как они
устроены изнутри. Я напоминал C-3PO, понимающего
«бинарный код испарителей». Мне повезло, что я вырос
в то время, когда можно было изучать работу большинства
вещей без микроскопа.
Оглядываясь назад, я понимаю, что в Нью-Джерси у меня было невероятное
детство. Я разбирал все, что можно, часто испытывая на прочность мамины
нер­вы. Родители покупали мне множество наборов «50 в одном», но им стало не
по себе, когда я начал объединять их и собирать то, чего не было в инструкции.
Кульминацией стала охранная сигнализация для подушки, которая застала
зубную фею «на месте преступления», — неудачное решение в экономическом
смысле, но тем не менее подарившее мне массу эмоций. Я таскал сломанные
телевизоры и другую бытовую технику из мусорных баков, чтобы разбирать
их, изучать, как они работают, и создавать новые из частей старых. Одной из
моих любимых игрушек был отцовский конструктор 1929 года. Космическая
программа еще больше подогрела мой интерес к технологиям; я помню, как
однажды ночью мы с отцом стояли во дворе перед домом и смотрели, как по
небу движется спутник «Эхо-1».
Большинство детей занимались разноской газет; я ремонтировал телевизоры
и стереосистемы. Мой отец работал в IBM, и я иногда ходил с ним на работу
и восхищался большими компьютерами. Он взял меня с собой на электрошоу

Предисловие   21
в Атлантик-Сити, когда мне было восемь лет, и я помню, как играл с IBM 1620.
Я также помню, как меня восхищало оборудование на стенде Tektronix — оно,
возможно, повлияло на мой дальнейший выбор работы. Год спустя я посетил
Всемирную выставку в Нью-Йорке и был очарован стендом Bell System; позже
мне довелось работать с одним из его проектировщиков.
Я получил прекрасное образование в государственной школе пост-«спутни­
кового» образца — таких больше нет в Америке. В пятом классе мы передавали
из рук в руки банку со ртутью. В шестом классе я взорвал химическую лабораторию и извлек из этого урок. (Я до сих пор могу назвать формулу получения
трехйодистого азота.) Я помню, как в восьмом классе учитель физики отвез
нас в Нью-Йорк, чтобы показать фильм «Космическая одиссея 2001 года», потому что он считал это важным. Он сделал это, не оставив записок родителям
и не получив разрешений; учитель, который сотворит подобное сегодня, скорее
всего, потеряет работу или того хуже. На химии в старших классах мы делали
порох, на физике пускали друг в друга ракеты на футбольном поле, на биологии
протыкали пальцы, чтобы определить группу крови. Это так непохоже на день
сегодняшний, когда столько людей уже утонуло в ведре воды, что теперь на
ведрах пишут предупреждения, когда мокрые полы вселяют страх, а чиновники
настолько не разбираются в науке, что не могут отличить лабораторный опыт
от теракта.
Помимо учебы родители записали меня в школу бойскаутов, которая мне нравилась, и в Малую (бейсбольную. — Примеч. ред.) лигу, которую я ненавидел.
Скаутинг научил меня многому — от верховой езды до безопасного обращения
с огнем и выживания в дикой природе. Малая лига научила меня нелюбви
к командным видам спорта.
В те дни были популярны кружки радиолюбителей; они были одними из тех
мест, где можно было ставить опыты. Я пошел волонтером в местную группу
экстренной радиосвязи гражданской обороны, специально чтобы повозиться
с оборудованием. У них была примитивная радиотелеграфная система, которую я переделал и в итоге создал такие же для других районов. Мне нравилось
трехмерное механическое устройство, которое собой представлял телеграфный
аппарат.
Когда я учился в старшей школе, друг рассказал мне о скаутском отряде Explorer,
который собирался каждый понедельник вечером в Bell Telephone Laboratories
в соседнем Марри-Хилле. Я присоединился к нему и начал заниматься компьютерами, когда те еще были размером с дом. Меня зацепило. Вскоре я стал
рано уходить из школы, добираться автостопом до лабораторий и уговаривать
сотрудников впустить меня. Это превратилось в серию потрясающих летних
подработок с невероятными людьми, изменившими мою жизнь. Я многому
­научился, просто заглядывая в лаборатории и спрашивая сотрудников, что они
делают. В конце концов, я написал для них программы, хотя планировал изучать

22   Предисловие
электротехнику, потому что проекты, связанные с оборудованием, просто нельзя
было завершить за лето.
Я чувствовал, что лучший способ отдать дань своим соратникам-скаутам — это
пойти по их стопам, пытаясь по мере возможности помочь новым поколениям
подающих надежды молодых технарей на их пути. Это оказалось непросто,
поскольку расцвет американских исследований уступил место увеличению
акционерной стоимости; сами продукты не ценятся так высоко, как прибыль,
которую они приносят, — из-за этого трудно обосновать важность исследований.
Компании редко позволяют детям разгуливать в производственных помещениях,
потому что это большая ответственность. Изначально я думал, что буду работать
через скаутинг, но понял, что не смогу, потому что скауты приняли некоторые
правила, которые я не мог поддержать, так как никогда не получал значка за
дискриминацию по половому признаку. Вместо этого я записался добровольцем
в местную школу.
Я начал писать эту книгу, чтобы дополнить курс, который вызвался вести. Я сделал это до того, как интернет стал таким же легкодоступным, как сегодня. Я живу
в довольно бедном фермерском регионе, поэтому первоначальный проект этой
книги включал в себя почти все, что можно, поскольку я исходил из того, что
ученики не смогут позволить себе дополнительные материалы. Но объять все
оказалось невозможно.
Сейчас в интернете доступно множество материалов о языках программирования и концепциях, и у большинства людей есть доступ в интернет дома, в школе
или в библиотеке. Я переписал материал в надежде, что теперь читателям будет
намного легче найти дополнительную информацию в сети. Итак, если вам что-то
неясно или нужны подробности, просто погуглите.
Недавно несколько моих знакомых студентов выразили разочарование по поводу того, как их учат программированию. Хотя они могут найти информацию
в интернете, они все время спрашивают, есть ли место, в котором собрано все необходимое. Эта книга как раз и написана, чтобы стать таким единым источником.
Мне повезло, что я вырос одновременно с компьютерами. Мы развивались
вместе. Мне трудно представить, каково это — перейти сразу в зрелую сферу современных вычислений, не имея опыта. Самым сложным в написании этой книги
было решить, насколько далеко можно вернуться в прошлое ради примеров и какие элементы современных технологий выбрать для обсуждения. Я остановился
на некоторых ретрообразцах, поскольку большую часть необходимого можно
извлечь из более старых и простых технологий, которые легче понять. Новые,
более сложные технологии строятся из тех же блоков, что и старые; знание этих
блоков значительно упрощает понимание новых технологий.
Время изменилось. Гаджеты стало гораздо сложнее разбирать, ремонтировать
и модифицировать. Компании нарушают законы, такие как Закон о защите

От издательства   23
авторских прав в цифровую эпоху (Digital Millennium Copyright Act, DMCA),
чтобы запретить людям ремонтировать принадлежащие им устройства, что,
к счастью, иногда приводит к законам о «праве на ремонт». Мы, американцы,
получаем неоднозначные сообщения от правительства; с одной стороны, нас
поощряют за карьеру в технической сфере, а с другой — мы видим, как игнорируют научный подход, а технические функции передают на аутсорсинг.
Неясно, стали бы США мощной технологической державой, если бы так было
и полвека назад.
Но есть и положительные моменты. Пространства для творчества множатся.
Некоторым детям разрешают создавать вещи, и малыши обнаруживают, что это
весело. Электронные детали дешевле, чем когда-либо (это не касается деталей
с проводами). Вычислительная мощность современного смартфона больше, чем
у всех компьютеров в мире во времена моего детства, вместе взятых. Компьютеры дешевле, чем кто-либо мог себе представить; микрокомпьютеры, такие как
Raspberry Pi и Arduino, стоят дешевле пиццы и имеют огромное количество
доступных начинок.
Имея такую доступную мощность, заманчиво просто побаловаться высокоуровневой функциональностью. Это похоже на игру с LEGO. Мои родители
подарили мне один из первых наборов LEGO; в нем были только прямоугольники. Но с помощью воображения я мог построить все, что хотел. Сегодня вы
можете купить набор LEGO «Звездные войны», чтобы собрать модель мастера
Йоды. Придумывать новых персонажей гораздо сложнее. Необычные вещи
мешают воображению.
В классическом фильме 1939 года «Волшебник страны Оз» есть отличная сцена,
в которой волшебник появляется и кричит: «Не обращайте внимания на того
человека за ширмой». Эта книга для тех, кто не собирается его слушать и хочет
узнать, что скрывается за ширмой. Моя цель — пролить свет на главные кирпичики, из которых строится высокоуровневая функциональность. Эта книга для
тех, чье воображение не удовлетворяется только функциональностью высокого
уровня; она для тех, кто стремится создавать новые высокоуровневые функции.
Если вы хотите стать волшебником, а не только владеть магическими предметами, то эта книга для вас.

От издательства
Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com
(издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На веб-сайте издательства www.piter.com вы найдете подробную информацию
о наших книгах.

Введение

Несколько лет назад я ехал на подъемнике вместе с нашей шведской студенткой по обмену. Я спросил ее, думала ли она о том, чем будет заниматься после выпуска.
Она сказала, что подумывает об инженерии и в прошлом
году ходила на курсы программирования. Я поинтересовался, чему они учат. Она ответила: «Java». Я инстинктивно
отреагировал: «Очень жаль».
Почему я так сказал? Мне потребовалось время, чтобы понять это. Дело не
в том, что Java — плохой язык программирования; он на самом деле довольно
неплох. Просто он (как и другие языки) обычно используется для обучения
программированию без передачи каких-либо знаний о компьютерах. Если вам
это кажется странным, моя книга — для вас.
Язык программирования Java был создан в 1990-х годах Джеймсом Гослингом
(James Gosling), Майком Шериданом (Mike Sheridan) и Патриком Нейтоном
(Patrick Naughton) из Sun Microsystems. Частично он был смоделирован по
образцу языка C, который широко использовался в то время. Язык C не поддерживает автоматическое управление памятью, и потому связанные с этим
ошибки были настоящей головной болью. Java намеренно исключил данный
класс ошибок программирования; он скрыл от программиста управление памятью. В том числе поэтому он очень хорош для начинающих. Но для подготовки
компетентных специалистов и создания качественных программ требуется
нечто гораздо большее, чем просто хороший язык. Кроме того, оказалось, что
Java привнес целый класс новых проблем программирования, которые труднее
поддаются отладке, включая низкую производительность из-за скрытой системы
управления памятью.

Почему важно программировать хорошо   25
Как вы увидите в этой книге, понимание памяти — ключевой навык для программистов. Учась программировать, легко выработать привычки, от которых
потом трудно избавиться. Исследования показали, что дети, которые выросли,
играя на так называемых «безопасных» площадках, чаще травмируются в старшем возрасте, чем остальные (предположительно потому, что такие дети не
знают, что падение — это больно). Аналогичная ситуация с программированием.
Безопасная среда программирования снимает страх перед началом работы, но,
помимо этого, нужно подготовиться к реальным условиям внешней среды. Эта
книга поможет осуществить такой переход.

Почему важно программировать хорошо
Чтобы понять, почему сложно преподавать программирование без обучения
тому, как работают компьютеры, сначала подумайте, как прочно компьютеры
вошли в нашу жизнь. Цена на них упала настолько резко, что компьютер стал
самым дешевым способом создания множества вещей. Например, для вывода на
приборную панель автомобиля устаревшего аналогового циферблата дешевле
использовать компьютер, чем настоящие механические часы. Это результат
того, как производятся компьютерные микросхемы; они печатаются огромными
партиями. Выпустить чип, содержащий миллиарды компонентов, больше не
составляет труда. Заметьте, что я говорю о цене самих компьютеров, а не о цене
объектов, в которые они включены. В целом компьютерный чип сегодня стоит
меньше, чем упаковка, в которой он поставляется. Доступны компьютерные
чипы, которые стоят копейки. Скорее всего, наступит время, когда будет сложно
найти устройство, в котором нет электроники.
Огромное количество компьютеров, выполняющих массу задач, означает множество компьютерных программ. Поскольку компьютеры так широко распространены, программирование невероятно разнообразно. Подобно врачам, многие
программисты становятся узкими специалистами. Можно сосредоточиться на
таких областях, как техническое зрение, анимация, веб-страницы, телефонные
приложения, промышленное управление, медицинские устройства и многое
другое.
Но что самое странное — в отличие от медицины, в программировании можно
стать узким специалистом, не являясь универсалом. Скорее всего, вам не нужен
кардиохирург, который никогда не изучал анатомию, но среди программистов
подобное — норма. Так ли это важно? На самом деле, множество фактов свидетельствуют о том, что это не очень хорошо, учитывая почти ежедневные сообщения о нарушениях безопасности и отзыве продуктов. Случалось, что люди,
осужденные за вождение в нетрезвом виде на основе данных алкотестера, выигрывали суды о пересмотре кода алкотестера. Оказывалось, что код содержал
массу багов, и обвинения в таких случаях снимались. Недавно антивирусная
программа вызвала отказ медицинского оборудования во время операции на

26   Введение
сердце. Проблемы с конструкцией самолета Boeing 737 MAX привели к человеческим жертвам. Большое количество подобных инцидентов не внушает
особого доверия.

Научиться писать код — только начало
Одна из причин такого положения дел в том, что не так уж и сложно написать
программу, которая кажется работоспособной или выполняется без проблем
большую часть времени. Возьмем для сравнения изменения в музыке (не диско!) 1980-х годов. Раньше людям приходилось потрудиться, прежде чем писать музыку. Изучить теорию музыки, композицию и освоить музыкальный
инструмент, натренировать слух и провести много часов за практикой. Затем
появился стандарт цифрового интерфейса музыкальных инструментов (Musical
Instrument Digital Interface, MIDI), предложенный Икутаро Какехаши (Ikutaro
Kakehashi) из Roland и позволивший любому человеку создавать «музыку» на
компьютере — без усилий. Я считаю, что лишь малая доля таких «произведений»
является музыкой на самом деле; по большей части это шум. Музыку создают
настоящие музыканты вне зависимости от того, применяют они MIDI для
записи основы или нет. В наши дни программирование во многом похоже на
использование MIDI. Больше не нужно потеть от усердия, или тратить годы на
практику, или даже изучать теорию, чтобы писать программы. Но это не значит,
что они будут хороши или надежны.
Ситуация может стать еще хуже, по крайней мере в Соединенных Штатах.
Корыстные инвесторы, такие как владельцы компаний-разработчиков программного обеспечения, лоббируют закон, обязывающий изучать программирование уже в школе. В теории это звучит великолепно, но на практике это
не лучшая идея, потому что не у всех есть способность к программированию.
Мы не требуем, чтобы все учились играть в футбол, потому что знаем, что он не
для всех. Вероятная цель этой инициативы не в том, чтобы готовить отличных
программистов, а в том, чтобы увеличить прибыль компании-разработчика
за счет наводнения рынка множеством неквалифицированных специалистов,
что приведет к снижению заработной платы. Люди, стоящие за этой идеей, не
очень заботятся о качестве кода — они также настаивают на принятии закона,
ограничивающего ответственность за некачественные продукты. Конечно, вы
можете программировать для развлечения так же, как и играть в футбол. Просто
не ждите, что вас выберут на Суперкубок.
В 2014 году президент Обама сказал, что научился программировать. Он действительно переместил пару элементов в превосходном инструменте визуального программирования Blockly и даже напечатал строку кода на JavaScript
(язык программирования, не связанный с Java и изобретенный в компании
Netscape, предшественнице Mozilla Foundation, поддерживающей многочисленные программные пакеты, включая веб-браузер Firefox). Как вы думаете,

Низкоуровневые знания важны   27
он действительнонаучился программировать? Подсказка: если вы ответите
да, вам, вероятно, следует вдобавок к чтению этой книги поработать над оттачиванием навыков критического мышления. Наверняка, он кое-что узнал
о программировании, но нет, он не научился программировать. Если бы он мог
научиться программировать за час, это бы значило, что программирование очень
простая штука и его не нужно преподавать в школах.

Низкоуровневые знания важны
Интересное и несколько отличающееся от общепринятого мнение о том, как обу­
чать программированию, было выражено в статье из блога Стивена Вольфрама
(Stephen Wolfram), создателя Mathematica и языка Wolfram, под названием «Как
научить вычислительному мышлению». Вольфрам определяет такое мышление
как «формулирование инструкций с достаточной ясностью и достаточным
систематическим подходом, чтобы передавать их компьютеру». Я полностью
согласен с этим определением. Фактически оно во многом мотивировало меня
на написание этой книги.
Но я категорически не поддерживаю позицию Вольфрама, согласно которой
будущие программисты должны развивать навыки вычислительного мышления
с помощью мощных высокоуровневых инструментов (таких, как те, которые
он разработал), а не изучать лежащие в основе дисциплины фундаментальные
технологии. Например, из растущего интереса к статистике, а не к расчетам
становится ясно, что «обработка данных» — это развивающаяся область. Но что
происходит, когда люди просто загружают груды данных в причудливые программы, принцип работы которых не понимают до конца?
Есть вероятность, что они получают интересные, но бессмысленные или неверные результаты. Например, недавнее исследование («Gene Name Errors Are
Widespread in the Scientific Literature» («Распространенные ошибки в названиях
генов в научной литературе». — Примеч. ред.) Марка Циманна (Mark Ziemann),
Йотама Эрена (Yotam Eren) и Ассама Эль-Оста (Assam El-Osta)) показало, что
пятая часть опубликованных статей по генетике содержит неточности из-за
неправильного использования электронных таблиц. Подумайте только, какие
ошибки и их последствия могут вызвать более мощные инструменты в руках
большего числа людей! Особенно важно принимать правильные решения, если
речь идет о человеческих жизнях.
Понимание базовых технологий помогает определить, что может пойти не
так. Знание только высокоуровневых инструментов ведет к постановке неправильных вопросов. Прежде чем взять в руки гвоздезабивной пистолет, стоит
научиться обращаться с молотком. Еще одна причина для изучения базовых
систем и инструментов заключается в том, что это дает возможность создавать
новые инструменты. Это важно, ведь потребность в создателях будет всегда, даже

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

Кому стоит прочитать эту книгу?
Эта книга предназначена для тех, кто хочет стать отличным программистом.
Что для этого требуется? Прежде всего хорошие навыки критического мышления и анализа. Чтобы решать сложные задачи, программист должен уметь
оценивать, действительно ли его программа решает поставленную задачу. Это
труднее, чем кажется. Нередко опытный профессионал смотрит на чужую программу и язвительно констатирует: «Эта штука сложна, но она не решает даже
простую проблему, которая и не проблема вовсе».
Вам наверняка знаком классический фэнтезийный образ волшебника, который
обретает власть над вещами, узнав их настоящие имена. И горе тем из них, кто
забывает детали. Хороший программист похож на волшебника, способного
держать в уме суть вещей, не опуская мелочей.
Подобно умелым ремесленникам, компетентные программисты — люди творческие. Нередко можно встретить совершенно непонятный код — точно так же
многих англоговорящих сбивает с толку роман Джеймса Джойса «Поминки по
Финнегану». Хорошие программисты пишут код, который не только работает,
но понятен другим и прост в обслуживании.
Наконец, хороший программист должен прекрасно разбираться в том, как работают компьютеры. Невозможно решать сложные задачи, имея лишь поверхностное представление об основах. Эта книга предназначена для тех, кто изучает
программирование, но при этом ощущает отсутствие глубины знаний. Она также
подойдет тем, кто уже занимается программированием, но хочет большего.

Что такое компьютеры?
Обычно в ответ можно услышать что-то вроде: «Компьютеры — это устройства,
с помощью которых люди проверяют электронную почту, совершают онлайнпокупки, работают с документами, сортируют фотографии и играют». Это
определение отчасти является результатом терминологической небрежности,
которая распространилась, когда компьютеры стали потребительским товаром.
Другой популярный ответ: компьютеры — это мозги наших высокотехнологичных игрушек, таких как сотовые телефоны и музыкальные плееры. Второй
вариант ближе к истине.
Отправлять электронную почту и играть в игры можно благодаря программам,
работающим на компьютерах. Сам компьютер похож на новорожденного ребенка. Он на самом деле многого не умеет. Мы почти никогда не задумываемся

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

Что такое программирование компьютеров?
Преподаватели обучают человека выполнять определенные задачи. Точно так
же начать программировать означает стать учителем для компьютеров. Программисты учат компьютеры делать то, что от них хотят.
Умение обучать компьютеры полезно, особенно когда вы хотите, чтобы они
делали нечто новое для них, и вы не можете просто купить нужную программу,
ведь ее еще никто не создал. Например, вы, вероятно, воспринимаете Всемирную
паутину как должное, но она была изобретена не так давно, когда сэру Тиму
Бернерсу-Ли (Tim Berners-Lee) понадобился лучший способ организовать обмен
информацией между учеными из Европейского центра ядерных исследований
(Conseil Européen pour la Recherche Nucléaire, CERN). За это он был посвящен
в рыцари. Круто, не правда ли?
Обучать компьютеры сложно, но это легче, чем учить людей. Мы знаем намного больше о том, как работают компьютеры. И вряд ли техника когда-нибудь
взбунтуется.
Компьютерное программирование включает два этапа.
1. Понять Вселенную.
2. Объяснить ее трехлетнему ребенку.
Что это значит? Невозможно писать компьютерные программы, не разбираясь
в задачах, которые они выполняют. Например, нельзя написать программу для
проверки правописания, не зная правил орфографии, а хорошую видеоигру — не
зная физики. Итак, первый шаг к тому, чтобы стать хорошим программистом, —
узнать как можно больше об окружающем мире. Решения часто приходят откуда не ждали, поэтому не игнорируйте что-то лишь потому, что оно не кажется
актуальным.
Второй шаг процесса требует объяснения того, что вы знаете, машине, которая
буквально воспринимает окружающий мир — как маленький ребенок. Эта детская прямолинейность очень наглядна в возрасте около трех лет. Допустим, вы
пытаетесь выйти из дома и спрашиваете ребенка: «Где твоя обувь?» Ответ: «Вот
она». Ребенок ответил на ваш вопрос. Проблема в том, что он не понимает, что

30   Введение
на самом деле вы просите его надеть туфли, чтобы куда-то пойти. Гибкость и способность делать выводы — навыки, которые дети усваивают по мере взросления.
Но компьютеры похожи на Питера Пэна: они никогда не вырастают.
Компьютеры похожи на маленьких детей тем, что они не умеют обобщать.
Они по-прежнему полезны, потому что, как только вы поймете, как им что-то
объяснить, они будут делать это очень быстро и неутомимо, хотя и не будут
обладать здравым смыслом. Компьютер может без устали делать то, о чем вы
просите, не оценивая, правильно ли это, во многом как заколдованные метлы
в отрывке «Ученик чародея» из мультфильма «Фантазия» 1940 года. Поручать
компьютеру сделать что-то — все равно что просить джинна из волшебной лампы (не в версии ФБР1) исполнить желание. Вы должны быть очень осторожны,
формулируя запрос!
Вы, возможно, сомневаетесь в моих словах, потому что компьютеры кажутся
более способными, чем они есть на самом деле. Например, они умеют рисовать,
исправлять орфографические ошибки, понимать, что вы говорите, проигрывать
музыку и т. д. Но имейте в виду, что это делает не компьютер, а сложный набор
кем-то написанных программ, благодаря которым он выполняет все эти задачи.
Компьютеры функционируют отдельно от программ, которые на них работают.
Это то же самое, что смотреть на машину, которая движется по дороге. Кажется,
что она довольно хорошо останавливается и начинает движение в нужное время,
объезжает препятствия, добирается туда, куда нужно, ест, когда проголодается,
и т. д. Но машина не действует сама по себе. Все это происходит благодаря связке
автомобиля и водителя. Компьютеры подобны машинам, а программы — водителям. Не имея нужных знаний, невозможно сказать, что делает автомобиль,
а что — водитель.
В общем, программирование подразумевает изучение того, что вам нужно знать
для решения задачи, а затем объяснение этих вещей маленькому ребенку. Поскольку существует множество способов решить задачу, программирование — это
не только искусство, но и наука. Оно предполагает поиск элегантных решений,
а не использование грубой силы. Да, вы можете выбраться из дома, пробив дыру
в стене, но, вероятно, намного проще выйти через дверь. Многие могут создать
ресурс наподобие HealthCare.gov в миллионы строк кода, но, чтобы сделать это
в тысячах строк, требуются определенные навыки.
Однако, прежде чем обучать трехлетку, нужно узнать побольше о нем и о том,
что он понимает. И это не обычный ребенок — это инопланетная форма жизни.
Компьютер не играет по тем же правилам, что и мы. Возможно, вы слышали
об искусственном интеллекте (ИИ), который пытается заставить компьютеры
действовать похоже на людей. Прогресс в этой области идет намного медленнее,
1

Отсылка к троянской программе Magic Lantern («Волшебная лампа») для чтения зашифрованной информации на компьютерах подозреваемых. — Примеч. ред.

Кодинг, программирование, инженерия и сomputer science   31
чем предполагалось изначально. В основном потому, что в действительности мы
не понимаем проблемы; мы недостаточно знаем о том, как работает наш мозг.
Довольно сложно научить инопланетянина думать, как мы, если нам самим
неизвестно, каково это.
Человеческий мозг позволяет нам совершать действия бессознательно. Мозг появился как аппаратное обеспечение, которое затем было запрограммировано. Например, вы научились шевелить пальцами, а затем — хватать предметы. После достаточной практики вы просто берете вещи в руки, не задумываясь о механизмах,
которые делают это возможным. Философы, такие как Жан Пиаже (французский
психолог, 1896–1980) и Ноам Хомский (американский лингвист, родившийся
в 1928 году), разработали разные теории о том, как происходит процесс обучения.
Является ли мозг простым инструментом или в нем есть специальные аппаратные
средства для таких функций, как язык? Этот вопрос все еще изучается.
Наша невероятная способность выполнять действия бессознательно мешает
учиться программированию, потому что оно требует разбиения задач на более
мелкие шаги, которые может выполнять компьютер. Например, вы наверняка
умеете играть в крестики-нолики. Соберите группу людей и попросите каждого
самостоятельно перечислить шаги, которые должен предпринять игрок, чтобы
сделать хороший ход для любой конфигурации доски. (Ответ можно найти в интернете, но постарайтесь этого не делать.) После того как все составят списки,
проведите соревнование. Узнайте, чьи правила лучше! А хороши ли были ваши
правила? Что вы пропустили? Правда ли вы знаете, что делаете, когда играете
в игру? Скорее всего, вы не объяснили ряд условий, потому что понимаете их
интуитивно.
Итак, если это еще не очевидно: первый шаг — понимание Вселенной — гораздо
важнее, чем второй — объяснение ее трехлетнему ребенку. Подумайте: что хорошего в том, чтобы уметь говорить, если вы не знаете, что сказать? Несмотря
на это, нынешнее образование делает упор на второй шаг. Все потому, что гораздо легче обучать механическим аспектам задачи, чем творческим, так же как
и оценивать усвоение материала. И в целом учителя не имеют достаточной подготовки в этой области и работают по кем-то созданным и предоставленным им
программам. Однако в этой книге основное внимание уделяется первому шагу.
Хотя книга не может охватить всё вокруг, она исследует задачи и их решения
в компьютерной вселенной вместо разбора точного синтаксиса программирования, необходимого для реализации этих решений.

Кодинг, программирование, инженерия
и сomputer science
Для описания работы с программами используются разные термины. У них нет
точных определений, но есть примерные.

32   Введение
Кодинг — относительно новый термин, ставший популярным как часть «обучения программированию». Кодинг можно в определенном смысле рассматривать
как работу переводчика. Сравним это с использованием кодов Международной
классификации болезней (МКБ). Постановка диагноза врачом — сравнительно простая задача. Сложнее всего перевести этот диагноз в один из более чем
100 000 кодов в стандартах МКБ (МКБ-10 на момент написания книги). Сертифицированный специалист, который выучил эти коды, знает, что, когда врач ставит диагноз «лягнула корова», этому диагнозу нужно присвоить код W55.2XA.
Эта работа на самом деле сложнее, чем большая часть кодинга в программировании, потому что МКБ-кодов огромное множество. Но процесс аналогичен
тому, что сделал бы кодер, если бы его попросили «выделить текст жирным»
на веб-странице: он знает, какой код использовать, чтобы выполнить задание.
Стандарт МКБ-10 настолько сложен, что немногие сертифицированные специа­
листы знают его полностью. Медицинские кодировщики получают сертификаты
по отдельным специальностям, например «Заболевания нервной системы» или
«Психические и поведенческие расстройства». Это аналогично тому, что программисты становятся специалистами по отдельно взятым языкам, например
HTML или JavaScript.
Но программирование, то есть работа программиста, означает знать больше, чем
одну-две области специализации. Врач в этом сценарии подобен программисту.
Врач ставит диагноз, оценивая пациента. Это может быть довольно сложно. Например, если у пациента есть ожоги и он промок насквозь, это «неестественный
внешний вид» (код R46.1) или «ожог горящими водными лыжами, первый
контакт» (код V91.07XA)? После того как врач поставит диагноз, можно разработать план лечения. План должен быть эффективным; врач, вероятно, не
захочет снова принять пациента, страдающего от тяжелой степени «чрезмерной
родительской опеки» (Z62.1).
Программист, как и врач, оценивает проблему и находит решение. Например,
есть потребность в веб-сайте, который позволил бы людям ранжировать коды
МКБ-10 с точки зрения глупости. Программист определит лучшие алгоритмы
хранения и обработки данных, структуру взаимодействия между веб-клиентом
и сервером, пользовательский интерфейс и т. д. Это не простая «вставка кода».
Инженерия — это следующий уровень сложности. В общем, инженерия — это искусство извлекать знания и использовать их для достижения чего-либо. Можно
рассматривать создание стандартов МКБ как инженерную разработку; обширная
область медицинских диагнозов была сведена к набору кодов, которые было
легче отслеживать и анализировать, чем записи врача. Представляет ли такая
сложная система хорошую разработку — вопрос открытый. В качестве примера
компьютерной инженерии — много лет назад я работал над проектом создания
недорогого медицинского монитора, такого как те, которые вы видите в больницах. Мне было поручено создать систему, в которой врач или медсестра могли

Ландшафт   33
разобраться менее чем за 5 минут без чтения документации. Как вы понимаете,
для этого требовалось гораздо больше, чем просто знание программирования.
И я добился цели — знакомство с моим решением занимает около 30 секунд.
Программирование часто путают с сomputer science. В то время как многие
специалисты в области компьютерных наук программируют, большинство программистов не являются специалистами в computer science. Сomputer science —
это наука о компьютерных технологиях и вычислениях. Открытия в этой сфере
используют инженеры и программисты.
Кодинг, программирование, инженерия и сomputer science — независимые, но связанные дисциплины, которые различаются по типу и объему требуемых знаний.
Специалист по компьютерным наукам, разработчик или кодер не становится хорошим программистом автоматически. Хотя книга дает представление о том, как
думают разработчики и специалисты по сomputer science, она не сделает вас ими;
для этого обычно требуется высшее образование и определенный наработанный
опыт. Разработка и программирование похожи на музыку или живопись — они
отчасти являются навыками, а отчасти искусством. Рассмотрение обоих аспектов
в этой книге должно помочь вам улучшить навыки программирования.

Ландшафт
Компьютерное проектирование и программирование — большая область для изу­
чения, которую я не смогу охватить здесь в полной мере. Ее можно схематично
представить так, как показано на рис. 1.
Имейте в виду, что рис. 1 — упрощенное представление и что линии, разделяющие разные слои, на самом деле не такие четкие.
Пользователи
Прикладное программирование
Системное
программирование
Эта книга

Аппаратное обеспечение
Логическое
проектирование
Проектирование схем
Основы науки (физика ихимия)

Рис. 1. Компьютерный ландшафт

34   Введение
Большинство людей являются пользователями компьютерных систем. Вы наверняка тоже сейчас принадлежите к ним. Существуют особые пользователи,
называемые системными администраторами, которые поддерживают работу
компьютерных систем. Они устанавливают программное обеспечение, управляют учетными записями пользователей, создают резервные копии и т. д.
Обычно они обладают особыми полномочиями, недоступными обычным
пользователям.
Людей, которые пишут такие программы, как веб-страницы, приложения для
телефонов и музыкальные проигрыватели, называют прикладными программистами. Они пишут программное обеспечение для взаимодействия пользователей
с компьютерами, используя блоки, созданные другими специалистами. Прикладное программирование преподается на большинстве курсов по «обу­чению
программированию», как будто все программисты должны научиться импортировать эти блоки и склеивать их вместе. Хотя в большинстве случаев это не столь
обязательно, гораздо лучше понять суть самих «блоков» и «клея» между ними.
Прикладные программы не взаимодействуют с компьютерным оборудованием
напрямую; вот где в игру вступает системное программирование. Системные
программисты создают строительные блоки, используемые программистами
прикладных задач. Системным программистам необходимо разбираться в оборудовании, потому что их код взаимодействует с ним. Одна из целей этой
книги — научить вас тому, что нужно знать, чтобы стать хорошим системным
программистом.
Аппаратное обеспечение компьютера включает в себя не только ту часть, которая выполняет фактические вычисления, но и то, как эта часть соединяется
с внешним миром. Аппаратное обеспечение выражается в виде логики. Это та
же логика, которая используется при написании программ, и она является
ключом к пониманию работы компьютера. Логика построена из различных
типов электронных схем. Проектирование схем выходит за рамки этой книги, но вы можете узнать о нем больше, изучая схемотехнику. Если вы хотите
править миром, подумайте о двойной специальности в области схемотехники
и сomputer science.
Конечно, в основе всего лежит фундаментальная наука, дающая знания обо
всем, начиная от нашего понимания электричества до химии, необходимой для
создания микросхем.
Как показано на рис. 1, каждый уровень основывается на предыдущем. Это означает, что неправильный выбор проектирования или ошибки на более низких
уровнях влияют на все высшие уровни. Например, ошибка проектирования
в процессорах Intel Pentium около 1994 года привела к тому, что некоторые
операции деления дали неверные результаты. Это коснулось всего программного обеспечения, которое использовало деление с плавающей запятой в этих
процессорах.

Ландшафт   35
Как видите, системное программирование находится в нижней части иерархии
программного обеспечения. Оно похоже на инфраструктуру — дороги, электричество и воду. Всегда важно быть хорошим программистом, но еще важнее быть
им, если вы — системный программист, потому что другие полагаются на вашу
инфраструктуру. Также видно, что системное программирование находится
между прикладным программированием и аппаратным обеспечением, а это
означает, что для работы необходимы знания обеих этих областей. Слово йога
переводится с санскрита как «союз», и так же, как практикующие йогу стремятся
объединить свой разум и тело, системные программисты являются технойогами,
соединяющими аппаратное и программное обеспечение.
Вам не нужно изучать системное программирование, чтобы работать на одном
из других уровней. Но если вы этого не сделаете, вам придется найти кого-то,
кто поможет вам справиться с проблемами вне вашей предметной области, так
как вы не сможете решить их самостоятельно. Понимание основ технологии
также приводит к лучшим решениям на более высоких уровнях. Это не только
мое мнение; см. пост в блоге Вилле-Матиаса Хейккиля (Ville-Matias Heikkilä)
The Resource Leak Bug of Our Civilization за 2014 год — он высказывает похожее
мнение.
Эта книга тоже стремится охватить часть истории. Большинство программистов
не изучают историю своего дела, потому что им нужно усвоить очень много
материала. В результате многие делают ошибки, которые случались и раньше.
Знание истории по крайней мере позволяет делать новые ошибки, а не повторять
старые. Помните, что новейшие технологии, которые вы используете сегодня,
уже завтра устареют.
Если говорить об истории, то эта книга до отказа набита интересными технологиями и именами их создателей. Найдите время, чтобы узнать больше как
о технологиях, так и о людях. Большинство упомянутых специалистов решили
по крайней мере одну интересную задачу, и стоит узнать, как они воспринимали
мир и как подходили к проблемам и решали их. В романе Нила Стивенсона (Neal
Stephenson) «Анафема» (2008) есть отличный диалог:
«Нам угрожает инопланетный корабль, начиненный атомными бомбами.
У нас есть транспортир».
«Ладно, я сбегаю домой за линейкой и куском бечевки».
Стоит отметить опору на основы. Это не «Давай посмотрим, что делать, в “Википедии”», или «Я спрошу на Stack Overflow», или «Я найду какой-нибудь
пакет на GitHub». Научиться справляться с проблемами, которые еще никто
не решил, — важнейший навык.
Во многих примерах в этой книге разбираются старые технологии, такие как
16-битные компьютеры. Ведь с их помощью можно узнать почти все, что нужно,
и они более простые.

36   Введение

Структура книги
Книга концептуально разделена на три части. В первой части исследуется
компьютерное оборудование: что это такое и как оно устроено. Вторая часть
изучает поведение программ, запущенных на оборудовании. Последняя часть
посвящена искусству программирования — совместной работе над созданием
хороших программ.
Глава 1. Внутренний язык компьютеров. В этой главе начинается исследование менталитета трехлетнего ребенка. Компьютеры — битовые
игроки; их жизнь — управление битами. Вы узнаете, что они собой представляют и что с ними можно сделать. Мы будем присваивать битам
и совокупностям битов гипотетические значения.
Глава 2. Комбинаторная логика. В этой главе исследуется обоснование
использования битов вместо цифр и применение цифровых компьютеров. Глава включает в себя обсуждение некоторых старых технологий,
которые проложили путь к тому, что мы имеем сегодня. Охватываются
основы комбинаторной логики. Вы узнаете, как создавать более сложные
функции из битов и логики.
Глава 3. Последовательная логика. Здесь вы узнаете, как использовать
логику для построения памяти. Мы будем учиться генерировать время,
потому что память — это не что иное, как состояние, которое сохраняется во времени. В этой главе рассматриваются основы последовательной
логики и обсуждаются различные технологии памяти.
Глава 4. Анатомия компьютера. В этой главе показано, как компьютеры
собираются из логических элементов и элементов памяти, которые обсуждались в предыдущих главах. Рассматриваются различные методологии
реализации.
Глава 5. Архитектура компьютера. В этой главе мы рассмотрим некоторые дополнения к базовому компьютеру, изученному в главе 4. Вы узнаете,
как они обеспечивают необходимую функциональность и эффективность.
Глава 6. Разбор связей. Компьютеры должны взаимодействовать с внешним
миром. В этой главе изучаются ввод и вывод. Эта часть книги также пересматривает разницу между цифровыми и аналоговыми компьютерами и то,
как мы обеспечиваем работу цифровых компьютеров в аналоговом мире.
Глава 7. Организация данных. Теперь, когда вы увидели, как работают
компьютеры, мы узнаем, как их эффективно использовать. Компьютерные программы манипулируют данными в памяти, и важно представить
способ ее использования для решаемой задачи.
Глава 8. Обработка языка. Языки были изобретены, чтобы людям было
проще программировать компьютеры. В главе обсуждается процесс преобразования языков в то, что действительно работает на компьютерах.

Структура книги   37
Глава 9. Веб-браузер. Для веб-браузеров было запрограммировано множество всего. В этой главе рассматривается, как работает веб-браузер,
и выделяются его основные компоненты.
Глава 10. Прикладное и системное программирование. В этой главе мы
напишем две версии программы, которая работает на двух разных уровнях, показанных на рис. 1. Здесь раскрываются многие различия между
программированием на уровне приложений и на уровне систем.
Глава 11. Сокращения и приближения. Очень важно сделать программы
эффективными. В этой главе исследуются некоторые способы, с помощью которых можно этого добиться, освободив программы от ненужной
работы.
Глава 12. Взаимоблокировки и состояния гонки. Во многих системах
используется более одного компьютера. В этой главе рассматриваются
некоторые проблемы взаимодействия компьютеров.
Глава 13. Безопасность. Компьютерная безопасность — это непростая
тема. Здесь рассматриваются основы работы со сложной математикой.
Глава 14. Машинный интеллект. В этой главе также поднимается более
сложная тема. Новые приложения являются результатом сочетания
больших данных, искусственного интеллекта и машинного обучения — от
вождения автомобиля до искусства сводить с ума рекламой.
Глава 15. Влияние реальных условий. Программирование — очень
методичный и логичный процесс. Но в определении того, что и как программировать, участвуют люди, а им часто не хватает логики. В этой главе
обсуждаются некоторые вопросы программирования в реальном мире.
Читая эту книгу, имейте в виду, что многие объяснения упрощены и, следовательно, в мельчайших деталях могут быть неточны. Чтобы сделать объяснения идеальными, потребуется слишком много отвлекающих подробностей.
Не удивляйтесь, если вы поймете это по мере того, как будете узнавать больше.
Считайте эту книгу глянцевым путеводителем по стране компьютеров. Он не
может охва­тить все подробно, и, когда вы начнете углубляться в детали, вы
обнаружите множество тонких различий.

1

Внутренний язык компьютеров

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

Что такое язык?
Язык — это удобное сокращение. Он позволяет описывать сложные концепции,
не демонстрируя их. Он также позволяет передавать концепции на расстоянии,
даже через посредников.

Письменный язык   39
Каждый язык — будь то письменный, разговорный или включающий серию
жестов — имеет значение, закодированное в виде набора символов. Однако кодирования значения символами недостаточно. Язык работает только в том случае,
если все общающиеся стороны располагают одинаковым контекстом, позволяющим присвоить одинаковое значение одним и тем же символам. Например,
видя слово Тото, многие вспоминают собаку из «Волшебника страны Оз», в то
время как другие представляют японского производителя сидений для унитазов
с подогревом. Недавно я столкнулся с большим недопониманием, обсуждая
одежду с одним из своих французских студентов по обмену. Оказывается, слово
camisole в Америке обычно обозначает майку, а во Франции — смирительную
рубашку! В обоих примерах одни и те же символы можно различить только
по контексту, и этот контекст не всегда легко определить. С компьютерными
языками та же проблема.

Письменный язык
Письменный язык — это последовательность символов. Мы образуем слова,
располагая символы в определенном порядке. Например, в английском языке
мы образуем слово yum, разместив три символа (то есть буквы) в порядке слева
направо следующим образом: y u m.
Существует множество символов и их комбинаций. В английском языке 26 основных символов (A — Z) — без учета прописных и строчных букв, знаков
препинания, лигатур и т. д., — которые носители английского языка изучают
в раннем детстве. В других языках используются другие типы и количество символов. В некоторых иероглифических языках, таких как китайский и японский,
символов очень много, и каждый символ представляет собой отдельное слово.
В языках также разный порядок расположения элементов, например справа
налево на иврите и вертикально на китайском. Важен и порядок символов: d o g
(«пёс») — это не то же самое, что g o d («бог»).
Хотя стиль в некотором смысле можно рассматривать как язык сам по себе, мы
не различаем символы по шрифту: a, a и a — это один и тот же символ.
Технологию письменного языка, включая компьютерный язык, формируют три
компонента:
контейнеры, содержащие символы;
допустимые символы в контейнерах;
порядок контейнеров.
Некоторые языки включают более сложные правила, которые ограничивают
разрешенные символы в контейнерах на основе символов в других контейнерах.
Например, некоторые символы не могут находиться в соседних контейнерах.

40   Глава 1. Внутренний язык компьютеров

Бит
Начнем с контейнера. Он может назваться символом на человеческом языке
и битом на языке компьютера. Термин «бит» представляет собой неудачное
сочетание понятий «двоичный» (binary) и «цифровой» (digit). Неудачное, потому что двоичный код означает что-то состоящее из двух частей, тогда как слово
«цифра» означает любой из 10 символов (0–9) повседневной системы счисления.
В следующей главе вы узнаете, почему мы используем биты; пока ограничимся
тем, что они дешевы и просты в сборке.
Бит является двоичным — битовый контейнер может содержать только один
из двух символов наподобие точки и тире из азбуки Морзе. В азбуке Морзе используются всего два символа для представления сложной информации путем
объединения этих символов в различные комбинации. Например, буква A — это
точка-тире.
B — это тире-точка-точка-точка, C — тире-точка-тире-точка и т. д. Порядок символов важен, как и в человеческом языке: тире-точка означает N, а не A.
Понятие символов абстрактно. На самом деле, не так важно, что они означают;
они могут значить что угодно — включение и выключение, день и ночь, утка
и гусь. Но помните, что язык не работает без контекста. Было бы странно, если
бы отправитель думал, что он говорит U (точка-точка-тире), а получатель в это
время слышал «утка-утка-гусь».
В оставшейся части этой главы вы узнаете о некоторых распространенных
способах присваивать битам значения для вычислений. Имейте в виду, что мы
часто будем использовать гипотетические примеры, например, такие как: «Представим, что этот бит означает “синий”». На самом деле программирование так
и работает, поэтому, даже изучая некоторые стандартные способы использования
битов, не бойтесь изобретать свои, если это уместно.

Логические операции
Биты используются для представления ответов на вопросы типа «да/нет», например: «Холодно?» или «Тебе нравится моя шляпа?». Мы используем термины истина для обозначения ответа «да» и ложь для обозначения ответа «нет».
На вопросы вроде «Где тут вечеринка с собаками?» нельзя ответить «да» или
«нет», поэтому их нельзя представить в виде единственного бита.
В человеческом языке мы часто объединяем несколько предложений с «да/нет»
в одно. Мы вполне можем сказать: «Наденьте пальто, если холодно или идет
дождь» или «Катайтесь на лыжах, если идет снег и не нужно идти в школу».
Можно выразить это по-другому: «Наденьте пальто — это истина, если холодно — истина или идет дождь — истина» или «Катайтесь на лыжах — это истина,

Логические операции   41
если идет снег — истина, а нужно идти в школу — ложь». Это и есть логические
операции, каждая из которых создает новый бит на основе содержимого других
битов.

Булева алгебра
Подобно тому как алгебра — это набор правил для работы с числами, булева
алгебра, изобретенная в XIX веке английским математиком Джорджем Булем,
представляет собой набор правил, которые используются для работы с битами.
Как и в случае с обычной алгеброй, в булевой алгебре применяются правила
ассоциативности, коммутативности и дистрибутивности.
Существуют три основные логические операции НЕ, И и ИЛИ, а также одна
составная операция — исключающее ИЛИ:
НЕ (NOT). Эта операция означает «противоположность». Например,
если бит ложный, НЕ этот бит будет истинным. Если бит истинен, НЕ
этот бит будет ложным.
И (AND). В этой операции задействовано 2 бита или более. В 2-битной
операции результат будет истинным, только если И первый, И второй
бит истинны. Если задействовано более 2 бит, результат будет истинным
только в том случае, если все биты истинны.
ИЛИ (OR). Эта операция также включает 2 бита или более. В 2-битной
операции результат будет истинным, если первый ИЛИ второй бит истинен; в противном случае результат будет ложным. Если бит больше, чем
2, результат будет истинным, если какой-либо из битов истинен.
Исключающее ИЛИ (XOR). Результат операции «исключающее ИЛИ»
будет истинным, если первый и второй биты имеют разные значения.
На рис. 1.1 логические операции представлены графически в виде так называемых таблиц истинности. Входы находятся снаружи квадратов, а выходы —
внутри. В этих таблицах И означает Истина, а Л — Ложь.
НЕ

И
Л

И

ИЛИ
Л И

Исключающее
ИЛИ
Л И

Л И

Л

Л

Л

Л

Л

И

Л

Л

И

И Л

И Л

И

И И

И

И И

Л

Рис. 1.1. Таблицы истинности для логических операций
На рис. 1.2 показано, как читать таблицы истинности для операций НЕ и И.
Определить выход можно, проследив путь от входа или входов.

42   Глава 1. Внутренний язык компьютеров

Л

И

Л

И

Л

И

Л

И

Л И

Л И

Л Л

Л

Л Л

Л

Л Л

Л

Л Л

Л

И Л

И Л

И Л

И

И Л

И

И Л

И

И Л

И

НЕ Л = И

НЕ И= Л

Л ИЛ = Л

ИИ Л = Л

Л ИИ = Л

ИИ И= И

Рис. 1.2. Использование таблиц истинности
Как видите, операция НЕ просто меняет значение входа на противоположное.
В то же время, операция И возвращает истину только тогда, когда оба входа
истинны.
ПРИМЕЧАНИЕ
Операция «исключающее ИЛИ» основана на других операциях. Например, исключающее ИЛИ двух битов a и b означает то же, что (a ИЛИ b) И НЕ (a И b). Это
показывает, что базовые логические операции можно комбинировать по-разному
для получения одного и того же результата.

Закон де Моргана
В XIX веке британский математик Огастес де Морган (Augustus De Morgan)
добавил закон, применимый только к булевой алгебре, названный законом
де Моргана. Этот закон гласит, что операция a И b эквивалентна операции НЕ
(НЕ a ИЛИ НЕ b), как показано на рис. 1.3.
a

b

a И b

НЕ a

НЕ b

НЕ a ИЛИ НЕ b

НЕ (НЕ a ИЛИ НЕ b)

Л
Л
И
И

Л
И
Л
И

Л
Л
Л
И

И
И
Л
Л

И
Л
И
Л

И
И
И
Л

Л
Л
Л
И

Рис. 1.3. Таблица истинности закона де Моргана
Обратите внимание, что результаты a И b во втором столбце идентичны результатам, перечисленным в последнем столбце НЕ (НЕ a ИЛИ НЕ b). Это означает,
что при достаточном количестве операций НЕ можно заменить операции И операциями ИЛИ (и наоборот). Это полезно, потому что компьютеры работают
с настоящими входными данными, которые они не могут контролировать. Хотя
было бы неплохо, если бы входные данные всегда были представлены в виде холодно или дождь, но они часто приходят в виде НЕ холодно и НЕ дождь. Подобно
двойному отрицанию в естественных языках («Это не может не радовать»), закон
де Моргана — это инструмент, который позволяет оперировать положениями
отрицательной логики в дополнение к положительной логике, которую мы уже

Представление целых чисел с помощью битов   43
рассмотрели. На рис. 1.4 показано решение задачи «надеть ли пальто» как для
положительной, так и для отрицательной логики.
холодно дождь надеть пальто
Л
Л
И
И

Л
И
Л
И

Л
И
И
И

не холодно нет дождя
Л
Л
И
И

Л
И
Л
И

не надевать пальто
Л
Л
Л
И

Рис. 1.4. Положительная и отрицательная логика
С левой стороны (положительная логика) можно принять решение, используя
единственную операцию ИЛИ. С правой стороны (отрицательная логика)
закон де Моргана позволяет принимать решение, используя единственную
операцию И. Если бы закона де Моргана не существовало, пришлось бы реализовать случай отрицательной логики как НЕ не-холодно ИЛИ НЕ не-дождь.
Хотя и это сработает, каждая операция влечет за собой затраты в стоимости
и производительности, поэтому сокращение операций минимизирует затраты.
Оборудование, которое выполняет операцию НЕ, стоит реальных денег, и, как
вы узнаете из следующей главы, каскадные операции замедляют работу.
Де Морган говорит, что наш пример эквивалентен варианту «холодно и дождь»,
что намного проще.

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

Представление положительных чисел
Мы обычно используем десятичную систему счисления, потому что она соответствует нашей анатомии (у нас 10 пальцев. — Примеч. ред.). В контейнеры
помещаются десять различных символов, называемых цифрами: 0123456789.
Контейнеры заполняются справа налево. Каждый контейнер имеет имя, отличное от его содержимого; крайний правый контейнер мы называем единицами, следующий — десятками, затем сотнями, тысячами и т. д. Они являются
производными от степеней десятки; 100 — один, 101 — десять, 102 — сто, 103 —
тысяча. Эта система называется десятичной, поскольку 10 является основанием степени. Значение числа получается из суммы произведения каждого
значения контейнера и значения его содержимого. Например, число 5028 — это
сумма 5 тысяч, 0 сотен, 2 десятков и 8 единиц, или 5 × 103 + 0 × 102 + 2 × 101 +
+ 8 × 100, как показано на рис. 1.5.

44   Глава 1. Внутренний язык компьютеров

10
5

3

2

10
0

1

10 10
2

0

8

Рис. 1.5. Число 5028 в десятичной системе счисления
Аналогичный подход можно использовать для записи чисел с помощью битов.
Поскольку вместо цифр используются биты, мы имеем только два символа:
0 и 1. Но это не проблема. В десятичном формате новый контейнер добавляется всякий раз, когда не хватает места; 9 можно поместить в один контейнер, но
для 10 уже понадобятся 2 контейнера. То же самое в двоичном формате — нам
просто нужен новый контейнер для чего-то большего, чем 1. В самом правом
контейнере все равно будут единицы, но что мы поместим в следующий? Двойки.
Значение контейнера в десятичном формате, в котором содержится 10 символов, в 10 раз больше, чем значение в контейнере справа от него. Таким образом,
в двоичном формате с двумя символами значение в выбранном контейнере в два
раза больше, чем значение в контейнере справа. Вот и все! Значения контейнера
являются степенями двойки, что означает, что это система с основанием 2, а не
с основанием 10.
В табл. 1.1 перечислены некоторые степени числа 2. Рассмотрим ее как справочный материал, чтобы изучить двоичное представление числа 5028.
Таблица 1.1. Степени двойки
Расширение

Степень

Десятичноечисло

2÷2

20

1

2

21

2

2×2

22

4

2×2×2

23

8

2×2×2×2

24

16

2×2×2×2×2

25

32

2×2×2×2×2×2

26

64

2×2×2×2×2×2×2

27

128

2×2×2×2×2×2×2×2

28

256

2×2×2×2×2×2×2×2×2

29

512

2×2×2×2×2×2×2×2×2×2

210

1024

Представление целых чисел с помощью битов   45

Расширение

Степень

Десятичное число

2×2×2×2×2×2×2×2×2×2×2

211

2048

2×2×2×2×2×2×2×2×2×2×2×2

212

4096

2×2×2×2×2×2×2×2×2×2×2×2×2

213

8192

2×2×2×2×2×2×2×2×2×2×2×2×2×2

214

16 384

2×2×2×2×2×2×2×2×2×2×2×2×2×2×2

215

32 768

Каждое число в крайнем правом столбце табл. 1.1 представляет значение двоичного контейнера. На рис. 1.6 показано, как число 5028 может быть записано
в двоичной системе с использованием, по сути, тех же вычислений, что и для
десятичной записи выше.
12

2

1

2

11

0

10

2

0

2

9

1

2
1

8

2
1

7

2

6

0

2

5

1

2

4

2

0

0

3

2

2

1

2

1

0

2

0

0

Рис. 1.6. Число 5028 в двоичной системе
Результат преобразования в двоичный формат:
1 × 212 + 0 × 211 + 0 × 210 + 1 × 29 + 1 × 28 + 1 × 27 + 0 × 26 + 1 × 25 + 0 × 24 + 0 × 23 +
+ 1 × 22 + 0 × 21 + 0 × 20 = 5028.
Как видите, число 5028 в двоичном формате состоит из одного числа 4096 (212),
нуля чисел 2048 (211), нуля чисел 1024 (210), одного числа 512 (29), одного числа
256 (28) и т. д., пока не получится 1001110100100. Выполняя те же вычисления,
что и для десятичных чисел, запишем 1 × 212 + 0 × 211 + 0 × 210 + 1 × 29 + 1 × 28 +
+ 1 × 27 + 0 × 26 + 1 × 25 + 0 × 24 + 0 × 23 + 1 × 22 + 0 × 21 + 0 × 20. Подставляя десятичные числа из табл. 1.1, получим 4096 + 512 + 256 + 128 + 32 + 4, что равно 5028.
5028 — четырехзначное десятичное число. В двоичном формате его можно представить 13-битным числом.
Количество цифр определяет диапазон значений, которые можно представить
в десятичном виде. Например, 100 различных значений в диапазоне 0–99 могут
быть представлены двумя цифрами. Точно так же количество битов определяет
диапазон значений, которые можно представить в двоичном формате. Например,
2 бита могут представлять четыре значения в диапазоне 0–3. Таблица 1.2 отображает как количество, так и диапазон значений, которые можно представить
с разным количеством битов.

46   Глава 1. Внутренний язык компьютеров
Таблица 1.2. Диапазоны значений двоичных чисел
Количество бит

Количество значений

Диапазон значений

4

16

0…15

8

256

0…255

12

4096

0…4095

16

65 536

0…65 535

20

1 048 576

0…1 058 575

24

16 777 216

0…16 777 215

32

4 294 967 296

0…4 294 967 295

64

18 446 744 073 709 551 616

0…18 446 744 073 709 551 615

Крайний правый бит в двоичном числе называется наименьшим значащим битом, а крайний левый — наибольшим значащим битом, потому что изменение
значения самого правого бита оказывает наименьшее влияние на значение
числа, а изменение значения самого левого — наибольшее. Компьютерщики
любят трехбуквенные аббревиатуры, или TLA (three-letter acronym), поэтому
эти биты обычно называют LSB (least significant bit) и MSB (most significant bit)
соответственно. На рис. 1.7 показан пример числа 5028, записанного в 16 битах.
MSB

0

0

0

1

0

0

1

1

1

0

1

0

0

1

0

15

14

13

12

11

10

9

8

7

6

5

4

3

2

1

0

LSB

0

Рис. 1.7. MSB и LSB
Вы заметите, что, хотя двоичное представление 5028 занимает 13 бит, рис. 1.7
показывает его в 16-битном формате. Как и в десятичной системе счисления,
всегда можно использовать больше контейнеров, чем требуется, добавляя ведущие нули слева.
В десятичном виде 05 028 имеет то же значение, что и 5028. Двоичные числа
часто представлены таким образом, потому что компьютеры созданы на основе
битовых блоков.

Сложение двоичных чисел
Теперь, когда вы знаете, как представлять числа в двоичном формате, перей­дем
к простой арифметике с двоичными числами. В десятичном сложении мы складываем все цифры справа (наименьшая значащая цифра) налево (наибольшая
значащая цифра) и, если результат больше 9, переносим 1. Точно так же мы

Представление целых чисел с помощью битов   47
складываем все биты в двоичных числах, переходя от наименьшего значащего
к наибольшему значащему биту, и, если результат больше 1, переносим 1.
На самом деле сложение в двоичном формате немного проще, поскольку существуют только 4 возможные комбинации из 2 бит по сравнению со 100 комбинациями из 2 цифр. Например, на рис. 1.8 показано, как сложить 1 и 5, используя
двоичные числа, с учетом цифр над каждым столбцом.
1
5
6

0

0
1
1

1

0
0
1

0

1
1
0

Рис. 1.8. Сложение двоичных чисел
Число 1 — 001 в двоичном формате, а число 5 — 101, потому что (1 × 4) +
+ (0 × 2) + (1 × 1) = 5. Чтобы сложить двоичные числа 001 и 101, мы начинаем
с наименьшего значащего бита в крайнем правом столбце. Сложение двоичных
чисел 1 и 1 в этом столбце дает 2, но символа для 2 в двоичном формате не существует. Однако мы знаем, что 2 на самом деле равно 10 в двоичном формате
([1 × 2] + [0 × 1] = 2), поэтому пишем 0 в качестве суммы и переносим 1 к следующей цифре. Поскольку средние биты — нули, мы получаем в качестве суммы
только 1, перенесенную из предыдущего столбца. Затем складываем цифры
в крайнем левом столбце: 0 плюс 1 — просто 1 в двоичном формате. В результате
получится двоичное число 110, или 6 в десятичной системе счисления — то же
самое, что мы получили бы, сложив 1 и 5.
Вы могли заметить, что правила сложения двоичных чисел могут быть выражены
в терминах логических операций, которые мы обсуждали ранее, как показано
на рис. 1.9. В главе 2 мы увидим, что именно так компьютеры и выполняют
двоичное сложение.
A

B

AИB

0
0

0
1

1
1

исключающее

A+B

A ИЛИ B

A

B

0
0

00
01

0
1

0
0

0
1

0

0

01

1

1

0

1

1

10

0

1

1

Рис. 1.9. Сложение двоичных чисел с использованием логических операций
Результат сложения двух бит тот же, что и для операции «исключающее ИЛИ»
с этими двумя битами, а переносимое значение содержит то же, что ИЛИ этих двух
бит. Вы можете проверить эту теорию по рис. 1.9, где сложение 1 и 1 в двоичном
формате дает 10. В этом примере переносимое значение равно 1 — то же самое, что

48   Глава 1. Внутренний язык компьютеров
и результат операции «1 И 1». Точно так же выражение (1 исключающее ИЛИ 1)
дает 0, то есть значение, которое мы присваиваем самой позиции бита.
Сложение двух битов — операция, которая редко выполняется изолированно.
Вернитесь к рис. 1.8 — кажется, что мы складываем по 2 бита в каждом столбце,
но на самом деле мы складываем 3 бита из-за переноса. К счастью, чтобы сложить
3 бита, не придется изучать ничего нового (потому что A + B + C эквивалентно
(A + B) + C согласно сочетательному закону), поэтому можно сложить 3 бита
вместе, используя два сложения двух битов.
Что произойдет, если результат сложения не поместится в имеющемся количестве
битов? Это приведет к переполнению, которое происходит всякий раз, когда мы
переносим цифру из наиболее значащего бита. Например, если мы складываем
4-битные числа, например прибавляем 1001 (910) к 1000 (810), в результате должно
получиться число 10001 (1710), но в итоге получится 0001 (110), потому что для
наиболее значащего бита не осталось места. Позже мы более подробно рассмотрим,
что в компьютерах есть регистр кодов условий, который представляет собой место,
где хранятся некие фрагменты информации. Один из таких — бит переполнения,
который содержит значение переноса из наиболее значащего бита. Мы можем
проверить это значение, чтобы определить, произошло ли переполнение.
Вы, наверное, знаете, что можно вычесть одно число из другого, прибавив к первому числу отрицательный вариант второго. В следующем разделе мы узнаем,
как представлять отрицательные числа. Заимствование за пределами наиболее
значащего бита называется потерей значимости. Для этого у компьютеров тоже
есть код условия.

Представление отрицательных чисел
Все числа, двоичный формат которых мы разбирали в предыдущем разделе,
были положительными. Но многие задачи в реальной жизни связаны как
с положительными, так и с отрицательными числами. Посмотрим, как можно
использовать биты для представления отрицательных чисел. Предположим,
что у нас есть 4 бита. Как вы узнали в предыдущем разделе, 4 бита могут представлять 16 чисел в диапазоне от 0 до 15.
Тот факт, что мы можем хранить 16 чисел в 4 битах, не означает, что эти числа
должны находиться в диапазоне от 0 до 15. Помните, что язык работает через
значение и контекст. Это означает, что можно изобретать новые контексты для
интерпретации битов.

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

Представление целых чисел с помощью битов   49
с помощью бита. Мы произвольно будем использовать крайний левый бит
(MSB) для знака, оставив 3 бита, которые могут представлять число от 0 до 7.
Если бит знака равен 0, мы считаем это число положительным. Если он равен
1, то число считается отрицательным. Это позволяет представить всего 15 различных положительных и отрицательных чисел, а не 16, потому что существует
и положительный, и отрицательный 0. Таблица 1.3 показывает, как с учетом
этого можно представить числа от –7 до +7.
Это называется представлением в виде прямого кода со знаком, потому что есть
бит, который представляет знак, и биты, которые представляют величину — то,
насколько значение далеко от нуля.
Представление в виде прямого кода со знаком мало используется по двум причинам.
Во-первых, создание битов требует денег, поэтому не стоит тратить их зря на
два разных представления для нуля; лучше использовать эту комбинацию битов
для представления другого числа. Во-вторых, арифметика с использованием
исключающего ИЛИ и И не работает в таком представлении.
Таблица 1.3. Прямой код со знаком для двоичных чисел
Знак

22

21

20

Десятичное число

0

1

1

1

+7

0

1

1

0

+6

0

1

0

1

+5

0

1

0

0

+4

0

0

1

1

+3

0

0

1

0

+2

0

0

0

1

+1

0

0

0

0

+0

1

0

0

0

–0

1

0

0

1

–1

1

0

1

0

–2

1

0

1

1

–3

1

1

0

0

–4

1

1

0

1

–5

1

1

1

0

–6

1

1

1

1

–7

50   Глава 1. Внутренний язык компьютеров
Допустим, нужно прибавить +1 к –1. Мы ожидали получить 0, но при использовании представления в виде прямого кода со знаком получается другой
результат, как показано на рис. 1.10.

+

0
1

0
0

0
0

1
1

+1
–1

1

0

1

0

–2

Рис. 1.10. Сложение с использованием прямого кода со знаком
Как видно, 0001 представляет положительную 1 в двоичной системе, потому что
его бит знака равен 0. 1001 представляет –1 в двоичной системе, потому что бит
знака равен 1. Сложение их с использованием арифметики исключающего ИЛИ
и И дает 1010. Этот результат эквивалентен –2 в десятичной системе счисления,
что не равно сумме +1 и –1.
Мы могли бы работать с арифметикой прямого кода, используя более сложную
логику, но наша задача — максимально все упростить. Рассмотрим несколько
других способов представления чисел и выберем лучший подход.

Обратный код
Еще один способ получить отрицательные числа — взять положительные и инвертировать все биты, что называется обратным кодом. Мы разделяем биты
аналогично примеру прямого кода со знаком. В этом контексте мы получаем
дополнение с помощью операции НЕ. Таблица 1.4 показывает числа в диапазоне
от –7 до 7 с использованием обратного кода.
Таблица 1.4. Двоичные числа с обратным кодом
Знак

22

21

20

Десятичное число

0

1

1

1

+7

0

1

1

0

+6

0

1

0

1

+5

0

1

0

0

+4

0

0

1

1

+3

0

0

1

0

+2

0

0

0

1

+1

0

0

0

0

+0

1

1

1

1

–0

Представление целых чисел с помощью битов   51

Знак

22

21

20

Десятичное число

1

1

1

0

–1

1

1

0

1

–2

1

1

0

0

–3

1

0

1

1

–4

1

0

1

0

–5

1

0

0

1

–6

1

0

0

0

–7

Как видите, инвертирование каждого бита 0111 (+7) дает 1000 (–7).
При использовании обратного кода все еще сохраняется проблема двух представлений нуля, что по-прежнему не позволяет легко выполнять сложение.
Чтобы обойти это, мы применим круговой перенос, чтобы добавить 1 к LSB при
переносе из наиболее значащей позиции и получить правильный результат
(рис. 1.11).
1

1

+

0
1

+

0
0
0

1

0
1

0

1
1

0
0

0
0

0
0

0
1

0

0

1

+2
–1
0
1 (Круговой перенос)
+1

Рис. 1.11. Сложение в обратном коде
Чтобы сложить +2 и –1 с использованием обратного кода, выполним обычное
сложение двоичных чисел 0010 и 1110. Поскольку при добавлении цифр в самый
значащий бит (бит знака) получается результат 10, записываем 0 и круговым
переносом переносим 1 для каждой цифры. Но у нас всего лишь 4 бита, поэтому,
перейдя к MSB, мы возвращаем перенос к первому биту, чтобы получить 0001,
или +1, что является правильной суммой +2 и –1. Как видите, все эти вычисления значительно всё усложняют.
Такой способ работает, но это все же не лучшее решение, потому что нам нужно
дополнительное оборудование, чтобы добавить бит для кругового переноса.
В современных компьютерах не используются ни прямой, ни обратный коды.
Арифметика с применением этих методов не работает без дополнительного оборудования, а оно стоит денег. Посмотрим, сможем ли мы придумать решение
этой проблемы.

52   Глава 1. Внутренний язык компьютеров

Дополнительный код
Что произойдет, если не добавлять никакое оборудование, а просто использовать операции «исключающее ИЛИ» и И? Выясним, какой набор битов при
добавлении к +1 даст 0, и назовем его –1. Если придерживаться 4-битных чисел,
число +1 будет равно 0001 в двоичной системе. Добавление к нему 1111 даст
0000, как показано на рис. 1.12, поэтому мы будем использовать этот набор битов
для представления –1.
1

+1
–1
0

0
1
0

1

0
1

0

1

0
1
0

0

1
1
0

Рис. 1.12. Нахождение −1
Этот способ представления называется дополнительным кодом, и это наиболее
часто используемое двоичное представление для целых чисел со знаком. Можно
получить отрицательное число, дополнив число (то есть выполнив операцию
НЕ для каждого бита), а затем добавив 1, отбрасывая любой перенос из MSB.
Дополнение к +1 (0001) равно 1110, а добавление 1 дает 1111 вместо –1. Аналогично +2 — это 0010, его дополнение — 1101, а добавление 1 дает 1110, представляя –2. В табл. 1.5 показаны значения в диапазоне от –8 до 7 с использованием
дополнительного кода.
Таблица 1.5. Двоичные числа с дополнительным кодом
Знак

22

21

20

Десятичное число

0

1

1

1

+7

0

1

1

0

+6

0

1

0

1

+5

0

1

0

0

+4

0

0

1

1

+3

0

0

1

0

+2

0

0

0

1

+1

0

0

0

0

+0

1

1

1

1

–1

1

1

1

0

–2

1

1

0

1

–3

1

1

0

0

–4

Представление целых чисел с помощью битов   53

Знак

22

21

20

Десятичное число

1

0

1

1

–5

1

0

1

0

–6

1

0

0

1

–7

1

0

0

0

–8

Проверим то же самое для 0, чтобы увидеть, решает ли дополнительный код
проблему двух разных представлений нуля. Если взять 0000 и инвертировать
каждый бит, то получим 1111 в качестве его дополнения. Добавление 1 к 1111
дает [1]0000, но, поскольку это 5-битное число, превышающее количество доступных битов, можно не учитывать 1 в бите переноса. Остается 0000, с чего мы
и начали, так что ноль имеет только одно представление в дополнительном коде.
Программистам необходимо знать, сколько битов требуется для хранения нужных им чисел. Со временем это войдет в привычку. А пока можно обратиться
к табл. 1.6, где показан диапазон значений, который можно представить с помощью дополнительного кода для чисел разного размера.
Таблица 1.6. Диапазоны значений двоичных чисел с дополнительным кодом
Количество битов

Количество значений

Диапазон значений

4

16

–8…7

8

256

–128…127

12

4096

2048…2047

16

65 536

–32 768…32 767

20

1 048 576

–524 288…524 287

24

16 777 216

–8 388 608…8 388 607

32

4 294 967 296

–2 147 483 648…2 137 483 647

64

18 446 744 073 709 551 616

–9 223 372 036 854 775 808...
…9 223 372 036 854 775 807

Как видно из табл. 1.6, по мере увеличения количества битов диапазон представляемых значений увеличивается экспоненциально. Важно помнить, что всегда
нужен контекст, чтобы определить, является ли рассматриваемое 4-битное число
числом 15 вместо –1 с использованием дополнительного кода, –7 с использованием прямого кода или –0 с использованием обратного кода. Вы должны знать,
какое представление используете.

54   Глава 1. Внутренний язык компьютеров

Представление действительных чисел
До сих пор мы представляли целые числа в двоичном формате. А как насчет
действительных чисел? Действительные числа включают десятичную точку в основании 10. Нам нужен способ представить эквивалентную двоичную
точку в основании 2. Опять же, этого можно добиться путем интерпретации
битов в разных контекстах.

Представление с фиксированной точкой
Один из способов представления дробей в двоичном формате — выбор произвольного места для двоичной точки, двоичного эквивалента десятичной точки.
Например, если у нас есть 4 бита, мы можем представить, что два из них находятся
справа от двоичной точки, представляя четыре дробных значения, а два — слева,
представляя четыре целых значения. Это называется представлением с фиксированной точкой1, потому что положение двоичной точки фиксировано (табл. 1.7).
Таблица 1.7. Двоичные числа с фиксированной точкой
Целая часть

1

Дробная часть

Значение

0

0

.

0

0

0

0

0

.

0

1

1

/4

0

0

.

1

0

1

/2

0

0

.

1

1

3

/4

0

1

.

0

0

1

0

1

.

0

1

1 1/4

0

1

.

1

0

1 1/2

0

1

.

1

1

1 3/4

1

0

.

0

0

2

1

0

.

0

1

2 1/4

1

0

.

1

0

2 1/2

1

0

.

1

1

2 3/4

1

1

.

0

0

3

1

1

.

0

1

3 1/4

1

1

.

1

0

3 1/2

1

1

.

1

1

3 3/4

В российской нотации — числа с фиксированной запятой. — Примеч. ред.

Представление действительных чисел   55
Целые числа слева от точки должны быть знакомы по двоичной системе счисления. Подобно рассмотренным ранее целым числам, мы имеем четыре значения
из двух битов справа от точки; они представляют собой четверти вместо привычных десятичных долей.
Хотя этот подход работает довольно хорошо, он нечасто используется в компьютерах общего назначения, поскольку для представления полезного диапазона
чисел требуется слишком много битов. Некоторые специализированные компьютеры, называемые процессорами для цифровой обработки сигналов (digital
signal processor, DSP), по-прежнему используют числа с фиксированной точкой.
И, как вы увидите в главе 11, числа с фиксированной точкой особенно полезны
в некоторых приложениях.
Компьютеры общего назначения созданы для решения общих задач, работающих с широким диапазоном чисел. Вы можете получить представление об этом
диапазоне, полистав учебник физики. Существуют крошечные числа, такие как
постоянная Планка (6.63 × 10–34 джоуля в секунду), и огромные числа, например
постоянная Авогадро (6.02 × 1023 моль–1), то есть диапазон чисел составляет от
1057 до 2191. Это почти 200 бит! Биты не настолько дешевы, чтобы использовать
их сотнями для представления каждого числа, поэтому нам нужен другой подход.

Представление с плавающей точкой
Эта проблема решается использованием двоичной версии экспоненциальной
запи­си — она применяется для представления широкого диапазона чисел, включая постоянные Планка и Авогадро. Экспоненциальная запись представляет
большой диапазон чисел (а как иначе?), создавая новый контекст для интерпретации. Она задействует число с одной цифрой слева от десятичной точки, называемое мантиссой, умноженное на 10 в некоторой степени, называемое экспонентой.
Компьютеры используют эту же систему, за исключением того, что мантисса
и экспонента представлены в двоичных числах, а вместо 10 используется 2.
Это называется представлением с плавающей точкой1, что сбивает с толку, потому что двоичная (или десятичная) точка всегда находится в одном и том же
месте: между единицей и половинками (десятые доли в десятичной системе
счисления). Представление с плавающей точкой — это просто еще один способ
научной записи, которая позволяет записывать 1.2 × 10–3 вместо 0.0012.
Обратите внимание, что нам не нужны дополнительные биты, чтобы указать,
что основание равно 2, потому что определение с плавающей точкой подразу­
мевает это по умолчанию. Благодаря отделению значащих цифр от экспонент
система с плавающей точкой позволяет представлять очень маленькие или очень
большие числа без необходимости хранить все эти нули.
1

В российской нотации — числа с плавающей запятой. — Примеч. ред.

56   Глава 1. Внутренний язык компьютеров
В табл. 1.8 показан пример 4-битного представления с плавающей точкой с двумя
битами мантиссы и двумя битами экспоненты.
Таблица 1.8. Двоичные числа с плавающей точкой
Мантисса

Экспонента

Значение

0

0

.

0

0

0 (0 × 20)

0

0

.

0

1

0 (0 × 21)

0

0

.

1

0

0 (0 × 21)

0

0

.

1

1

0 (0 × 23)

0

1

.

0

0

0.5 (Ѕ × 20)

0

1

.

0

1

1.0 (Ѕ × 21)

0

1

.

1

0

2.0 (Ѕ × 22)

0

1

.

1

1

4.0 (Ѕ × 23)

1

0

.

0

0

1.0 (1 × 20)

1

0

.

0

1

2.0 (1 × 21)

1

0

.

1

0

4.0 (1 × 22)

1

0

.

1

1

8.0 (1 × 23)

1

1

.

0

0

1.5 (1Ѕ × 20)

1

1

.

0

1

3.0 (1Ѕ × 21)

1

1

.

1

0

6.0 (1Ѕ × 22)

1

1

.

1

1

12.0 (1Ѕ × 23)

Хотя в этом примере используется всего несколько битов, мы уже можем
заметить некоторые недостатки, присущие системе с плавающей точкой.
Во-первых, появляется множество бесполезных комбинаций битов. Например — четыре способа представления 0 и два способа представления 1.0, 2.0
и 4.0. Во-вторых, не существует наборов битов для каждого возможного числа;
из-за экспоненты расстояние между числами возрастает по мере их увеличения. Один из побочных эффектов заключается в том, что, хотя мы можем
сложить 0.5 и 0.5, чтобы получить 1.0, мы не можем сложить 0.5 и 6.0, потому
что не существует набора битов, представляющего 6.5. (Есть целая отрасль
математики, называемая численным анализом, которая включает отслеживание
погрешности вычислений.)

Представление действительных чисел   57

Стандарт IEEE для чисел с плавающей точкой
Как ни странно, система с плавающей точкой является стандартным способом
представления действительных чисел в вычислениях. В ней используется больше битов, чем в табл. 1.8, а кроме них — два знака: один для мантиссы и один
скрытый (он является частью экспоненты). Также существует множество способов убедиться, что операции наподобие округления работают максимально
хорошо, и свести к минимуму количество бесполезных комбинаций битов. Все
это описывается в стандарте IEEE 754. IEEE означает Институт инженеров
электротехники и электроники (Institute of Electrical and Electronic Engineers) —
это организация, деятельность которой включает разработку стандартов.
Мы хотим максимизировать точность с учетом доступных битов. Один из способов сделать это называется нормализацией — корректировка мантиссы так,
чтобы избежать ведущих (находящихся слева) нулей. Каждая корректировка
мантиссы влево требует соответствующей корректировки экспоненты. Еще один
прием от Digital Equipment Corporation (DEC) удваивает точность, отбрасывая
крайний левый бит мантиссы, поскольку мы знаем, что он всегда будет равен 1,
что дает место для еще одного бита.
Вам не нужно подробно разбирать IEEE 754 (по крайней мере, пока). Но стоит
учитывать два типа чисел с плавающей точкой, с которыми вы будете часто
сталкиваться: числа с одинарной и двойной точностью. Числа с одинарной
точностью используют 32 бита и могут представлять числа примерно в диапазоне ±10±38 с точностью около 7 цифр. Числа с двойной точностью используют
64 бита и могут представлять более широкий диапазон чисел, приблизительно
±10±308, с точностью около 15 цифр (рис. 1.13).
Одинарная точность
З

Экспонента

Мантисса
0

23 22

31 30

Двойная точность
З

63 62

Мантисса (высокая)

Экспонента
52 51

32

Мантисса (низкая)
31

0

Рис. 1.13. Форматы чисел с плавающей точкой стандарта IEEE
В обоих форматах учитывается бит знака для мантиссы — буква З на рис. 1.13.
Видно, что в числах с двойной точностью имеется на три разряда больше,

58   Глава 1. Внутренний язык компьютеров
чем в числах с одинарной, что дает в восемь раз больший диапазон. Кроме того,
в числах с двойной точностью мантисса на 29 бит больше, чем в числах с одинарной, что обеспечивает большую точность. Однако все это возможно за счет
использования вдвое большего количества битов, чем для чисел с одинарной
точностью.
Вы могли заметить, что для экспонент нет явного бита знака. Разработчики
IEEE 754 решили, что экспоненты всех нулей и всех единиц будут иметь особое
значение, поэтому фактические экспоненты необходимо втиснуть в оставшиеся
наборы битов. Им это удалось благодаря использованию смещенного значения
экспоненты. Для чисел с одинарной точностью смещение составляет 127. Это
означает, что набор битов для 127 (01111111) представляет собой экспоненту 0.
Набор битов для 1 (00000001) представляет экспоненту –126, а 254 (11111110)
представляет +127. То же самое действует и для двойной точности, за исключением того, что смещение составляет 1023.
Еще одно удобство IEEE 754 заключается в следующем. В него добавлены
специальные наборы битов для представления таких операций, как деление на
ноль, которое дает положительную или отрицательную бесконечность. Стандарт
также определяет специальное значение, называемое NaN — от английского
«not a number» («не число») — поэтому, если вы получили значение NaN, то это,
вероятно, означает, что совершенная операция недопустима. Эти специальные
комбинации битов используют зарезервированные значения экспоненты, о которых мы уже говорили.

Двоично-десятичная система счисления
Мы рассмотрели некоторые наиболее распространенные способы представления чисел в двоичном формате, однако существует и множество альтернативных
систем счисления. Одна из них — двоично-десятичная система (binary-coded
decimal, BCD), в которой используется 4 бита для представления всех десятичных цифр. Например, число 12 в двоичном формате равно 1100. Но в двоичнодесятичном формате оно представлено как 0001 0010, где 0001 — 1 в разряде
десятков, а 0010 — 2 в разряде единиц. Это гораздо более привычное и удобное
представление для людей, привыкших работать с десятичными числами.
Раньше компьютеры знали, как работать с BCD-числами, но эта система утратила свою популярность. Тем не менее ее все еще можно встретить, поэтому стоит
иметь о ней представление. В частности, многие устройства, взаимодействующие
с компьютерами, например дисплеи и акселерометры, используют BCD.
Основная причина, по которой BCD потеряла популярность, заключается в том,
что она не так эффективно использует биты, как двоичная. Для представления
BCD-числа требуется больше битов, чем для его двоичного варианта. Хотя сейчас биты стоят намного дешевле, чем раньше, они все еще не настолько дешевы,

Более простые способы работы с двоичными числами   59
чтобы можно было просто выбросить 6 бит из всех 16-битных комбинаций, поскольку это было бы равносильно потере колоссального количества доступных
битов (37.5 %).

Более простые способы работы
с двоичными числами
Доподлинно известно, что работа с двоичными числами может привести к слепоте — настолько она визуально утомительна! Люди придумали несколько
способов облегчить чтение двоичных чисел. Рассмотрим некоторые из них.

Восьмеричное представление
Один из подходов, которые не испортят вам зрение, — восьмеричное представление. Восьмеричный означает «имеющий основание 8», и идея состоит в том,
чтобы сгруппировать биты в тройки. К этому моменту вы уже должны знать,
что 3 бита можно использовать для представления 23, или восьми значений от 0
до 7. Допустим, есть чудовищное двоичное число, такое как 100101110001010100.
На него просто больно смотреть. На рис. 1.14 показано, как преобразовать его
в восьмеричное представление.
100 1 0 1 1 1 0 0 0 1 0 1 0 100
4

5

6

1

2

4

Рис. 1.14. Восьмеричное представление двоичных чисел
Таким образом, мы делим биты на группы по три бита, присваиваем каждой
группе восьмеричное значение и в результате получаем число 456124, прочитать
которое гораздо проще. Например, чтобы получить восьмеричное значение 100,
мы просто переводим его в двоичное число: (1 × 22) + (0 × 21) + (0 × 20) = 4.

Шестнадцатеричное представление
Восьмеричное представление все еще используется, но не так часто, как раньше.
Пальма первенства перешла к шестнадцатеричному представлению (основание 16), потому что в наши дни начинка компьютеров создается с точностью
до 8 бит, которые делятся без остатка на 4, но не на 3.
Было легко воспринимать некоторые знакомые цифры как двоичные, потому
что мы использовали всего две цифры — 0 и 1. Для восьмеричной системы
счисления понадобилось 8 цифр из 10. Для шестнадцатеричной системы нужно взять 16 цифр, но у нас столько нет. Нам нужен отдельный символ для

60   Глава 1. Внутренний язык компьютеров
представления 10, еще один для 11, и так до 15. Предположим (я уже говорил,
что мы будем так делать), что символы abcdef (или ABCDEF) представляют
значения от 10 до 16. Допустим, у нас есть еще одно страшное двоичное число —
11010011111111000001.
На рис. 1.15 показано, как преобразовать его в шестнадцатеричный формат.
1101 0011 1111 1100 0001
d

3

f

c

1

Рис. 1.15. Шестнадцатеричное представление двоичных чисел
В этом примере мы делим биты на четыре группы. Затем присваиваем каждой
группе один из 16 символов (0123456789abcdef). Например, 1101 (первая группа
из 4 бит) будет представлена как d, потому что она вычисляется как 1(23) + 1(22)
+ 0(21) + 1(20) = 13 в десятичной системе счисления, а число 13 соответствует
символу d. Сопоставляем следующую группу из 4 бит (0011) с другим символом
и т. д. Например, 11010011111111000001 преобразуется в d3fc1 в шестнадцатеричном формате. В табл. 1.9 представлен удобный список шестнадцатеричных
значений, к которому вы можете обращаться, пока не привыкнете переводить
их из одной системы в другую самостоятельно.
Таблица 1.9. Преобразование двоичного числа в шестнадцатеричное
Двоичное

Шестнадцатеричное

Двоичное

Шестнадцатеричное

0000

0

1000

8

0001

1

1001

9

0010

2

1010

a

0011

3

1011

b

0100

4

1100

c

0101

5

1101

d

0110

6

1110

e

0111

7

1111

f

Представление контекста
Откуда узнать, как интерпретировать число? Например, число 10 обозначает 2,
если это двоичное число, 8, если восьмеричное, 10, если десятичное, и 16, если

Именованные группы битов   61
шестнадцатеричное. В книгах по математике используются индексы, поэтому
можно применять их, чтобы различать числа: 102, 108, 1010 или 1016. Но индексы
неудобно набирать с клавиатуры. Было бы неплохо использовать одни и те же
обозначения, но, к сожалению, многие думают, что найдут лучший способ, и продолжают изобретать новые. Во многих языках программирования встречаются
следующие обозначения:
Число, начинающееся с 0, является восьмеричным, например 017.
Число, которое начинается с одной из цифр от 1 до 9, является десятичным
числом, например 123.
Число с префиксом 0x является шестнадцатеричным, например 0x12f.
Обратите внимание, что мы не можем отличить восьмеричный и десятичный 0,
но это не важно, потому что они имеют одинаковое значение. И лишь в немногих языках есть обозначение для двоичного кода, потому что числа в двоичной
системе на самом деле используются редко и обычно их можно определить по
контексту. Некоторые языки, такие как C++, используют префикс 0b для представления двоичных чисел.

Именованные группы битов
Компьютеры — это не просто неорганизованные связки битов. Люди, которые проектируют компьютеры, должны принимать решения о количестве
битов и их организации из соображений стоимости. Как и в случае с представлениями чисел, было опробовано много идей, но только некоторые из
них прижились.
Биты слишком малы, чтобы быть полезными сами по себе, поэтому их чаще всего
собирают в более крупные блоки. Например, компьютеры серии Honeywell 6000
использовали 36-битные блоки в качестве базовой структуры и позволяли разбивать их на 18-, 9- или 6-битные блоки или объединять в 72-битные блоки. DEC
PDP-8, первый коммерческий мини-компьютер (представленный в 1965 году),
использовал 12-битные блоки. Со временем мир остановился на 8-битных блоках
как на фундаментальной единице, которая была названа байтом.
Фрагменты разного размера имеют собственные имена, чтобы на них было легче
ссылаться. В табл. 1.10 приведены названия и размеры некоторых распространенных блоков, используемых сегодня.
Вы спросите, почему мы используем полуслова, длинные и двойные слова, но не
простые слова. Слово используется для описания естественного размера объектов
в конкретном компьютерном дизайне. Естественный размер относится к самому
большому фрагменту, с которым можно быстро работать. Например, на DEC
PDP-11 можно было работать с байтами, полусловами и длинными словами, но

62   Глава 1. Внутренний язык компьютеров
его внутренняя структура была 16-битной, поэтому и естественный размер для
этого компьютера был 16-битным. Такие языки программирования, как C и C++,
позволяют объявлять переменные как int (сокращение от integer) — переменные
естественного размера. Также можно объявлять переменные, используя набор
поддерживаемых определенных размеров.
Таблица 1.10. Названия коллекций битов
Имя

Число битов

Полубайт

4

Байт

8

Полуслово

16

Длинное слово

32

Двойное слово

64

Существует несколько стандартных терминов, которые позволяют легко ссылаться на большие числа. Сначала был один стандарт, а позже его заменили на
новый. У инженеров есть привычка находить слова, которые означают что-то
близкое к тому, что они хотели бы выразить, а затем использовать их, как если
бы эти слова действительно имели нужное значение. Например, в метрической
системе кило означает тысячу, мега означает миллион, гига — миллиард, а тера —
триллион. Эти термины были заимствованы, но немного изменены, потому что
в вычислениях используется основание 2 вместо 10.
Однако когда мы говорим о килобите или килобайте (Кбит или Кбайт) в вычислениях, то на самом деле не имеем в виду тысячу. Мы имеем в виду число,
ближайшее к тысяче в основании 2, что равно 1024, или 210. То же самое касается
мегабайта (Mбайт), который равен 220, гига (Гбайт) — 230, и тера (Tбайт) — 240.
Но в некоторых случаях имеется в виду версия с основанием 10. Чтобы определить нужную интерпретацию, необходим контекст. Традиционно версия с основанием 10 использовалась для обозначения размера дисковых накопителей.
Американский адвокат притворился, что ничего не знает об этом, и подал в суд
(на компанию Safier v. WDC), утверждая, что объем дискового накопителя меньше заявленного. (На мой взгляд, это было так же глупо, как и судебные иски,
утверждающие, что размер пиломатериалов 2 × 4 в действительности не 2 на
4 дюйма, несмотря на то что именно так всегда обозначались размеры неоструганных, необработанных досок.) Это привело к созданию новых стандартных
префиксов IEC: киби (КиБ) для 210, меби (МиБ) для 220, гиби (ГиБ) для 230

Представление текста   63
и теби (ТиБ) для 240. Они постепенно завоевывают популярность, хотя «киби»
для меня звучит как название корма для собак.
Термин символ часто используется как синоним слова байт, потому что, как
мы увидим в следующем разделе, коды символов обычно разрабатывались так,
чтобы соответствовать байтам. Теперь, с развитием поддержки неанглийских
языков, часто возникает необходимость говорить о многобайтовых символах.

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

Американский стандартный код обмена информацией
Как и в случае с числами, появилось несколько конкурирующих идей для представления текста. Победителем еще в далеком 1963 году стал Американский
стандартный код обмена информацией (American Standard Code for Information
Interchange, ASCII). В его версии всем символам на клавиатуре были присвоены
7-битные числовые значения. Например, 65 означает заглавную A, 66 — заглавную B и т. д. Проиграл же стандарт IBM, Расширенный двоично-десятичный
код обмена информацией (Extended Binary-Coded Decimal Interchange Code,
EBCDIC), основанный на кодировке, используемой для перфокарт. И да, часть
«BCD» в EBCDIC обозначает ту же двоично-десятичную систему, которую мы
видели ранее. В табл. 1.11 показаны коды ASCII.
Найдем в этой таблице букву А. Видим, что она имеет значение 65 в десятичной
системе счисления, что составляет 0x41 в шестнадцатеричной системе, а также
0101 в восьмеричной. Как оказалось, коды символов ASCII — это то место, где
восьмеричные числа по-прежнему широко используются по историческим
причинам.
В таблице ASCII много забавных кодов. Их называют управляющими символами, потому что они чем-то управляют, а не выводятся на печать. В табл. 1.12
показаны их значения.
Многие из них предназначались для управления передачей сообщений. Например, ACK (подтверждение) означает «я получил сообщение», а NAK (отрицательное
подтверждение) — «я не получил сообщение».

64   Глава 1. Внутренний язык компьютеров
Таблица 1.11. Таблица кодов ASCII
Десятичное

Восьмеричное

Символ

Десятичное

Восьмеричное

Символ

Десятичное

Восьмеричное

Символ

Десятичное

Восьмеричное

Символ

0

00

NUL

32

20

SP

64

40

@

96

60

`

1

01

SOH

33

21

!

65

41

A

97

61

a

2

02

STX

34

22



66

42

B

98

62

b

3

03

ETX

35

23

#

67

43

C

99

63

c

4

04

EOT

36

24

$

68

44

D

100

64

d

5

05

ENQ

37

25

%

69

45

E

101

65

e

6

06

ACK

38

26

&

70

46

F

102

66

f

7

07

BEL

39

27



71

47

G

103

67

g

8

08

BS

40

28

(

72

48

H

104

68

h

9

09

HT

41

29

)

73

49

I

105

69

i

10

0A

NL

42

2A

*

74

4A

J

106

6A

j

11

0B

VT

43

2B

+

75

4B

K

107

6B

k

12

0C

FF

44

2C

,

76

4C

L

108

6C

l

13

0D

CR

45

2D

-

77

4D

M

109

6D

m

14

0E

SO

46

2E

.

78

4E

N

110

6E

n

15

0F

SI

47

2F

/

79

4F

O

111

6F

o

16

10

DLE

48

30

0

80

5

P

112

70

p

17

11

DC1

49

31

1

81

51

Q

113

71

q

18

12

DC2

50

32

2

82

52

R

114

72

r

19

13

DC3

51

33

3

83

53

S

115

73

s

20

14

DC4

52

34

4

84

54

T

116

74

t

21

15

NAK

53

35

5

85

55

U

117

75

u

22

16

SYN

54

36

6

86

56

V

118

76

v

23

17

ETB

55

37

7

87

57

W

119

77

w

24

18

CAN

56

38

8

88

58

X

120

78

x

25

19

EM

57

39

9

89

59

Y

121

79

y

26

1A

SUB

58

3A

:

90

5A

Z

122

7A

z

27

1B

ESC

59

3B

;

91

5B

[

123

7B

{

28

1C

FS

60

3C

<

92

5C

\

124

7C

|

29

1D

GS

61

3D

=

93

5D

]

125

7D

}

30

1E

RS

62

3E

>

94

5E

^

126

7E

~

31

1F

US

63

3F

?

95

5F

_

127

7F

DEL

Представление текста   65
Таблица 1.12. Управляющие символы ASCII
NUL

Пустой символ

SOH

Начало заголовка

STX

Начало текста

ETX

Конец текста

EOT

Конец передачи

ENQ

Запрос

ACK

Подтверждение

BEL

Звуковой сигнал

BS

Возврат на шаг

HT

Горизонтальная табуляция

NL

Новая строка

VT

Вертикальная табуляция

FF

Прогон страницы

CR

Возврат каретки

SO

Переключение на другую ленту

SI

Переключение на исходную ленту

DLE

Экранирование управляющих
символов

DC1

1-й код управления устройством

DC2

2-й кодуправления устройством

DC3

3-й код управления устройством

DC4

4-й код управления устройством

NAK

Отрицательное подтверждение

SYN

Пустой символ для синхронного
режима передачи

ETB

Конец блока передаваемых данных

CAN

Отмена

EM

Конец носителя

SUB

Символ замены

ESC

Управляющая последовательность

FS

Разделитель файлов

GS

Разделитель групп

RS

Разделитель записей

US

Разделитель полей

SP

Пробел

DEL

Удаление

Развитие других стандартов
Некоторое время было достаточно стандарта ASCII, потому что он содержал
символы, необходимые для английского языка. Большинство первых компьютеров были американскими, а остальные — британскими. Потребность в поддержке других языков росла по мере того, как компьютеры становились более
доступными. Международная организация по стандартизации (International
Standards Organization, ISO) приняла стандарты ISO-646 и ISO-8859, которые
в основном представляют собой ASCII с некоторыми расширениями для символов ударения и других диакритических знаков, используемых в европейских
языках. Комитет по японским промышленным стандартам (Japanese Industrial
Standards, JIS) разработал стандарт JIS X 0201 для японских иероглифов. Также
существуют китайские, арабские стандарты и многие другие.
Одна из причин создания всех этих стандартов заключается в том, что они были
разработаны в то время, когда биты стоили намного дороже, чем сегодня, поэтому
символы были упакованы в 7 или 8 бит. Когда цена битов начала падать, был разработан новый стандарт Unicode, который назначал символам 16-битные коды.

66   Глава 1. Внутренний язык компьютеров
В то время считалось, что 16 бит будет достаточно, чтобы вместить все символы
на всех языках Земли с запасом места. С тех пор Unicode был расширен до 21 бита
(из которых действительны 1 112 064 значения), что должно сработать, но, скорее,
всего ненадолго, учитывая нашу любовь к созданию эмодзи с котиками.

8-битная форма представления Unicode
Компьютеры используют 8 бит для хранения символа ASCII, потому что они
не предназначены для обработки 7-битных величин. Опять же, хотя биты стали
намного доступнее, чем раньше, все же они не настолько дешевы, чтобы использовать 16 из них для хранения одной буквы, если можно обойтись восемью.
Unicode решает эту проблему, используя разные кодировки для кодов символов.
Кодировка — это комбинация битов, которая представляет другую комбинацию
битов. Совершенно верно — мы используем абстракции (биты) для создания
чисел, представляющих символы, а затем используем другие числа для представления этих чисел! Понимаете, что я имел в виду под гипотетическим значением?
В частности, существует кодировка, называемая 8-битной формой представления
Unicode (Unicode Transformation Format-8 bit, UTF-8), придуманная американским программистом Кеном Томпсоном (Ken Thompson) и его канадским
коллегой Робом Пайком (Rob Pike). Эта кодировка чаще всего используется
благодаря своей эффективности и обратной совместимости. UTF-8 назначает
8 бит каждому символу ASCII, поэтому они не занимают дополнительного места
для данных ASCII. Символы, отличные от ASCII, кодируются таким образом,
чтобы не нарушать работу программ, ожидающих ввода ASCII-символов.
UTF-8 кодирует символы как последовательность 8-битных блоков, часто называемых октетами. Одно из удобств UTF-8 заключается в том, что количество
наиболее значащих единиц в первом блоке определяет длину последовательности,
потому первый блок легко распознать. Это полезно, поскольку программам легко
находить границы символов. Все символы ASCII умещаются в 7 битах, поэтому
на каждый символ приходится ровно один фрагмент, что довольно удобно для
англоговорящих, поскольку для хранения символов английского языка нужно
меньше места, чем для других языков, которым необходимы не-ASCII-символы.
На рис. 1.16 показано, как UTF-8 кодирует символы по сравнению с Unicode.
На рис. 1.16 видно, что числовой код буквы A одинаков в ASCII и Unicode. Чтобы
закодировать A в UTF-8, мы обговариваем условие, что любые коды, которые
умещаются в 7 бит, включаются в один фрагмент UTF-8, и задаем MSB равным 0.
Вот почему в кодировке UTF-8 для буквы A в начале стоит 0. Далее видно, что
Unicode-вариант для символа π не умещается в 7 битах, ему нужно 11. Для кодирования π в UTF-8 мы используем два 8-битных фрагмента, первый из которых
начинается со 110, а второй — с 10, в результате чего в каждом фрагменте остается
5 и 6 бит соответственно для хранения оставшегося кода. Наконец, Unicode для
символа ♣ помещается в 16 битах и поэтому занимает три фрагмента UTF-8.

Использование символов для представления чисел   67

15 14 13 12 11 10 9
0

0

0

0

0

0

0

15 14 13 12 11 10 9
0

0

0

0

0

0

1

8

7

6

5

4

3

2

1

0

0

0

0

1

0

0

0

1

1

Unicode A
(0x0041)

0

0

1

0

0

0

1

1

UTF-8 A
(0x41)

7

6

5

4

3

2

1

0

8

7

6

5

4

3

2

1

0

1

1

1

0

0

0

0

0

0

Unicode
(0x03C0)

UTF-8
(0xCF 0x80)

1

1

0

0

1

1

1

1

1

0

0

0

0

0

0

0

7

6

5

4

3

2

1

0

7

6

5

4

3

2

1

0

15 14 13 12 11 10 9

8

7

6

5

4

3

2

1

0

0 0

1

1

0

0

0

1

1

Unicode
(0x2663)

UTF-8
(0xE2 0x99 0xA3)

0

1

0

0

1

1

0

1

1

1

0

0

0

1

0

1

0

0

1

1

0

0

1

1

0

1

0

0

0

1

1

7

6

5

4

3

2

1

0

7

6

5

4

3

2

1

0

7

6

5

4

3

2

1

0

Рис. 1.16. Примеры кодировки UTF-8

Использование символов
для представления чисел
UTF-8 использует числа для представления чисел, представляющих числа,
состоящие из битов, представляющих символы. Но и это еще не все! Теперь
мы собираемся применять символы для обозначения некоторых из этих чисел.
На заре межкомпьютерных коммуникаций люди хотели передавать между
компьютерами нечто большее, чем просто текст; нужно было отправлять двоичные данные. Но сделать это было непросто, потому что, как мы видели ранее,
многие значения ASCII были зарезервированы для управляющих символов и не
обрабатывались последовательно между системами. Также некоторые системы
поддерживали передачу только 7-битных символов.

Кодировка Quoted-Printable
Кодировка Quoted-Printable, также известная как QP-кодировка, — это механизм,
позволяющий передавать 8-битные данные по пути, который поддерживает только
7-битные данные. Она был создана для вложений электронной почты. Эта кодировка позволяет представить любое 8-битное байтовое значение тремя символами:
символом =, за которым следует пара шестнадцатеричных чисел, по одному для

68   Глава 1. Внутренний язык компьютеров
каждого полубайта. Конечно, при этом знак = получает особое значение и поэтому
должен быть представлен с использованием =3D, его значения из табл. 1.11.
В кодировке Quoted-Printable есть несколько дополнительных правил. Символы
табуляции и пробела должны быть представлены как =09 и =20 соответственно,
если они расположены в конце строки. Длина закодированных строк не может
превышать 76 символов. Знак = в конце строки — это мягкий разрыв строки,
который удаляется при декодировании данных получателем.

Кодировка Base64
Хотя кодировка Quoted-Printable работает, она не очень эффективна, поскольку
для представления единственного байта требуются три символа. Более эффективна кодировка Base64, и она действительно имела большое значение, когда
обмен данными между компьютерами занимал гораздо больше времени, чем
сегодня. Кодировка Base64 упаковывает 3 байта данных в 4 символа. 24 бита
данных в трех байтах разделены на четыре 6-битных блока, каждому из которых
назначается печатный символ, как показано в табл. 1.13.
Таблица 1.13. Кодировка символов Base64
Число

Символ

Число

Символ

Число

Символ

Число

Символ

0

A

16

Q

32

g

48

w

1

B

17

R

33

h

49

x

2

C

18

S

34

i

50

y

3

D

19

T

35

j

51

z

4

E

20

U

36

k

52

0

5

F

21

V

37

l

53

1

6

G

22

W

38

m

54

2

7

H

23

X

39

n

55

3

8

I

24

Y

40

o

56

4

9

J

25

Z

41

p

57

5

10

K

26

a

42

q

58

6

11

L

27

b

43

r

59

7

12

M

28

c

44

s

60

8

13

N

29

d

45

t

61

9

14

O

30

e

46

u

62

+

15

P

31

f

47

v

63

/

Байты 0, 1, 2 будут закодированы как AAEC (рис. 1.17).

Представление цветов   69

0
0

0

0

0

1
0

0(A)

0

0 0

0 0

0

0(A)

0

2
0

0

0

1

0

4(E)

0

0

0

0

0

1

0

2(C)

Рис. 1.17. Кодировка Base64
Эта кодировка преобразует каждый набор из трех байтов в четыре символа.
Но при этом не гарантируется, что длина данных будет кратна трем байтам.
Проблема решается с помощью символов заполнения; символ = будет добавлен
в конец строки, если строка занимает только 2 байта, либо же будут добавлены
символы ==, если строка займет всего 1 байт.
Кодировка Base64 до сих пор часто используется для вложений электронной
почты.

Кодировка URL
В одном из предыдущих разделов вы видели, что кодировка Quoted-Printable
наделила особой силой символ = и что эта же кодировка включала механизм для
представления обычного символа =. Практически идентичная схема используется в URL-адресах веб-страниц.
Если вы когда-нибудь рассматривали URL-адрес веб-страницы, вы могли заметить такие последовательности символов, как %26 и %2F. Они существуют
потому, что определенные символы имеют особое значение в контексте URLадреса. Но иногда нам нужно использовать эти символы как литералы — другими
словами, без специальных значений.
Как мы уже видели, символы представлены в виде последовательности 8-битных
блоков. Каждый фрагмент может быть представлен двумя шестнадцатеричными
символами, как показано на рис. 1.16. Кодировка URL, также известная как процентная кодировка, заменяет символ на знак %, за которым следует шестнадцатеричное представление этого символа.
Например, косая черта (/) имеет особое значение в URL-адресах. Она имеет
код 47 в ASCII, что равно 2F в шестнадцатеричном формате. Если нужно использовать / в URL-адресе, не учитывая его особое значение, мы заменяем его
на %2F. (И поскольку мы только что придали особое значение символу %, его
необходимо заменить на %25, если мы буквально имеем в виду %.)

Представление цветов
Еще одно распространенное использование чисел — представление цветов.
Вы уже знаете, что числа можно использовать для отображения координат на

70   Глава 1. Внутренний язык компьютеров
графике. Компьютерная графика предполагает создание изображений путем
нанесения капель цвета на эквивалент электронной миллиметровой бумаги.
Капля, нанесенная на каждую пару координат, называется элементом изображения, или, чаще, пикселем.
Компьютерные мониторы генерируют цвета, смешивая красный, зеленый и синий основные цвета, таким образом применяя цветовую модель RGB (от red,
green, blue — «красный, зеленый, синий»). Ее можно представить в виде цветового куба, в котором каждая ось представляет основной цвет, как показано на
рис. 1.18. Значение 0 означает, что конкретный основной цвет не используется,
а 1 означает, что он максимально яркий.
Синий
(0, 0, 1)

Голубой
(0, 1, 1)

Белый
(1, 1, 1)

Пурпурный
(1, 0, 1)

Серая линия
Зеленый
(0, 1, 0)

Черный
(0, 0, 0)

Красный
(1, 0, 0)

Желтый
(1, 1, 0)

Рис. 1.18. Цветовой куб RGB
Видим, что черный цвет обозначает, что ни один основной цвет не используется, а белый — что все основные цвета имеют максимальную яркость. Оттенок
красного цвета получается, если включается только основной красный цвет.
Смешивание красного и зеленого дает желтый оттенок. Серый цвет получается
в результате выбора одного и того же уровня для всех трех источников цвета.
Этот способ смешивания называется системой сложения цветов, поскольку
сложение основных цветов дает разный результат. На рис. 1.19 показаны координаты нескольких цветов в кубе.
Если вы пробовали рисовать, то наверняка лучше знакомы с субтрактивной
системой цветов, в которой основными цветами являются голубой, пурпурный
и желтый. Субтрактивная система цветов определяет цвета путем удаления
длин волн белого света, а не добавления цветного света, как в системе сложения.
Хотя ни одна из систем не может воспроизвести все цвета, которые видит глаз,

Представление цветов   71
в субтрактивной системе доступно больше оттенков, чем в системе сложения.
Существует целый набор предпечатных технологий, благодаря которым дизайн
будет выглядеть одинаково как на экране монитора, так и в печатном издании.
Если вас действительно интересует тема цвета, прочтите книгу Морин Стоун
(Maureen Stone) «A Field Guide to Digital Color».
B

B

B

G

G

R

Бордовый (0.5, 0, 0)

R

Пурпурный (0.5, 0, 0.5)

G

R

Оливковый (0.5, 0.5, 0)

Рис. 1.19. Примеры цветового куба RGB
Человеческий глаз — очень сложный механизм, созданный для выживания, а не
для вычислений. Он может различать около 10 миллионов цветов, но при этом
он нелинеен; удвоение уровня освещенности не обязательно приводит к удвоению воспринимаемой яркости. Хуже того, реакция глаза медленно меняется
со временем в зависимости от общего уровня освещенности. Это называется
адаптацией к темноте. К тому же глаз реагирует на цвета по-разному; он очень
чувствителен к изменениям зеленого цвета, в отличие от синего, — этот феномен
использовался в стандарте Национального комитета по телевизионным системам (National Television System Committee, NTSC). В современных компьютерах
решено округлить 10 миллионов до ближайшей степени двойки и использовать
24 бита для представления цвета. Эти 24 бита разделены на три 8-битных поля,
по одному для каждого из основных цветов.
Можно заметить, что в табл. 1.10 нет названия для 24 бит. Все потому, что современные компьютеры не предназначены для работы на 24-битных устройствах
(хотя существовало несколько 24-битных машин, таких как Honeywell DDP224). В результате цвета упаковываются в ближайший стандартный размер,
который составляет 32 бита (длинное слово), как показано на рис. 1.20.
Не используется
31

Красный
24 23

Зеленый
16 15

Синий
8 7

0

Рис. 1.20. Хранение цветов RGB
Видим, что эта схема оставляет 8 неиспользуемых бит для каждого цвета. Это
много, учитывая, что в компьютерных мониторах сегодня более 8 миллионов

72   Глава 1. Внутренний язык компьютеров
пикселей. Нельзя просто позволить этим битам пропасть зря, так что же с ними
делать? Можно использовать их для чего-то, чего не хватает в обсуждении цвета
выше: прозрачности, то есть способности «видеть сквозь» цвет. До сих пор мы
обсуждали только непрозрачные цвета, но их нельзя использовать, например,
для розовых очков.

Добавление прозрачности
В ранних анимационных фильмах каждый кадр рисовали вручную. Во-первых, это
была очень трудоемкая работа, а во-вторых, появлялся эффект визуального «дрожания», потому что было невозможно точно воспроизвести фон на каждом кадре.
Американские аниматоры Джон Брэй (John Bray) (1879–1978) и Эрл Херд
(Earl Hurd) (1880–1940) решили эту проблему: в 1915 году они изобрели целлулоидную анимацию. Движущихся персонажей рисовали на прозрачных листах
целлулоида, которые затем можно было перемещать по статическому фоновому
изображению.
Хотя компьютерная анимация уходит корнями в 1940-е годы, она стала понастоящему популярной в 1970-х и 1980-х годах. В то время компьютеры были
недостаточно быстрыми, чтобы делать все, что хотели режиссеры (подозреваю,
для режиссеров они никогда не будут достаточно быстрыми). И нам понадобился
механизм для объединения объектов, сгенерированных разными алгоритмами.
Как и целлулоидная анимация, прозрачность позволяет создавать композиции
или комбинировать изображения из разных источников. Вам, вероятно, знакомо
это понятие, если вы когда-нибудь работали в графическом редакторе, таком
как GIMP или Photoshop.
В 1984 году Том Дафф (Tom Duff) и Томас Портер (Thomas Porter) из Lucasfilm
изобрели способ реализации прозрачности и композиции, который стал стандартом. Они добавили значение прозрачности под названием альфа (α) к каждому пикселю. α — математическое значение от 0 до 1, где 0 означает, что цвет
полностью прозрачен, а 1 — что цвет полностью непрозрачен. Набор уравнений
композиционной алгебры определяет, как цвета с разными альфа-значениями
объединяются для создания новых цветов.
Дафф и Портер продумали грамотную реализацию. Поскольку они не применяют систему с плавающей точкой, значение α, равное 1, они представляют с числом
255, используя преимущества этих дополнительных 8 бит (см. рис. 1.20). Вместо
того чтобы сохранять красный, зеленый и синий цвета, Дафф и Портер сохраняют значения цвета, умноженные на α. Например, если бы цвет был средне-красным, он имел бы значение 200 для красного и 0 для зеленого и синего. Значение
красного было бы равно 200, если бы цвет был непрозрачным, потому что α было
бы равно 1 (со значением α, равным 255). Но α наполовину прозрачного среднего
красного цвета будет равно 0.5, поэтому сохраненное значение для красного

Выводы   73
цвета будет равно 200 × 0.5 = 100, а сохраненное значение α — 127 (255 × 0.5 =
= 127). На рис. 1.21 показано, как работает хранение пикселей с α.
Красный ×
31

Зеленый ×
24 23

Синий ×
16 15

8 7

0

Рис. 1.21. Хранение цветов RGBα
Следовательно, композиция изображений включает в себя умножение значений
цвета на α. Хранение цветов в предварительно умноженной форме означает, что
нам не нужно выполнять это умножение каждый раз, когда используется пиксель.

Кодирование цветов
Поскольку веб-страницы — это в основном текстовые документы, то есть представляют собой последовательность удобочитаемых символов (часто в UTF-8),
нам нужен способ представления цветов с помощью текста.
Это делается аналогично кодировке URL: цвета определяются с помощью
шестнадцатеричных троек. Шестнадцатеричная тройка — это символ #, за
которым следуют шесть шестнадцатеричных значений в формате #rrggbb, где
rr — значение красного цвета, gg — зеленого, а bb — синего. Например, #ffff00
означает желтый цвет, #000000 — черный, а #ffffff — белый. Каждое из трех
8-битных значений цвета преобразуется в двухсимвольное шестнадцатеричное
представление.
Хотя α также доступно на веб-страницах, не существует краткого формата для
его представления. Оно полностью использует дополнительный набор схем.

Выводы
Из этой главы вы узнали, что, несмотря на концептуальную простоту битов, их
можно использовать для представления сложных вещей, таких как очень большие числа, символы и даже цвета. Вы разобрались, как представлять десятичные
числа в двоичном формате, выполнять простую арифметику с использованием
двоичных чисел и представлять отрицательные числа и дроби. Вы также узнали
о различных стандартах кодирования букв и символов с помощью битов.
Гики шутят: «В мире есть 10 типов людей — те, кто понимает двоичные числа,
и те, кто нет». Теперь вы в первой группе.
В главе 2 вы познакомитесь с основами работы аппаратной части компьютеров —
мы разберем их физические компоненты и то, почему они вообще используют
биты.

2

Комбинаторная логика

В эпизоде «Город на краю вечности» из сериала «Звездный путь» 1967 года Спок говорит: «Мэм, я пытаюсь
построить мнемоническую схему памяти с помощью
каменных ножей и медвежьих шкур». Подобно Споку, люди придумали всевозможные гениальные способы
создания вычислительных устройств с использованием доступных ресурсов. Некоторые фундаментальные технологии
были созданы специально для вычислений; а многие — изобретены для других
целей, а затем адаптированы для вычислений. В этой главе мы рассмотрим
некоторые аспекты этой эволюции, в том числе удобное и довольно молодое
изобретение — электричество.
Из главы 1 вы узнали, что современные компьютеры используют двоичные контейнеры, называемые битами, в качестве внутреннего языка. Вам может быть
интересно, почему компьютеры применяют именно их, а не удобные для людей
десятичные числа. Чтобы понять, почему в современных технологиях принято
использовать биты, эту главу мы начнем с рассмотрения некоторых ранних вычислительных устройств, которые этого не делали. Биты сами по себе неудобны
для вычислений, поэтому мы поговорим о том, как упростить их использование.
Мы разберем некоторые более старые и простые технологии — механические
реле и вакуумные лампы, — а затем сравним их с современной реализацией битов
в аппаратном обеспечении с применением электричества и интегральных схем.
В главе 1 биты обсуждались в довольно абстрактной форме, а здесь мы перей­
дем к деталям. Физические устройства, в том числе те, которые работают с битами, называются аппаратным обеспечением. Мы поговорим об аппаратном

Задача для цифровых компьютеров   75
обеспечении, которое реализует комбинаторную логику, или булеву алгебру,
которую мы обсуждали в главе 1. Как и в предыдущей главе, сначала рассмотрим
простые строительные блоки, а затем объединим их, чтобы получить расширенную функциональность.

Задача для цифровых компьютеров
Для начала рассмотрим некоторые механические вычислительные устройства на
основе шестерен, которые появились еще до современной эпохи. Для двух соединенных шестерен соотношение количества их зубьев определяет относительную
скорость, что делает их полезными для умножения, деления и других вычислений.
Одним из механических устройств с зубчатой передачей является антикитерский
механизм, старейший известный пример компьютера, найденный на греческом
острове и датируемый примерно 100 годом до н. э. С помощью этого механизма
выполнялись астрономические расчеты — пользователь вводил дату, поворачивая
циферблат, а затем вращал рукоятку, чтобы получить положение Солнца и Луны
в этот день. Другой пример — компьютеры управления артиллерией времен
Второй мировой войны, которые выполняли тригонометрические и дифференциальные вычисления с использованием множества шестерен странной формы
со сложной конструкцией, что делало их настоящими произведениями искусства.
Примером механического компьютера без шестеренок является логарифмическая линейка, изобретенная английским священником и математиком Уильямом
Отредом (William Oughtred) (1574–1660). Она основана на логарифмах, придуманных шотландским физиком, астрономом и математиком Джоном Напьером
(John Napier) (1550–1617). Главная функция логарифмической линейки — выполнять умножение, используя тот факт, что log(x × y) = log(x) + log(y).
Логарифмическая линейка имеет фиксированную и подвижную шкалы. Произведение двух чисел вычисляется путем совмещения фиксированной шкалы x
с подвижной шкалой y, как показано на рис. 2.1.
log(3)
1
1

2
2

3

3
4

4
5

6

5
7

6
8

9

7

8

9

10

10

log(1.5)

log(1.5) + log(3) = log(4.5)

Рис. 2.1. Сложение с помощью логарифмической линейки
Логарифмическая линейка считается первым массовым вычислительным
устройством. Она представляет собой отличный пример того, как люди решали

76   Глава 2. Комбинаторная логика
проблему с помощью доступных им в то время технологий. Сегодня пилоты самолетов все еще используют в качестве резервного устройства круговую версию
логарифмической линейки — бортовой компьютер, выполняющий вычисления,
связанные с навигацией.
Подсчет — исторически важное применение вычислительных устройств. Поскольку количество пальцев у человека ограниченно и они нужны ему для
других целей, кости и палки с надрезами, называемые счетными палочками,
использовались в качестве вычислительных средств еще за 18 000 лет до н. э.
Существует даже теория, что египетский глаз Гора применялся для представления двоичных дробей.
Английский ученый Чарльз Бэббидж (Charles Babbage) (1791–1871) убедил
британское правительство профинансировать строительство сложного десятичного механического вычислителя, названного разностной машиной, которая
первоначально была разработана гессенским военным инженером Иоганном
Хельфрихом фон Мюллером (Johann Helfrich von Müller) (1746–1830). Снискавшая популярность благодаря роману Уильяма Гибсона (William Gibson)
и Брюса Стерлинга (Bruce Sterling)1, названному в ее честь, разностная машина
опередила свое время, потому что технологии обработки металла в тот период
не подходили для изготовления деталей с требуемой точностью.
Однако уже тогда выпускались простые десятичные механические калькуляторы, поскольку они не требовали такой же сложной металлообработки. Например, счетные машины, которые могли складывать десятичные числа, были
созданы в середине XVII века для бухгалтерского учета и счетоводства. Было
выпущено много различных моделей, а в более поздних версиях счетных машин
ручные рычаги были заменены на электродвигатели, что упростило их работу.
Фактически легендарный старомодный кассовый аппарат представлял собой
счетную машину с денежным ящиком.
Все эти исторические примеры делятся на две категории, о чем мы поговорим
дальше.

Разница между аналоговым и цифровым представлением
Существует важное различие между такими устройствами, как логарифмическая
линейка (счетные палочки) и счетная машина. На рис. 2.2 показано сравнение одной из шкал логарифмической линейки с рис. 2.1 с пронумерованными пальцами.
И шкала логарифмической линейки, и количество пальцев измеряются от 1 до
10. Мы можем представить значения наподобие 1.1 на шкале, что довольно
удобно, но не можем сделать это с помощью пальцев, не применив фантазию
1

В России книга была издана под не совсем корректным названием «Машина различий». — Примеч. ред.

Задача для цифровых компьютеров   77
(или ловкость рук). Это потому, что шкала, как говорят математики, непрерывна — она может представлять действительные числа. А вот пальцы математики
назвали бы дискретными, потому что с их помощью можно представить только
целые числа. Между такими числами нет других значений. Мы можем только
переходить от одного целого числа к другому, как с пальца на палец.
1.1

1

2

2

3

4

3

5

8
4

7

5

6

7

8

9

10

9

6

1

10

Рис. 2.2. Непрерывные и дискретные измерения
Говоря об электронике, мы используем слово «аналоговый» в значении «непрерывный» и «цифровой» в значении «дискретный» (легко запомнить, что
пальцы цифровые (digital), потому что на латинский слово «палец» переводится
как digitus (цифра)). Вы, наверное, уже знакомы с терминами «аналоговый»
и «цифровой». Несомненно, вы учились программировать с помощью цифровых компьютеров, однако вы наверняка не знали, что существуют и аналоговые
компьютеры, такие как логарифмические линейки.
Аналоговые вычисления кажутся более удобными, потому что в них можно
оперировать действительными числами. Однако в этом случае появляются
проблемы с точностью. Например, мы можем выбрать число 1.1 на шкале
логарифмической линейки на рис. 2.2, потому что эта часть шкалы шире, чем
остальные, и для 1.1 есть отдельная отметка. Но найти 9,1 намного сложнее, потому что эта часть шкалы гораздо более узкая, а число находится где-то между
отметками 9.0 и 9.2. Разницу между 9.1 и 9.105 было бы трудно различить даже
с помощью микроскопа.
Конечно, можно увеличить масштаб. Например, можно получить более точные
данные, если использовать шкалу длиной с футбольное поле. Однако сложно
создать портативный компьютер со 120-ярдовой шкалой, не говоря уже о том,
сколько энергии потребуется для управления таким большим объектом. Нам

78   Глава 2. Комбинаторная логика
нужны небольшие, быстрые компьютеры с низким энергопотреблением. В следующем разделе я объясню еще одну причину, по которой размер важен.

Почему для аппаратного обеспечения размер имеет значение
Представьте, что вам нужно отвозить детей в школу, которая находится в 10 милях от вас, и забирать их оттуда со средней скоростью 40 миль в час. Расстояние
и скорость таковы, что за час вы успеете проехать туда и обратно только два раза.
Нельзя завершить поездку быстрее, если только не увеличить скорость или не
выбрать другую школу поблизости.
Современные компьютеры вместо детей перевозят электроны. Электрическое
поле в проводнике распространяется со скоростью света — около 300 миллио­
нов метров в секунду (за исключением США, где скорость составляет около
миллиарда футов в секунду). Поскольку мы еще не нашли способ обойти это
физическое ограничение, единственный способ минимизировать время в пути
в компьютерах — расположить его части близко друг к другу.
Сегодняшние компьютеры имеют тактовую частоту около 4 ГГц, что означает,
что они могут выполнять четыре миллиарда операций в секунду. За четыре миллиардные доли секунды электрическое поле преодолеет всего 75 миллиметров.
На рис. 2.3 показан типичный центральный процессор (ЦП) размером около
18 миллиметров с каждой стороны. Чтобы совершить два полных обхода этого
процессора, как раз достаточно четырех миллиардных долей секунды. Отсюда
следует, что уменьшение размеров позволяет повысить производительность.

Рис. 2.3. Микрофотография ЦП (любезно предоставлена корпорацией Intel)

Задача для цифровых компьютеров   79
Кроме того, подобно перевозке детей в школу и обратно, передача данных
требует энергии, и одного кофе для этого недостаточно. Уменьшение размера
вещей сокращает количество обходов, что снижает объем необходимой энергии.
В итоге мы получаем более низкое энергопотребление и меньшее тепловыделение — чтобы телефон не прожег дыру в кармане. Это одна из причин, почему
создатели вычислительных устройств всегда стремились уменьшить размеры
аппаратного обеспечения. Но если делать вещи очень маленькими, возникают
другие проблемы.

Цифровые решения для более стабильных устройств
Несмотря на то что уменьшение размеров обеспечивает быстроту и эффективность, нарушить работу очень маленьких объектов довольно легко. Немецкий
физик Вернер Гейзенберг (Werner Heisenberg) (1901–1976) был абсолютно
уверен в этом.
Представьте стеклянный мерный стакан с делениями от 1 до 10 унций.
Если вы нальете немного воды в стакан и поднимете его, будет трудно сказать,
сколько унций в стакане, из-за того что рука немного дрожит. А теперь представьте мерный стакан в миллиард раз меньше. Никто не сможет удерживать
его достаточно неподвижно, чтобы получить точные показания. Фактически,
даже если поставить этот крошечный стакан на стол, это все равно не поможет,
потому что при таком размере движение атомов не позволит ему оставаться
неподвижным. В крошечных масштабах Вселенная очень нестабильна.
И мерный стакан, и линейка являются аналоговыми (непрерывными) устройствами — с них очень легко считать неверные показания. Таких отклонений,
как случайное космическое излучение, достаточно, чтобы создавать волны
в микроскопических мерных стаканчиках, но они с меньшей вероятностью повлияют на дискретные устройства: пальцы, счетные палочки или механические
калькуляторы. Это потому, что дискретные устройства используют критерии
принятия решения. При счете на пальцах не бывает «промежуточных» значений.
Можно изменить логарифмическую линейку, включив в нее критерии принятия решения — добавив фиксаторы (что-то вроде механических закрепок)
в целочисленных позициях. Но как только мы это сделаем, мы превратим ее
в дискретное устройство, потерявшее способность представлять действительные числа. Фактически критерии принятия решения мешают представлению
определенных диапазонов значений. Математически это похоже на округление
чисел до ближайшего целого.
До сих пор мы говорили только о помехах извне, поэтому вы можете подумать,
что можно минимизировать их, используя что-то вроде защитного экрана.
В конце концов, свинец защитил Супермена от криптонита. Но существует
и другой, более коварный источник помех. Электрический ток воздействует на

80   Глава 2. Комбинаторная логика
объекты на расстоянии, как гравитация, — и это хорошо, иначе у нас не было
бы радио. Но это также означает, что сигнал, идущий по проводникам внутри
микросхемы, может влиять на сигналы на других проводниках, особенно когда
они расположены так близко друг к другу. Расстояние между проводниками
внутри современного микропроцессора составляет несколько нанометров
(10–9 метров). Для сравнения: диаметр человеческого волоса составляет около
100 000 нанометров. Эти помехи немного похожи на ветер, который вы чувствуете, когда проезжаете мимо встречного автомобиля. Поскольку не существует
простого способа защиты от эффекта перекрестных помех, важно использовать
цифровую схему, которая имеет более высокую помехозащищенность от критериев принятия решения. Можно, конечно, уменьшить влияние помех, увеличив
размеры объектов, чтобы провода были дальше друг от друга, но это противоречит другим целям. Дополнительная энергия, необходимая для преодоления
критерия принятия решения, дает степень защищенности от шума, которую не
получить при использовании непрерывных устройств.
На самом деле стабильность, полученная благодаря критериям принятия решений, является основной причиной, по которой мы создаем цифровые (дискретные) компьютеры. Но, как вы, возможно, заметили, мир — это аналоговое
(непрерывное) место, если не брать в расчет вещи, которые настолько малы, что
к ним применима квантовая физика. В следующем разделе вы узнаете, как мы
управляем аналоговым миром, чтобы добиться цифрового поведения, необходимого для создания стабильных вычислительных устройств.

Цифровые устройства в аналоговом мире
Многие инженерные разработки включают в себя умное применение естественных передаточных функций, обнаруженных учеными. Они подобны функциям,
которые вы изучаете на уроках математики, за исключением того, что они представляют явления реального мира. Например, на рис. 2.4 показан график передаточной функции для датчика цифровой камеры (или пленки в аналоговой
камере старого образца, если на то пошло).
Верхняя часть
Записанная
яркость
(выход)

Линейная область

Основание
Свет (вход)

Рис. 2.4. Передаточная функция датчика камеры или пленки

Задача для цифровых компьютеров   81
Ось X показывает количество входящего света (вход), а ось Y представляет количество записанной яркости, или света, зарегистрированного датчиком (выход).
Кривая показывает отношения между ними.
Подберем значения X и Y для функции, перемещая воображаемый входной
шарик по кривой и получая разные выходы. Видим, что передаточная функция дает разные значения записанной яркости для различных значений света.
Обратите внимание, что кривая — это не прямая линия. Если слишком много
света попадает на верхнюю часть кривой, изображение будет передержано,
поскольку записанные значения яркости будут ближе друг к другу, чем
в реальности. Точно так же, если свет попадает на основание кривой, снимок
будет недодержан. Обычно, если только не требуется какой-то особый эффект, цель состоит в регулировании экспозиции таким образом, чтобы она
попала в линейную область — тогда снимок будет максимально приближен
к реальности.
Инженеры придумали всевозможные уловки для использования передаточных
функций — например, настройка выдержки и диафрагмы камеры так, чтобы свет
попадал в линейную область. Еще один пример — схемы усилителя, например
те, которые управляют динамиками или наушниками в плеере.
На рис. 2.5 показано влияние изменения громкости на передаточную функцию
усилителя.
Выход
2 3 4
Коэф1 фициент
5
усиле0 ния 6
9 8 7
Вход

Выход

6 7 8
Коэф5 фициент
9
усиле4 ния 0
3 2 1

Вход

Рис. 2.5. Влияние коэффициента усиления на передаточную функцию усилителя
Регулятор громкости регулирует коэффициент усиления, или крутизну кривой.
Как видите, чем больше коэффициент, тем круче кривая и тем громче выход.
А что было бы, если бы у нас был один из тех специальных усилителей из
фильма «Это — Spinal Tap!» 1984 года, где можно увеличить усиление до 11?
Тогда сигнал не ограничивался бы линейной областью. Данный результат
приводит к искажению, поскольку выходной сигнал уже не является точным
воспроизведением входного, что ухудшает звучание. На рис. 2.6 видно, что выход не похож на вход, потому что вход нарушает границы линейной области
передаточной функции.

82   Глава 2. Комбинаторная логика

Выход

Вход

Рис. 2.6. Ограничение усилителя
Небольшое изменение входных данных вызывает скачок выходных данных
на крутой части кривой. Это похоже на момент перехода с одного пальца на
другой — на критерий принятия решения, называемый порогом. Это искажение
является полезным явлением, потому что выходные значения находятся по обе
стороны от порогового значения; трудно поймать значения, близкие к порогу.
Непрерывное пространство при этом разделяется на дискретные области, что
нам и нужно для стабильности и помехоустойчивости — способности работать
при наличии помех. Аналоговое представление стремится к использованию
большой линейной области, а цифровое — малой.
Возможно, вы естественным образом открыли этот феномен, качаясь на качелях в детстве (если вам посчастливилось вырасти до того, как детские игровые
площадки стали считаться опасными). Гораздо стабильнее находиться у основания (в самом низу) качели или ее верхней части (в самом верху), чем пытаться
балансировать где-то между ними.

Почему вместо цифр используются биты
Мы говорили о том, почему цифровые технологии лучше подходят для компьютеров, чем аналоговые. Но почему компьютеры используют биты вместо цифр?
В конце концов, люди используют цифры, и нам удобно считать до десяти, потому что у нас 10 пальцев.
Очевидная причина заключается в том, что у компьютеров нет пальцев. (Компьютер с пальцами — жуткое зрелище.) С одной стороны, счет на пальцах интуитивно
понятен, но это не очень эффективное использование пальцев, потому что для
одной цифры используется один палец. С другой стороны, если применять каждый
палец для представления значения, как в случае с битами, можно сосчитать более
чем до тысячи. Эта идея не нова; фактически китайцы использовали 6-битные числа для работы с гексаграммами в «Книге Перемен» еще в IX в. до н. э. Применение
битов вместо пальцев повышает эффективность счета более чем в 100 раз. Даже
использование групп из четырех пальцев для представления десятичных чисел

Знакомство с принципами работы электрического тока   83
с применением двоично-десятичной системы (BCD), которую мы рассматривали
в главе 1, превосходит наш обычный метод подсчета по эффективности.
Еще одна причина, по которой в аппаратном обеспечении лучше использовать
биты, чем цифры, заключается в том, что не существует простого способа настроить передаточную функцию с помощью цифр так, чтобы получить 10 различных
пороговых значений. Можно создать аппаратное обеспечение, реализующее
левую часть рис. 2.7, но оно будет намного сложнее и дороже, чем 10 копий
устройств, создающих правую часть рисунка.

8
6
4
2
0
01234

6 7 59

1

9
7
5
3
1

8

0

Порог

1

0

Рис. 2.7. Десятичные и двоичные пороги
Конечно, если бы мы могли построить 10 порогов в одном пространстве, мы бы это
сделали. Но, как мы уже поняли, лучше задействовать 10 бит вместо одной цифры.
Именно так и работает современное аппаратное обеспечение. Мы используем преимущества передаточной функции в основании и верхней части, которые на языке
электротехники называются отсечением и насыщением соответственно. При этом
у нас есть много места для маневра; получить неправильные выходы можно только
при значительных помехах. Кривая передаточной функции настолько крута, что
выходной сигнал переключается с одного значения на другое.

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

Электрический ток на примере сантехники
Электричество невидимо, поэтому его трудно визуализировать, так что представим вместо него воду. Электричество поступает из источника энергии (например,
батареи) точно так же, как вода поступает из резервуара. Батареи разряжаются
и нуждаются в подзарядке — так же и резервуары для воды высыхают и нуждаются в повторном наполнении. Солнце — единственный главный источник

84   Глава 2. Комбинаторная логика
энергии, который у нас есть; в примере с водой тепло от Солнца вызывает испарение, а водяной пар затем превращается в дождь, наполняющий резервуар.
Начнем с простого водяного клапана, изображенного на рис. 2.8.
1
0

Рис. 2.8. Водяной клапан
Как видите, у клапана есть ручка для открывания и закрывания. На рис. 2.9
показан настоящий запорный клапан, получивший свое название от задвижки,
которая открывается и закрывается при помощи ручки. Вода может пройти по
клапану, когда он открыт. Представим, что 0 означает «закрыто», а 1 — «открыто».

Рис. 2.9. Закрытый и открытый запорный клапан
Используем два клапана и несколько труб, чтобы проиллюстрировать операцию И, как показано на рис. 2.10.
Как видите, вода течет только тогда, когда оба клапана открыты или равны 1, что,
как мы выяснили в главе 1, является определением операции И. Когда выход
одного клапана соединяется со входом другого, как на рис. 2.10, это называется
последовательным соединением, которое реализует операцию И. Параллельное
соединение,показанное на рис. 2.11, получается в результате соединения входов
и выходов клапанов, что реализует операцию ИЛИ.

Знакомство с принципами работы электрического тока   85

0

0

1
0

1
0

1

1

Рис. 2.10. Операция И на примере сантехники
1
0

0

0

1
0

1

1

Рис. 2.11. Операция ИЛИ на примере сантехники

86   Глава 2. Комбинаторная логика
Электрическому току требуется время на прохождение через компьютерную
микросхему — так и воде нужно время, чтобы распространиться по трубам. Вы,
вероятно, знаете из собственного опыта, что температура воды в душе меняется
не сразу после поворота ручки. Этот эффект называется задержкой распространения, и мы скоро поговорим о нем подробнее. Задержка непостоянна; в случае
с водой изменение температуры заставляет трубы расширяться или сжиматься,
что изменяет скорость потока и, следовательно, время задержки.
Ток проходит по проводу, как вода по трубе. Он представляет собой поток электронов. Кусок провода состоит из двух частей: металл внутри, как и пространство
внутри трубы, является проводником, а покрытие снаружи, как и сама водопроводная труба, является изолятором. Поток можно включать и выключать с помощью клапанов. В мире электричества клапаны называют переключателями.
Они настолько похожи между собой, что устаревшее устройство под названием
«вакуумная трубка» также было известно как термоэлектронный клапан.
Вода не просто пассивно течет по водопроводным трубам; она приводится
в движение давлением, которое может различаться по силе. Электрический
эквивалент давления воды — это напряжение (V), измеряемое в вольтах (В),
названных в честь итальянского физика Алессандро Вольта (1745–1827). Величина потока называется силой тока (I) и измеряется в амперах, названных
в честь французского математика Андре Мари Ампера (1775–1836).
Вода может течь по широким или по узким трубам, но чем у
 же труба, тем больше
сопротивление ограничивает количество воды, которая может пройти по трубе.
Даже при большом напряжении (давлении воды) вы не сможете получить большой ток (поток) из-за сопротивления при слишком узком проводнике (трубе).
Сопротивление (R) измеряется в Омах (Ω), названных в честь немецкого математика и физика Георга Симона Ома (1789–1854).
Эти три переменные — напряжение, сила тока и сопротивление — связаны
законом Ома, который гласит, что I = U/R, что читается как «сила тока равна
напряжению, деленному на сопротивление». Таким образом, как и в случае с водопроводными трубами, большее сопротивление означает меньшую силу тока.
Сопротивление также превращает электричество в тепло — по этому принципу
работает все, от тостеров до электрических одеял. На рис. 2.12 показано, как сопротивление затрудняет проталкивание тока напряжением.
Легкий способ понять закон Ома — выпить молочный коктейль через трубочку.

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

Знакомство с принципами работы электрического тока   87

Вольты

Вольты

Амперы

Вольты

Омы

Омы

Амперы

Амперы

Вольты

Омы

Омы

Амперы

Рис. 2.12. Закон Ома
Оказывается, воздух — неплохой изолятор; ток не может течь между двумя
металлическими предметами, если они не соприкасаются. (Обратите внимание:
я сказал, что воздух — «довольно хороший» изолятор; при достаточно высоком
напряжении воздух ионизируется и превращается в проводник. Молния — хороший пример.)
Водопроводную систему в здании можно показать на
чертеже. Электрические системы, называемые цепями,
изображаются с помощью принципиальных схем, в которых используются условные обозначения для каждого
из компонентов. На рис. 2.13 показано условное обозначение для простого переключателя.

Рис. 2.13. Условное
обозначение
однополюсного
переключателя на
одно направление

Такой переключатель похож на подъемный мост: ток
(автомобили) не может перейти с одной стороны моста
на другую, когда стрелка на схеме (мосте) направлена вверх. Это легко проиллюстрировать с помощью старомодных рубильников, показанных на рис. 2.14, — их
часто показывают в глупых научно-фантастических фильмах. Рубильники попрежнему используются для таких вещей, как электрические разъединители, но
в наши дни их обычно прячут в защитных контейнерах, чтобы поджарить себя
было не так-то просто.
На рис. 2.13 и 2.14 показаны однополюсные переключатели на одно направление
single-pole, single-throw, SPST). Полюса — это количество соединенных вместе
переключателей, которые перемещаются вместе. Водяные клапаны в предыдущем разделе были однополюсными; можно сделать двухполюсный клапан,
приварив стержень между ручками пары клапанов, чтобы они оба двигались
вместе при перемещении стержня. Переключатели и клапаны могут иметь любое
количество полюсов. Термин «в одном направлении» означает, что существует
только одна точка соприкосновения: что-то может быть либо включено, либо
выключено, но не то и другое одновременно. Для этого нам понадобится однополюсное устройство на два направления (single-pole, double-throw, SPDT).
На рис. 2.15 показано условное обозначение для этого монстра.

88   Глава 2. Комбинаторная логика

Рис. 2.14. Однополюсный рубильник на одно направление

Рис. 2.15. Условное обозначение SPDT-переключателя
Он похож на железнодорожную стрелку, которая направляет поезд на тот или
иной путь, или трубу, разделенную на две отдельные трубы, как показано на
рис. 2.16.

Рис. 2.16. Водяной SPDT-клапан
Когда ручка опущена, вода течет через верхний клапан. Вода будет течь через
нижний клапан, если поднять ручку.
Терминологию переключателя можно расширить, чтобы описать любое количество полюсов и направлений. Например, двухполюсный переключатель на два
направления (double-pole, double-throw, DPDT) должен быть нарисован, как показано на рис. 2.17, с пунктирной линией, указывающей, что полюса объединены,
то есть перемещаются вместе.

Знакомство с принципами работы электрического тока   89

Рис. 2.17. Условное обозначение DPDT-переключателя
На рис. 2.18 показан реальный вид DPDT-рубильника.

Рис. 2.18. DPDT-рубильник
Ранее я опустил некоторые подробности, касающиеся наших гидротехнических
сооружений: система не будет работать, если воде будет некуда течь. Вода не
может течь, если слив забит. Поэтому должен быть способ вернуть воду из слива
обратно в резервуар для воды, иначе вся система окажется бесполезной.
То же верно и для электрических систем. Ток от источника энергии проходит
через элементы схемы и возвращается к источнику. Вот почему это называется
электрической цепью. Представьте это так: бегун на трассе должен вернуться на
стартовую линию, чтобы пробежать еще один круг.
Посмотрите на простую электрическую схему на рис. 2.19. В ней появляются два
новых условных обозначения: один для источника напряжения (слева) и один
для лампочки (справа). Построив такую схему, можно включать и выключать
свет с помощью переключателя.
Ток не будет течь, если переключатель разомкнут. Когда переключатель замкнут,
ток течет от источника напряжения через переключатель, лампочку и обратно

90   Глава 2. Комбинаторная логика
к источнику напряжения. Последовательные и параллельные переключатели
работают так же, как и их аналоги с водяными клапанами.

Источник
напряжения

+
V


Лампочка

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

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

Реле
Электричество использовалось для питания компьютеров задолго до изобретения электроники. Электричество и магнетизм тесно взаимосвязаны, что
обнаружил датский физик Ганс Кристиан Эрстед (1777–1851) в 1820 году. Если
свернуть моток провода в катушку и пропустить через нее ток, то катушка
станет электромагнитом. Электромагниты можно включать и выключать,
а также использовать для перемещения вещей. Они подойдут и для управления водяными клапанами — так работает большинство автоматических систем
пожаротушения. Кроме того, электромагнетизм позволяет нам создавать различные электродвигатели. Если вокруг катушки с проволокой вращать магнит,
будет вырабатываться электричество — таков принцип работы генераторов;

Создание аппаратного обеспечения, работающего с битами   91
фактически именно так мы получаем большую часть электроэнергии. На всякий случай, если вам захочется это проверить, получить электричество от
электромагнита можно, если очень быстро вращать магнит вокруг катушки.
Подобный опыт может шокировать, но этот эффект, называемый обратной
ЭДС (электродвижущей силой), очень полезен; так катушка зажигания автомобиля производит искру для свечей зажигания. Таким же образом работают
электрические заборы.
Реле — это устройство, которое использует электромагнит для переключения
контактов. На рис. 2.20 показано условное обозначение реле с одной группой
контактов на переключение, которое, как видим, очень похоже на обозначение
переключателя, прикрепленного к катушке.

Движущаяся часть
Железо
Катушка

Рис. 2.20. Условное обозначение SPDT-реле
На рис. 2.21 показан реальный вид реле с одной группой контактов на замыкание.
Переключатель открыт, когда на катушку не подается питание, поэтому это реле
называется нормально разомкнутым. Если бы переключатель был замкнут без
питания, это было бы нормально замкнутое реле.

Рис. 2.21. Нормально разомкнутое SPST-реле

92   Глава 2. Комбинаторная логика
Контакты внизу идут к катушке реле; остальное очень похоже на вариацию переключателя. Контакт посередине перемещается в зависимости от того, находится
ли катушка под напряжением. Мы можем реализовать логические функции
с помощью реле, как показано на рис. 2.22.

Вход 1

Выход
И

Вход 2

Вход 1

Выход
ИЛИ

Вход 2

Рис. 2.22. Схемы соединений реле для логических операций И и ИЛИ
Видно, что в верхней части рис. 2.22 два выходных провода соединяются только
в том случае, если оба реле активированы, что определяет логическую операцию И. Точно так же внизу провода соединяются, если срабатывает какое-либо
реле, что представляет логическую операцию ИЛИ. Обратите внимание на
маленькие черные точки на рисунке. Они указывают на соединения проводов
на схемах; провода, пересекающиеся без точки, не соединены.
Реле позволяют делать то, что невозможно сделать с переключателями. Например, можно построить инверторы, реализующие логическое НЕ, без которого
возможности булевой алгебры очень ограниченны. Можно использовать выход
схемы И наверху, чтобы управлять одним из входов на схеме ИЛИ внизу. Именно
эта способность заставлять переключатели управлять другими переключателями
позволяет создавать сложную логику компьютеров.
С помощью реле создано множество удивительных вещей. Например, существует шаговое реле с одной группой контактов на 10 шагов с двумя катушками. Одна
катушка перемещает контакт в следующее положение при каждом включении,
а другая сбрасывает реле, возвращая контакт в исходную позицию. Огромные
здания, заполненные шаговыми реле, раньше использовались для подсчета
цифр телефонных номеров при наборе. На телефонных станциях всегда было
очень шумно. Шаговые реле также придают очарование старым автоматам для
игры в пинбол.

Создание аппаратного обеспечения, работающего с битами   93
Еще один интересный факт о реле — порог передаточной функции вертикален;
независимо от того, как медленно увеличивается напряжение на катушке, переключатель всегда переходит из одного положения в другое. Это озадачило меня
в детстве; только изучая уравнения Лагранжа — Гамильтона на первом курсе
колледжа, я узнал, что значение передаточной функции не определено на пороге, что вызывает переход.
Минусы реле заключаются в том, что они работают медленно, потребляют
много электроэнергии и перестают работать, если грязь (или жучки) попадает
на контакты переключателя. Фактически термин bug (ошибка или «жучок»)
был популяризирован американским ученым-компьютерщиком Грейс Хоппер
(Grace Hopper) в 1947 году, когда оказалось, что ошибка в компьютере Harvard
Mark II возникла из-за мотылька, застрявшего в контактах реле. Еще одна интересная проблема возникает во время использования контактов реле для управления другими реле. Вспомните, что внезапное отключение питания катушки
на мгновение генерирует очень высокое напряжение и что воздух становится
проводящим при высоких напряжениях. Это явление часто ведет к появлению
искры между контактами реле, что приводит к их износу. Из-за этих недостатков
люди начали искать что-то, что выполняло бы ту же работу, что и реле, но не
имело бы движущихся частей.

Вакуумные лампы
Британский физик и инженер-электрик сэр Джон Амброуз Флеминг (1849–
1945) изобрел вакуумную лампу. Она была основана на принципе термоэлектронной эмиссии, который гласит, что, если нагреть предмет до достаточной
температуры, это приведет к излучению электронов. У вакуумных ламп есть
нагреватель, который нагревает катод, действующий подобно питчеру в бейсболе. В вакууме электроны (бейсбольные мячи) текут от катода к аноду (кетчеру).
Примеры ламп показаны на рис. 2.23.

Рис. 2.23. Вакуумные лампы

94   Глава 2. Комбинаторная логика
Электроны обладают некоторыми общими с магнитами свойствами, в том числе тем, что противоположные заряды притягиваются, а одинаковые — отталкиваются. Вакуумная лампа может содержать дополАнод
нительный элемент-дозатор, называемый сеткой,
который отталкивает электроны, идущие от катода,
чтобы предотвратить их попадание на анод. Вакуумная лампа, содержащая три элемента (катод, сетСетка
ку и анод), называется триодом. На рис. 2.24 показано схематическое обозначение триода.
Здесь нагреватель нагревает катод, что приводит
к излучению электронов. Если сетка не отбрасывает их назад, они приземляются на анод. Можно
мысленно соотнести сетку с ручкой переключателя.

Катодный нагреватель

Рис. 2.24. Условное
обозначение триода

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

Транзисторы
В наши дни балом правят транзисторы. Транзистор (от transfer resistor — перенос
сопротивления) похож на вакуумную лампу, но использует особый тип материала,
называемый полупроводником, который может быть как проводником, так и изолятором. Именно это свойство позволило создавать клапаны для электричества, не
требующие наличия нагревателя и не имеющие движущихся частей. Но, конечно,
транзисторы несовершенны. Их можно сделать очень маленькими, и это хорошо,
но тонкие проводники имеют большее сопротивление, из-за чего выделяется
тепло. Избавиться от тепла в транзисторе — настоящая проблема, потому что
полупроводники легко плавятся.
Все об устройстве транзисторов знать не обязательно. Важно знать, что транзистор сделан на подложке или пластине из полупроводникового материала,
обычно кремния. В отличие от других технологий, таких как шестерни, клапаны,
реле и вакуумные лампы, транзисторы не производятся индивидуально. Они
создаются с помощью процесса под названием «фотолитография» — проецирования изображения транзистора на кремниевую пластину и затем его проявления. Этот процесс подходит для массового производства, потому что большое
количество транзисторов можно спроецировать на одну кремниевую подложку,
проявить, а затем разделить на отдельные компоненты.
Существует много различных типов транзисторов, из них выделяют два основных — биполярный (bipolar junction transistor, BJT) и полевой (field effect

Создание аппаратного обеспечения, работающего с битами   95
transistor, FET). Процесс производства включает в себя добавление примеси,
при котором материал подложки пропитывается неприятными химическими
веществами, такими как мышьяк, для изменения его характеристик. Примеси
создают области материала p- и n-типа. Конструкция транзистора предполагает
изготовление бутербродов из p- и n-типов. На рис. 2.25 показаны условные обозначения для некоторых типов транзисторов.
Коллектор

База

База

Эмиттер
Биполярный NPN

Эмиттер

Затвор

Коллектор
Биполярный PNP

Исток

Сток

Затвор

Сток

Исток
N-канальный
МОП-транзистор

P-канальный
МОП-транзистор

Рис. 2.25. Условные обозначения различных типов транзисторов
Термины NPN, PNP, N-канальный и P-канальный относятся к многослойным
конструкциям. Можно представить транзистор как клапан или переключатель,
а затвор (или базу) — как ручку. Электрический ток течет сверху вниз, когда
ручка поднимается, подобно тому как катушка в реле перемещает контакты.
Но в отличие от переключателей и клапанов, которые мы рассматривали до сих
пор, в биполярных транзисторах ток может течь только в одном направлении.
Видно, что между затвором и остальной частью транзистора в обозначениях полевых транзисторов есть зазор. Он символизирует то, что полевые транзисторы
работают при помощи электрического поля (отсюда и название — полевой);
это все равно, что использовать статическое прилипание (склонность легких
предметов прилипать к другим предметам из-за статического электричества)
для перемещения переключателя.
Полевой транзистор со структурой «металл-оксид-полупроводник» (полевой
МОП-транзистор), или MOSFET (metal-oxide semiconductor field effect transistor),
представляет собой разновидность полевого транзистора, очень часто используемую в современных компьютерных микросхемах благодаря низкому энергопотреб­
лению. Варианты с N- и P-каналами часто используются в дополнительных парах,
отсюда и происходит термин CMOS (complementary metal oxide semiconductor) —
комплементарная структура металл-оксид-полупроводник (КМОП).

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

96   Глава 2. Комбинаторная логика
вроде схемы для реализации логической операции И, по-прежнему требовало
множества компонентов.
Ситуация изменилась в 1958 году, когда Джек Килби (Jack Kilby) (1923–2005),
американский электротехник, и Роберт Нойс (Robert Noyce) (1927–1990), американский математик, физик и соучредитель Fairchild Semiconductor и Intel, изобрели интегральную схему. Благодаря интегральным схемам создание сложных
систем становится не дороже сборки одного транзистора. Интегральные схемы
стали называть микросхемами из-за их внешнего вида.
Как вы, наверное, заметили, многие из схем одного типа можно построить с использованием реле, вакуумных ламп, транзисторов или интегральных схем.
И с каждой новой технологией эти схемы становились меньше, дешевле и энергоэффективнее. В следующем разделе рассказывается об интегральных схемах,
разработанных для комбинаторной логики.

Логические вентили
В середине 1960-х годов работодатель Джека Килби, компания Texas Instruments,
представила семейства интегральных схем 5400 и 7400. Эти микросхемы содержали готовые схемы, выполняющие логические операции. Эти конкретные
схемы, называемые логическими вентилями, или просто вентилями, являются
аппаратными реализациями логических операций, которые мы называем комбинаторной логикой. Компания Texas Instruments продала миллиарды таких
схем. Они доступны и сегодня.
Логические вентили очень пригодились разработчикам аппаратного обеспечения:
им больше не приходилось делать все с нуля, и они могли создавать сложные логические схемы так же просто, как собирать многосоставные сантехнические системы. Аналогично тому, как сантехники находят корзины с тройниками, коленами
и соединительными муфтами в хозяйственном магазине, разработчики логики находят «корзины» с вентилями И, ИЛИ, исключающего ИЛИ и инверторов (штук,
выполняющих операцию НЕ). На рис. 2.26 показаны обозначения этих вентилей.
Как и следовало ожидать, выход Y логического элемента И истинен, если оба
параметра на входах A и B также истинны. (Информация о работе других вентилей представлена в таблицах истинности на рис. 1.1.)
A

A

A

Вентиль И

Y

B

B

B

Y A

Y

Y

Вентиль ИЛИ

Вентиль исключающего ИЛИ

Рис. 2.26. Обозначения вентилей

Инвертор

Логические вентили   97
Ключевой частью условного обозначения инвертора на рис. 2.26 является
○ (круг), а не тре­угольник, к которому он прикреплен. Треугольник без круга
называется буфером, и он просто передает свой вход на выход. Обозначение
инвертора в основном применяется только там, где инвертор не используется
в сочетании с чем-либо еще.
Создание логических элементов И и ИЛИ с использованием технологии транзисторно-транзисторной логики (transistor-transistor logic, TTL) компонентов
серий 5400 и 7400 неэффективно, потому что выходной сигнал простой схемы вентиля по умолчанию уже инвертирован, и для его правильного вывода
требуется лишний инвертор. Это сделало бы их более дорогими, медленными
и энергоемкими. Итак, в качестве основных вентилей использовались схемы
И-НЕ и ИЛИ-НЕ с инвертирующим кругом, как на рис. 2.27.
A

A
Y

Y
B

B
Вентиль И-НЕ

Вентиль ИЛИ-НЕ

Рис. 2.27. Вентили И-НЕ и ИЛИ-НЕ
К счастью, эта дополнительная инверсия не влияет на нашу способность разрабатывать логические схемы благодаря закону де Моргана. Он применяется
на рис. 2.28, чтобы показать, что логический элемент И-НЕ эквивалентен логическому элементу ИЛИ с инвертированными входами.

Рис. 2.28. Перерисовка логического элемента И-НЕ с использованием
закона де Моргана
Все рассмотренные ранее вентили имели два входа, не считая инвертора, но на самом деле у вентилей может быть более двух входов. Например, вентиль И с тремя входами будет иметь на выходе истину, если все три входа истинны. Теперь,
когда вы знаете, как работают вентили, рассмотрим некоторые сложности,
которые возникают при их использовании.

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

98   Глава 2. Комбинаторная логика
что логические сигналы мгновенно переходят из 0 в 1 и наоборот. В большинстве случаев это верно, особенно когда мы соединяем вентили друг с другом.
Но в реальном мире сигналы меняются гораздо медленнее.
Посмотрим, что произойдет при медленно меняющемся сигнале. На рис. 2.29
показаны два сигнала, которые постепенно переходят из 0 в 1.
Выход

Выход

1

0

1

0

Зашумленный вход

Тихий вход
0

0

1

1

Рис. 2.29. Сбой из-за шума
Вход слева тихий и не имеет шума, а к сигналу справа добавлен некоторый шум.
Видно, что зашумленный сигнал вызывает сбой на выходе, потому что шум заставляет сигнал пересекать пороговое значение более одного раза.
Этого можно избежать, используя гистерезис, в котором критерий принятия решения зависит от истории. Как показано на
рис. 2.30, передаточная функция не симметрична. Существуют разные передаточные
функции для сигналов нарастания (из 0 в 1)
и спада (из 1 в 0), как показано стрелками.
Когда выход равен 0, применяется кривая
справа, и наоборот.

0
Порог падения

1
Порог подъема

Рис. 2.30. Передаточная
В результате мы получаем два разных порога:
функция гистерезиса
один для нарастающих сигналов и один для
ниспадающих. Это означает, что, когда сигнал пересекает одно из пороговых
значений, ему нужно пройти намного дальше, прежде чем он пересечет другое,
благодаря чему получается более высокая помехоустойчивость.
Существуют специальные вентили с гистерезисом. Их
называют триггерами Шмитта в честь американского
ученого Отто Х. Шмитта (Otto H. Schmitt) (1913–1998),
который изобрел данную схему. Поскольку они сложнее
и дороже обычных вентилей, их используют только там, где
они действительно нужны. Гистерезис добавляется к условному обозначению, как показано для инвертора на рис. 2.31.

Рис. 2.31. Условное
обозначение
триггера Шмитта

Логические вентили   99

Дифференциальная передача сигналов
Иногда шума бывает так много, что даже гистерезиса недостаточно. Представьте
прогулку по тротуару. Назовем правый край тротуара положительным порогом,
а левый — отрицательным. Вы думаете о своем, когда кто-то, толкая коляску для
двойни, сбивает вас с правого края тротуара, а затем группа бегунов оттесняет
вас слева. В этом случае нам понадобится защита.
До сих пор мы измеряли сигнал относительно абсолютного порога или пары
порогов в случае триггера Шмитта. Но бывают ситуации, когда шум настолько
велик, что оба порога триггера Шмитта пересекаются, что делает его неэффективным.
Добавим в систему друзей. Представьте, что вы идете по тротуару с другом.
Если ваш друг находится слева от вас, назовем его 0; если справа — 1. Теперь
коляска и бегуны оттолкнут в сторону не только вас, но и вашего друга. Но вы
не сменили позицию, так что шум в итоге не повлиял на вас. Конечно, если вы
просто гуляете рядом, можно столкнуть с тротуара только одного из вас. Вот
почему лучше держаться за руки или обнимать друг друга за талию. Да, больше
объятий — больше помехозащищенности! Это называется дифференциальной
передачей сигналов, потому что мы измеряем разницу между парой комплементарных сигналов. На рис. 2.32 показана схема дифференциальной передачи
сигналов.

Формирователь
сигнала

Приемник

Рис. 2.32. Дифференциальная передача сигналов
Видим, что существует формирователь, который преобразует входной сигнал
в комплементарную пару, и приемник, который преобразует комплементарную
пару обратно в несимметричный выход. Обычно приемник включает триггер
Шмитта для дополнительной помехоустойчивости.
Конечно, существуют ограничения. Слишком сильный шум может вытолкнуть
электронные компоненты за пределы их указанного рабочего диапазона — представьте, что рядом с тротуаром есть здание и вас с другом вдавливают в стену.
Коэффициент подавления синфазного сигнала (common-mode rejection ratio,
CMRR) является частью спецификации компонента и указывает количество
шума, с которым компонент может справиться. Сигнал называется «синфазным», потому что это название относится именно к шуму, который является
общим для обоих сигналов в паре.

100   Глава 2. Комбинаторная логика
Дифференциальная передача сигналов используется во многих местах, например
на телефонных линиях. Такое применение стало необходимым в 1880-х годах,
когда появились электрические трамваи, поскольку они генерировали много
электрических шумов, мешающих телефонным сигналам. Шотландский изобретатель Александр Грэйам Белл (1847–1922) изобрел кабельную витую пару,
в которой пары проводов были скручены вместе для электрического эквивалента
объятий (рис. 2.33).
Он также запатентовал телефон. Сегодня витая пара распространена повсеместно; вы найдете ее в кабелях USB, SATA (для подключения жестких дисков)
и Ethernet.

Оболочка
Проводник

Изолятор

Рис. 2.33. Кабель Ethernet на основе витой пары
Интересное применение дифференциальной передачи сигналов можно найти
в концертной аудиосистеме Wall of Sound, используемой американской группой The Grateful Dead (1965–1995). Таким образом была решена проблема
обратной связи в микрофоне с помощью попарно соединенных микрофонов —
выходной сигнал одного микрофона вычитался из выхода другого. Любой звук,
попадавший в оба микрофона, был синфазным и подавлялся. Вокалисты пели
в один из микрофонов в паре, чтобы звучал только их голос. Искажение этой
системы, которое можно заметить, прослушав живые записи группы, состоит
в том, что шум публики получается каким-то металлическим. Это потому, что
низкочастотные звуки имеют большую длину волны, чем высокочастотные;
низкочастотный шум с большей вероятностью будет синфазным, чем высокочастотный.

Задержка распространения
Я упомянул задержку распространения в разделе «Электрический ток на примере сантехники» на с. 83. Задержка распространения — это время, необходимое

Логические вентили   101
для того, чтобы изменение входа отразилось на выходе. Это статистическая
мера, которая нужна в связи с различиями в производственных процессах и температуре, а также количествах и типах компонентов, подключенных к выходу
вентиля. Вентили имеют как минимальную, так и максимальную задержку;
фактическая задержка находится где-то между этими значениями. Задержка
распространения — один из факторов, ограничивающих максимальную скорость,
которая может быть достигнута в логических схемах. Разработчики должны
использовать наихудшие значения, чтобы их схемы работали. Это означает,
что схемы должны проектироваться с учетом как самых коротких, так и самых
длительных задержек.
На рис. 2.34 серые области показывают места, где нельзя полагаться на выходные
данные из-за задержки распространения.
A
B
A
B

D
C

C
D

1
0
1
0
1
0
1
0

Рис. 2.34. Пример задержки распространения
Выходные данные могут измениться уже у левого края серых областей, но нет
гарантий, что они изменятся до правого края. Длина серых областей будет увеличиваться по мере объединения все большего количества вентилей.
Существует множество диапазонов времени задержки распространения, которые зависят от технологического процесса. Отдельные компоненты, такие
как микросхемы серии 7400, могут иметь задержки в диапазоне 10 наносекунд
(то есть 10 миллиардных долей секунды). Задержки логического вентиля внутри
современных крупных компонентов, таких как микропроцессоры, могут составлять пикосекунды (триллионные доли секунды). Если вы читаете специ­фикации
компонентов, задержки распространения обычно указываются как tPLH и tPHL для
времени распространения от низкого к высокому (low to high) и от высокого
к низкому уровню (high to low) соответственно.
Обсудив входы и то, что происходит на пути к выходам, перейдем непосредственно к выходам.

Варианты выходов
Мы уже говорили о входах вентиля, но практически не упоминали выходы. Существует несколько разных типов выходов, использующихся в разных ситуациях.

102   Глава 2. Комбинаторная логика

Каскадный выход
Обычный выход вентиля называется каскадным, потому что транзисторы, установленные один поверх другого, напоминают каскад. Мы можем смоделировать
этот тип выхода с помощью переключателей, как показано на рис. 2.35.
1

1

1

Выход

Выход (0)

0

Выход (1)

0

0

Рис. 2.35. Каскадный выход
Схема слева показывает, как каскадные выходы получили свое название. Верхний переключатель на рисунке называется активным подтягивающим, потому
что он подключает выход к высокому логическому уровню, чтобы в результате
получить 1. Каскадные выходы нельзя соединять вместе. Как видно на рис. 2.35,
если соединить выход 0 с выходом 1, будут объединены положительный и отрицательный источники питания, что так же плохо, как скрещивание потоков
в фильме «Охотники за привидениями» 1984 года, — это приведет к расплавлению компонентов.

Выход с открытым коллектором
Другой тип выхода называется выходом с открытым коллектором или открытым стоком в зависимости от типа используемого транзистора. Схема и модель
переключателя для этого выхода показаны на рис. 2.36.
Выход

0

Выход (0)

0

0

Рис. 2.36. Выход с открытым коллектором/стоком

Выход (?)

Логические вентили   103
Такой подход кажется странным на первый взгляд. Все хорошо, если мы хотим
получить результат 0, но в другом случае выход просто плавает, поэтому мы не
знаем, каково его значение.
Поскольку версии с открытым коллектором и открытым стоком не имеют активных подтягиваний, можно без вреда соединить их выходы. Можно использовать пассивный подтягивающий резиV
стор, который представляет собой просто
подтягивающий резистор, соединяющий
выход с питающим напряжением, котоРезистор
рое является источником единиц. Это
Выходы
называется VCC для биполярных трансоткрытым коллектором
зисторов и VDD для МОП-транзисторов.
Пассивное подтягивание создает эффект
монтажного И, как показано на рис. 2.37.
CC

Здесь происходит следующее: когда ни
на одном из выходов с открытым коллектором не установлен низкий логический
Рис. 2.37. Монтажное И
уровень, резистор устанавливает выход
в 1. Резистор ограничивает силу тока, чтобы предотвратить перегрев компонетов схемы. Выход устанавливается в 0, когда на любом из выходов с открытым
коллектором присутствует логический 0. Таким образом можно соединить много
элементов, не используя логический вентиль И с большим количеством входов.
Другое использование выходов с открытым коллектором и открытым стоком —
управление устройствами, такими как светодиоды (light-emitting diode, LED).
Устройства с открытым коллектором и открытым стоком часто предназначены
для поддержки этого использования и могут выдерживать более высокий ток,
чем устройства с каскадным выходом. Некоторые версии позволяют подтянуть выход до уровня напряжения, превышающего уровень логической 1, что
позволяет взаимодействовать с другими типами схем. Это важно, потому что,
несмотря на согласованность порогов внутри семейства логических элементов,
например в серии 7400, пороги у других семейств различаются.

Выход с тремя состояниями
Хотя схемы с открытым коллектором позволяют соединять выходы вместе, они
не так быстры, как схемы с активным подтягиванием. Итак, отойдем от решения
с двумя состояниями и представим выходы с тремя состояниями. Третье состоя­
ние — это состояние, когда выход выключен. Добавлен дополнительный вход
включения, который включает и выключает выход, как показано на рис. 2.38.
Выключенное состояние известно как состояние hi-Z, или состояние с высоким
сопротивлением (импедансом). Z — это символ импеданса, комплексного числа,
обозначающего электрическое сопротивление цепи. Можно представить выход

104   Глава 2. Комбинаторная логика
Выход

Вход

Выход

Вход
Включение

Включение

Рис. 2.38. Выход с тремя состояниями
с тремя состояниями в виде схемы, показанной на рис. 2.35. Управление вентилями по отдельности дает четыре комбинации: 0, 1, hi-Z и «выход из строя».
Очевидно, что разработчики схем должны убедиться, что комбинация, при
которой схема выйдет из строя, не может быть выбрана.
Выходы с тремя состояниями позволяют соединять вместе большое количество
устройств. Важно помнить, что одновременно может быть включено только
одно устройство.

Создание более сложных схем
Введение вентилей значительно упростило процесс проектирования аппаратного
обеспечения. Мы избавились от необходимости разрабатывать всё из отдельных
компонентов. Например, если для создания логического элемента И-НЕ с двумя
входами требовалось около 10 электронных компонентов, то компонент из серии
7400 включал четыре из них в одном корпусе, что называлось мелкомасштабной
интеграцией (small-scale integration, SSI), так что один корпус микросхемы мог
заменить 40 электронных компонентов.
Разработчики аппаратного обеспечения могли построить что угодно из интегрированных логических вентилей, так же как и в случае с дискретными
компонентами. Это делало вещи дешевле и компактнее. И поскольку определенные комбинации вентилей используются часто, были введены компоненты
интеграции среднего масштаба (medium-scale integration, MSI), включающие
эти комбинации, что дополнительно снизило количество необходимых частей.
Позже появились крупномасштабная интеграция (large-scale integration, LSI),
сверхбольшая степень интеграции (very large-scale integration, VLSI) и т. д.
Мы рассмотрим некоторые комбинации вентилей в следующих разделах, но и это
еще не все. Мы используем эти функциональные строительные блоки высокого
уровня для создания компонентов еще более высокого уровня, подобно тому как
сложные компьютерные программы конструируются из более мелких программ.

Создание сумматора
Построим двоичный сумматор дополнительного кода. Возможно, вам никогда
не придется разрабатывать что-то подобное, но этот пример демонстрирует, как
умное управление логикой повышает производительность — что верно как для
аппаратного, так и для программного обес­печения.

Создание более сложных схем   105
В главе 1 мы видели, что сумма двух битов представляет собой их исключающее ИЛИ, а перенос
в старший разряд — И этих битов. На рис. 2.39 показана реализация вентиля.

A

B

Перенос

Сумма

Видим, что логический элемент исключающего ИЛИ
Рис. 2.39. Полусумматор
получает сумму, а вентиль И — перенос. На рис. 2.39
представлен всего лишь полусумматор, потому что
чего-то не хватает. Можно добавить два бита, но нужен третий вход, чтобы можно
было осуществлять перенос. То есть чтобы получить сумму для каждого бита, необходимы два сумматора. Мы делаем перенос, когда по крайней мере два входа
равны 1. Таблица 2.1 содержит таблицу истинности для полного сумматора.
Таблица 2.1. Таблица истинности для полного сумматора
A

B

C

Сумма

Перенос

0

0

0

0

0

0

0

1

1

0

0

1

0

1

0

0

1

1

0

1

1

0

0

1

0

1

0

1

0

1

1

1

0

0

1

1

1

1

1

1

Построить полный сумматор немного сложнее (рис. 2.40).
A
B
C

Сумма

Перенос

Рис. 2.40. Полный сумматор
Как видите, для него нужно гораздо больше логических вентилей. Но теперь,
создав полный сумматор, мы можем использовать его, чтобы создать сумматор
для более чем одного бита. На рис. 2.41 показана конструкция под названием
«каскадный сумматор».

106   Глава 2. Комбинаторная логика

0
B0
A0

C
B
A

B1
A1

C
B
A

B2
A2

C
B
A

Сумма

S0

Перенос

Сумма

S1

Перенос

Сумма

S2

Перенос
...

Рис. 2.41. Каскадный сумматор
Каскадный сумматор, или сумматор со сквозным переносом, получил свое название
по тому, как перенос переходит от одного бита к другому. Процесс похож на волну.
Пока он нормален, но можно заметить, что на один бит приходится две задержки
затвора и их число быстро растет, если создавать 32- или 64-битный сумматор.
Эти задержки можно устранить с помощью сумматора с упреждающим переносом.
Несколько базовых арифметических операций помогут понять, как это сделать.
На рис. 2.40 видно, что перенос выхода полного сумматора для бита i передается
как входной перенос в бит i + 1:
Ci+1 = (Ai И Bi) ИЛИ (Ai И Сi) ИЛИ (Bi И Ci).
Камнем преткновения здесь является то, что нам нужно знать Ci, чтобы получить Ci+1, что приводит к каскадной зависимости. Это видно из следующего
уравнения для Ci+2:
Ci+2 = (Ai+1 И Bi+1) ИЛИ (Ai+1 И Сi+1) ИЛИ (Bi+1 И Ci+1).
Можно устранить эту зависимость, подставив первое уравнение во второе, как
показано ниже:
Ci+2 = (Ai+1 И Bi+1)
ИЛИ (Ai+1 И ((Ai И Bi) ИЛИ (Ai И Ci) ИЛИ (Bi И Ci))).
ИЛИ (Bi+1 И ((Ai И Bi) ИЛИ (Ai И Ci) ИЛИ (Bi И Ci))).
Обратите внимание, что, хотя здесь намного больше операторов И и ИЛИ, задержка распространения по-прежнему зависит лишь от двух вентилей. Cn зависит только от входов A и B, поэтому время переноса и, следовательно, время

Создание более сложных схем   107
суммирования не зависят от количества битов. Cn всегда можно сгенерировать из
Cn–1, который использует все большее количество вентилей по мере увеличения
n. Хотя логические вентили недороги, они потребляют электроэнергию, поэтому
нужно учитывать компромисс между скоростью и потребляемой мощностью.

Построение дешифраторов
В разделе «Представление целых чисел с помощью
битов» на с. 43 мы создали, или зашифровали, числа
из битов. Дешифратор делает обратное, снова превращая зашифрованное число в набор отдельных
битов. Одно из применений дешифраторов — управление дисплеями. Возможно, вы видели газоразрядные индикаторы (показанные на рис. 2.42) в старых
научно-фантастических фильмах; это и в самом деле
классный ретродисплей для чисел. По сути, это набор неоновых вывесок, по одной на каждую цифру.
Каждый светящийся провод имеет собственное соединение, поэтому необходимо преобразовать 4-битное число в 10 отдельных выходов.
Вспомним, что восьмеричное представление принимает восемь разных значений и кодирует их в 3 бита.
На рис. 2.43 показан дешифратор 3 : 8, который преобразует восьмеричное значение обратно в набор
отдельных битов.

Рис. 2.42. Газоразрядный
индикатор

Если вход равен 000, вход Y0 истинен; когда на входе 001, Y1 истинен; и т. д. Дешифраторы в основном называются по количеству входов и выходов. Пример на
рис. 2.43 имеет три входа и восемь выходов, так что это дешифратор 3 : 8. Обычно
подобные дешифраторы изображаются, как показано на рис. 2.44.
S0

Y1

S1
...
S2

Y0

Y0

Y1
S0
S1

Y7

S2

Y2
Y3
Y4
Y5
Y6
Y7

Рис. 2.43. Дешифратор 3 : 8

Рис. 2.44. Условное обозначение
дешифратора 3 : 8

108   Глава 2. Комбинаторная логика

Построениедемультиплексоров
Дешифратор можно использовать для создания демультиплексора, обычно обозначаемого как dmux (от demultiplexer), который позволяет направлять входной
сигнал на один из нескольких выходов (как если бы вы распределяли студентов
Хогвартса по факультетам). Демультиплексор включает в себя дешифратор с дополнительными логическими вентилями, как показано на рис. 2.45.
D

Y0

Y1

Y0
S0

S0

S1

S1

Y2

Y1
Y2

Y3

Y3

Рис. 2.45. Демультиплексор 1 :  4
Демультиплексор направляет входной сигнал D на один из четырех выходов
Y0–3 на основе входов дешифратора S0–1. Обозначение на рис. 2.46 используется
в схемах для обозначения демультиплексора.

Y0
Y1

D

Y2
S0

S1

Y3

Рис. 2.46. Условное обозначение демультиплексора

Построение селекторов
Выбор одного входа из ряда входов — еще одна часто выполняемая функция.
Например, у нас может быть несколько источников операндов для сумматора,
и нужно выбрать только один. Используя вентили, можно создать еще один

Создание более сложных схем   109
функциональный блок, называемый селектором или мультиплексором (mux,
от multiplexer).
Селектор объединяет дешифратор с дополнительными вентилями, как показано
на рис. 2.47.
D3

D2
Y
D1

D0
Y0
S0

S0

S1

S1

Y1
Y2
Y3

Рис. 2.47. Селектор 4  : 1
Селекторы также часто используются и имеют свое условное обозначение.
На рис. 2.48 показано обозначение селектора 4 : 1, которое в значительной степени является обратным обозначением дешифратора.

D0
D1

Y

D2
D3 S
0

S1

Рис. 2.48. Условное обозначение селектора 4  : 1
Вы, наверное, уже знакомы с селекторами, но не знаете об этом. Возможно,
у вас есть тостер со шкалой с обозначениями «Выкл.», «Разогрев», «Выпечка»
и «Жарка». Это и есть селекторный переключатель с четырьмя положениями.
В тостере есть два нагревательных элемента: один сверху, а другой снизу. Логика
работы тостера показана в табл. 2.2.

110   Глава 2. Комбинаторная логика
Таблица 2.2. Логика работы тостера
Настройка

Верхний элемент

Нижний элемент

Выкл.

Выкл.

Выкл.

Выпечка

Выкл.

Вкл.

Разогрев

Вкл.

Вкл.

Жарка

Вкл.

Выкл.

Можно реализовать эту логику, используя пару селекторов 4 : 1, объединенных,
как показано на рис. 2.49.
Выкл. 0
Выпечка 0

Кверхнему нагревательному элементу

Разогрев 1
Жарка 1

0
1

Книжнему нагревательному элементу

1
0

Рис. 2.49. Селекторный переключатель тостера

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

3

Последовательная логика

Комбинаторная логика, о которой вы узнали в предыдущей главе, «плывет по течению». Другими словами,
выходные данные изменяются в зависимости от входных.
Но невозможно создавать компьютеры только на основе
комбинаторной логики, потому что она не предполагает
возможности удалить что-то из потока и запомнить это. Например, вы не сможете сложить все числа от 1 до 100, если не будете
отслеживать, в каком месте последовательности находитесь.
В этой главе вы узнаете о последовательной логике. Термин происходит от слова
«последовательность», что означает «одно следует за другим во времени». Будучи
человеком, вы интуитивно понимаете время и счет на пальцах, но это не значит, что
время является естественным для цифровых схем. Мы должны как-то создать его.
Комбинаторная логика имеет дело только с текущим состоянием входных
данных. А вот последовательная логика учитывает как текущее состояние, так
и прошлое. В этой главе вы узнаете о методиках создания времени и методиках
запоминания того, что было в прошлом. Мы проследим некоторые технологии,
которые использовались для этих целей, от их зарождения до наших дней.

Представление времени
Люди измеряют время с помощью некой периодической функции — вращения Земли. Мы называем один полный оборот сутками и делим его на более
мелкие единицы — часы, минуты и секунды. Секунду можно определить как
1/86 400 оборота Земли, поскольку в сутках 86 400 секунд.

112   Глава 3. Последовательная логика
В дополнение к использованию внешнего события, такого как вращение Земли,
можно генерировать собственные периодические функции, применяя определенные понятия физики — например, время, необходимое для одного движения
маятника. Именно благодаря этой технике старые дедовские часы издавали звук
«тик-так». Конечно, чтобы маятник был полезен, его необходимо откалибровать
по измеренной длине секунды.
Работая с компьютерами, мы используем электронику, поэтому нам нужен
периодический электрический сигнал. Можно создать что-то подобное, поместив переключатель так, чтобы маятник ударял по нему. Но, если только вы не
фанат стимпанка, вам, вероятно, не нужен компьютер с маятниковым питанием.
В следующем разделе мы узнаем о более современных подходах.

Осцилляторы
Посмотрим, что можно сделать с инвертором: можно подключить его выход ко входу, как показано
на рис. 3.1.

1
0

Это приведет к появлению обратной связи, такой
Время
же, как если поставить микрофон слишком близко
Рис. 3.1. Осциллятор
к громкоговорителю. Выходной сигнал инвертора
качается взад-вперед или колеблется между 0 и 1.
Скорость, с которой он колеблется, является функцией задержки распространения (см. раздел «Задержка распространения» на с. 100), которая имеет тенденцию меняться в зависимости от температуры. Полезно иметь генератор со
стабильной частотой, чтобы генерировать точную привязку ко времени.
Экономически эффективный способ сделать это — использовать кристалл.
Да, очень современно. Кристаллы, как и магниты, связаны с электричеством.
Если прикрепить электроды (провода) к кристаллу и сжать его, он будет
генерировать электрический ток. И если пустить ток по этим проводам,
кристалл изогнется. Это называется пьезоэлектрическим эффектом, он был
открыт братьями Полем-Жаком (1855–1941) и Пьером (1859–1906) Кюри
в конце XIX века. Пьезоэлектрический эффект имеет множество применений.
Кристалл может улавливать звуковые колебания, превращаясь в микрофон.
Звуковые колебания, возникающие при подаче электричества на кристаллы,
вызывают раздражающие звуковые сигналы, которые можно услышать от
многих приборов. На принципиальных электрических схемах кристалл обозначается символом, показанным на рис. 3.2.

Рис. 3.2. Условное обозначение кристалла

Представление времени   113
Кварцевый осциллятор попеременно подает напряжение на кристалл и принимает его обратно с помощью электронных однополюсных переключателей
с группой контактов на переключение. Время, необходимое кристаллу для
этого действия, предсказуемо и очень точно. Кристаллический материал, лучше
всего подходящий для такой цели, — кварц. Вот почему так часто можно видеть
рекламу точных кварцевых часов. В следующий раз, обратив внимание на цену
новых модных часов, вспомните, что действительно хороший кристалл продается всего за 25 центов.

Генераторы тактовых сигналов
Как вы убедились, осцилляторы позволяют измерять время. Очевидно, что компьютерам требуется точно отсчитывать его, например, чтобы воспроизводить
видео с постоянной скоростью. Но на это есть еще одна причина более низкого
уровня. В главе 2 мы обсудили, как задержка распространения влияет на время,
необходимое схемам для выполнения действий. Время дает возможность дождаться, например, наихудшей задержки в сумматоре, прежде чем изучать полученный результат — чтобы удостовериться, что он стабильный и правильный.
Осцилляторы обеспечивают в компьютерах тактовые сигналы. Компьютерные
тактовые сигналы подобны барабанщику в оркестре; они задают темп схемотехнике. Максимальная тактовая частота или самый быстрый темп определяется
задержками распространения.
Производство компонентов связано с большим количеством статистических данных, потому что от детали к детали появляются большие различия. В процессе
сортировки компоненты помещаются в разные ячейки или стопки в зависимости
от их измеренных характеристик. Самые быстрые части по самой высокой цене
пойдут в одну ячейку; более медленные и менее дорогие — в другую и т. д. Нецелесообразно иметь бесконечное количество ячеек, поэтому элементы в ячейке
могут немного отличаться друг от друга, хотя эта разница меньше, чем различия
между деталями во всей партии. Это одна из причин, по которой задержки распространения указываются в виде диапазона; производители предоставляют
минимальные и максимальные значения в дополнение к типовому значению.
Распространенной ошибкой проектирования логической схемы является использование типовых значений вместо минимумов и максимумов. Люди, разгоняющие свои компьютеры, делают ставку на то, что их деталь статистически
находится в середине ячейки — то есть ее тактовый сигнал может быть увеличен
на некоторую величину, и это не приведет к поломке детали.

Триггеры-защелки
Разобравшись с источниками времени, попробуем запомнить хотя бы небольшое количество информации. Это можно сделать с помощью обратной связи,

114   Глава 3. Последовательная логика
например, привязав выход логического элемента ИЛИ ко входу, как показано на
рис. 3.3. При этом не создается осциллятор, подобный тому, который мы видели
на рис. 3.1, поскольку здесь нет инверсии. Предположим, что выход начинается
с 0 в схеме на рис. 3.3. Теперь, если на входе мы имеем 1, выход тоже будет равен 1,
и, поскольку он подключен к другому входу, он останется одинаковым, даже если
поменять вход на 0. Другими словами, выход запоминает данные.

выход
вход

вход
выход

Рис. 3.3. Триггер-защелка логического элемента ИЛИ
Конечно, эта схема требует некоторой доработки, потому что невозможно повторно вычислить значение 0 для выхода. Нам нужен способ сбросить значение,
отключив обратную связь, как показано на рис. 3.4.

установка
сброс

сброс

выход
установка

сброс
выход

Рис. 3.4. Триггер-защелка логического элемента И-ИЛИ
Обратите внимание, что мы обозначили выход инвертора как сброс. Линия
поверх символа на аппаратном языке означает «противоположность». То есть
что-то истинно, если равно 0, и ложно, если равно 1. Иногда это называют активным низким уровнем, а не активным высоким уровнем, что означает выполнение
логики при 0, а не 1. Линия произносится как «черта», поэтому в устной речи
сигнал будет называться «сброс с чертой».
Когда сброс находится на низком уровне, сброс, наоборот, находится на высоком,
поэтому выход логического элемента ИЛИ возвращается на вход. Когда сброс
переходит на высокий уровень, сброс оказывается на низком, нарушая эту обратную связь, так что выход становится равным 0.
На рис. 3.5 показан RS-триггер — чуть более умный способ создания бита памяти.
RS означает установка-сброс (set-reset). Он имеет активные входы низкого уровня и дополнительные выходы, это означает, что один из них активен на низком
уровне, а другой — на высоком. Можно создать версию с активными высокими
входами, используя вентили ИЛИ-НЕ, но они часто более энергоемкие, чем
вентили И-НЕ, и собирать их сложнее и дороже.

Представление времени   115

установка
S

сброс

R

Q

Q

установка

сброс

Q

Q

0

0

1

1

0

1

1

0

1

0

0

1

1

1

память

память

Рис. 3.5. RS-триггер
Ситуация, когда активны и установка, и сброс, обычно не возникает, потому что
оба выхода будут истинными. Кроме того, если оба входа становятся неактивными (то есть переходят с 0 на 1) одновременно, состояние выходов невозможно
предсказать, поскольку оно зависит от задержек распространения.
Схема на рис. 3.5 имеет приятное свойство, которого нет в схеме на рис. 3.4, — ее
конструкция симметрична. Это означает, что задержки распространения одинаковы как для сигналов установки, так и для сигналов сброса.

Синхронный RS-триггер
Получив новый способ запоминания
информации, посмотрим, что нужно,
чтобы запомнить что-то в определенный момент времени. В схеме на
рис. 3.6 ко входам добавлена дополнительная пара логических вентилей.
Как видно, когда вход синхронизации (синхр.) неактивен (высокий),
не имеет значения, что происходит
при установке и сбросе; выходы не
изменятся, потому что оба входа для
вентилей S и R будут равны 1.

установка
S

Q

R

Q

синхр.

сброс

Рис. 3.6. Синхронный RS-триггер

Поскольку необходимо запомнить один бит информации, можно улучшить
схему следующим образом — добавить инвертор между входами установки
и сброса, так что останется один вход данных, который мы будем обозначать
как D. Это изменение показано на рис. 3.7.
Теперь, если D равен 1, когда синхр. низкий, выход Q будет установлен как 1.
Аналогично, если D равен 0, когда синхр. низкий, Q будет установлен как 0. Изменения на D при высоком синхр. не имеют никакого эффекта. Это означает, что
можно помнить состояние D. То же самое показано на временной диаграмме,
изображенной на рис. 3.8.

116   Глава 3. Последовательная логика
установка
D
S

Q

R

Q

синхр.

сброс

Рис. 3.7. Инвертирующий D-триггер
D
синхр.
Q
Q D D игнорируется

Q D

D игнорируется

Q D

D игнорируется

Q D

Рис. 3.8. Временная диаграмма инвертирующего D-триггера
Проблема с этой схемой заключается в том, что изменения D проходят всякий
раз при низком синхр., как видно в выделенной серым цветом части. Поэтому
приходится рассчитывать, что D будет «вести себя хорошо» и не изменится
при «открытии» «входа синхронизации». Возможность мгновенно открывать
вентиль предпочтительнее. Рассмотрим, как это сделать, в следующем разделе.

Триггеры
Как мы обсуждали в разделе выше, нужно свести к минимуму вероятность получения неверных результатов из-за изменения данных. Обычно это делается
с помощью захвата данных во время перехода между логическими уровнями,
а не на самом логическом уровне, который имеет определенное значение. Эти
переходы называются фронтами. Фронт можно рассматривать как критерий выбора для времени. Вернувшись к рис. 3.8, видим, что переход между логическими
уровнями происходит почти мгновенно. Такие типы электронных устройств
с запуском по фронту называются триггерами.
RS-триггеры — это строительные блоки, из которых собираются другие типы
триггеров. Можно сконструировать триггер с запуском по положительному
фронту — он называется прямой D-триггер, или просто D-триггер. Для этого
нужно скомбинировать три RS-триггера, как показано на рис. 3.9. Запуск по положительному фронту означает, что триггер сработает при переходе от логического
0 к логической 1; триггер с запуском по отрицательному фронту будет работать
при переходе от логической 1 к логическому 0.

Представление времени   117

S
Q
генератор
тактовых сигналов

Q
R

D

Рис. 3.9. Проект D-триггера
Эту схему не просто понять с первого взгляда. Два вентиля справа образуют
RS-триггер. Из рис. 3.5 мы знаем, что выходы не изменятся, если S или R не
станут низкими.
На рис. 3.10 показано, как схема ведет себя при различных значениях D и тактовой частоты. Тонкие линии показывают логические нули, толстые — логические
единицы. Начиная с левого края, видим, что, когда на генераторе 0, значение D
не так важно, потому что и S и R имеют высокий уровень, поэтому состояние
защелки в правой части рис. 3.9 не изменилось. Двигаясь вправо, по следующим двум схемам видим, что при низком R изменение значения D не влияет на

S
0

S
1

S
1

R

1

R

0

S
1

R

1

S

R

0

Рис. 3.10. Работа D-триггера

R

1

118   Глава 3. Последовательная логика
результат. Аналогичным образом две крайние правые схемы показывают, что при
низком R изменение значения D не влияет на результат. То есть изменения на D
не важны, если генератор тактовых сигналов имеет высокий или низкий уровень.
Теперь посмотрим, что произойдет, когда генератор сменит уровень с низкого
на высокий, как показано на рис. 3.11.

S
0

1

0

R

1

S

S

1

R

1

S

R

0

R

0

Рис. 3.11. Работа D-триггера с положительным фронтом
Слева видим, что при низком уровне генератора и высоком D
S и R также имеют высокий уровень, поэтому ничего не меняется.
Но когда генератор меняет значение на 1, S переходит на низкий
уровень, что меняет состояние триггера. Справа видим аналогичное поведение — при низком D и высоком уровне генератора
R становится низким и меняет состояние триггера. Из рис. 3.10
понятно, что никакие другие изменения не влияют на результат.
В 1918 году британские физики Уильям Экклс (William Eccles)
и Фрэнк Джордан (Frank Jordan) изобрели первую электронную
версию триггера, в которой использовались вакуумные лампы.
На рис. 3.12 показано условное обозначение чуть менее старинного D-триггера под названием 7474.

S
D

Q
CK Q
R

Рис. 3.12.
D-триггер

D-триггер имеет комплементарные выходы Q и Q, комплементарные входы S
(установка) и R (сброс). Это немного сбивает с толку, поскольку на схеме показаны S и R; комбинация со знаком ○ делает их S и R. Итак, это похоже на уже
изученный RS-триггер, за исключением непонятных обозначений в левой части.
Непонятные обозначения — это два дополнительных входа: D — для данных
(data) и CK — для тактовых сигналов (clock), которые представлены треугольником. Это триггер с запуском по положительному фронту, поэтому значение
входа D сохраняется всякий раз, когда сигнал на CK изменяется от 0 до 1.

Представление времени   119
Устройства, запускаемые по фронту, имеют и другие нюансы в подсчете
времени, кроме задержки распространения. Учитывается время установки,
которое представляет собой промежуток времени до фронта тактового сигнала, в течение которого сигнал должен быть стабильным, и время удержания,
которое представляет собой промежуток времени после фронта тактового
сигнала, в течение которого сигнал должен быть стабильным. Они показаны
на рис. 3.13.
установка удержание

Не имеет значения

D

Не имеет значения

CK
Начальное состояние

Q

Распространение

Действительный результат

Рис. 3.13. Время установки и удержания
Как видите, нам не нужно заботиться о том, что происходит на входе D, за
исключением времени установки и удержания по фронту тактового сигнала. И, как и в случае со всей другой логикой, выходной сигнал становится
стабильным после времени задержки распространения и остается таковым
независимо от входа D. Время установки (setup) и удержания (hold) обычно
обозначается как tsetup и thold.
Поведение триггеров на фронтах хорошо сочетается с генераторами тактовых
сигналов. Мы убедимся в этом на примере в следующем разделе.

Счетчики
Триггеры часто применяются для подсчета. Например, можно отсчитывать
время от осциллятора и управлять дисплеем с помощью дешифратора, получив в результате цифровые часы. На рис. 3.14 показана схема, которая выдает
3-битное число (C2, C1, C0), то есть количество раз, когда сигнал изменяется с 0
на 1. Сигнал сброса может использоваться для установки счетчика на 0.
C0

C1

S
D
сигнал

C2

S
Q

CK Q
R

D

S
Q

CK Q
R

D

Q
CK Q
R

сброс

Рис. 3.14. 3-битный счетчик со сквозным переносом

120   Глава 3. Последовательная логика
Данный счетчик называется счетчиком со сквозным переносом, и его результат
передается слева направо. C0 изменяет C1, C1 изменяет C2 и т. д., если есть другие
биты. Поскольку вход D каждого триггера соединен с его выходом Q, он будет
менять состояние при каждом положительном переходе сигнала CK.
Этот счетчик еще называют асинхронным, потому что все его элементы срабатывают только тогда, когда до них дойдет сигнал. Проблема асинхронных систем
заключается в том, что трудно понять, когда получен конечный результат. Выходы (C2, C1, C0) не активны во время сквозной передачи сигнала. Вы можете
увидеть на рис. 3.15, что для получения результата для каждого последующего
бита требуется все больше времени. Серые области на рисунке представляют
неопределенные значения из-за задержки распространения.
сигнал

сигнал

C0

C0

C1

C1

C2

C2
Время

Время

Рис. 3.15. Временная диаграмма счетчика со сквозным переносом
Временная диаграмма слева показывает, что мы получаем действительное 3-битное число, после того как установятся задержки распространения. Но справа
видно, что мы пытаемся считать результат быстрее, чем позволяют задержки
распространения, поэтому бывают случаи, когда действительное число не
получается.
Это похоже на проблему, которую мы рассматривали при изучении каскадного
сумматора на рис. 2.41. Подобно тому как мы смогли решить ее с помощью
упреждающего переноса, мы можем исправить проблему со сквозным переносом,
используя синхронный счетчик.
В отличие от счетчика со сквозным переносом все выходы синхронного счетчика
изменяются одновременно (синхронно). Это означает, что все триггеры синхронизируются параллельно. Трехбитный синхронный счетчик показан на рис. 3.16.
Видим, что все триггеры в состоянии счетчика меняются одновременно, потому
что все они одновременно синхронизируются. Хотя задержка распространения попрежнему определяет правильность результатов, каскадный эффект устраняется.
Счетчики — это еще один функциональный строительный блок, то есть у них
имеется свое схематическое обозначение. В данном случае это еще один прямо­
угольник, как видно на рис. 3.17.

Представление времени   121

C0

C1

D Q

D Q

C2

D Q

Q
генератор
тактовых сигналов

Рис. 3.16. 3-битный синхронный счетчик

D 0–n
CK

Счетчик
CLR U/D EN

Q 0–n
LD

Рис. 3.17. Условное обозначение счетчика на схеме
На рисунке показан ряд входов, которые мы раньше не видели. Счетчики могут
иметь все или только некоторые из этих входов. Большинство счетчиков имеют
вход CLR, который очищает счетчик, устанавливая его значение равным 0. Также
распространен вход EN, который включает счетчик (тот не ведет счет, если не
включен). Некоторые счетчики могут вести счет в любом направлении; вход
U/D выбирает направление вверх или вниз. Наконец, иногда счетчики имеют
входы данных D0–n и сигнала нагрузки LD, позволяющие задать определенное
значение для счетчика.
Теперь можно использовать счетчики для учета времени. Но это не единственное,
что умеют триггеры. Мы разберемся, как запоминать большие объемы информации, в следующем разделе.

Регистры
D-триггеры хорошо подходят для запоминания информации. Часто с той же
целью используются регистры, представляющие собой набор D-триггеров
в связке с общим генератором тактовых сигналов. На рис. 3.18 показан пример
регистра, хранящего результат сложения с использованием описанной ранее
схемы сумматора.
После синхронизации результата работы сумматора с регистром можно изменять
операнды, не меняя результата. Обратите внимание, что регистры часто имеют
входы разрешения, аналогичные таким же для счетчиков.

122   Глава 3. Последовательная логика

D0
Операнд 1
Сумматор
Операнд

2

Q0
Q1
Q2
Q3
Q4
Q5
Q6
Q7
EN

D1
D2
D3
D4
D5
D6
D7

Результат

генератор включение
тактовых
сигналов

Рис. 3.18. Регистр, хранящий результат работы сумматора

Организация памяти и обращение к памяти
Мы уже убедились, что триггеры полезны, когда нужно запомнить немного
информации, и что регистры удобны для запоминания набора битов. Но что
использовать, когда нужно запомнить намного больше? Например, если необходиРегистр 0
мо хранить несколько разных результатов
Регистр 1
сложения?
Что ж, можно начать с большой кучи регистров. Однако здесь возникает новая
проблема: как указать регистр, который
нужно использовать? Эта ситуация проиллюстрирована на рис. 3.19.

выход

вход

Регистр 2
Регистр n

Рис. 3.19. Несколько регистров

Один из способов решить эту проблему — присвоить каждому регистру номер,
как показано на рисунке. Можно указать этот номер, или адрес, чтобы выбрать
регистр, используя один из стандартных строительных блоков — дешифратор
из раздела «Построение дешифраторов» на с. 107. Выходы дешифратора подключены ко входам включения регистров.
Далее нужно предусмотреть возможность выбирать выход из указанного регистра. К счастью, мы узнали, как создавать селекторы, в разделе «Построение
селекторов» на с. 108, и они как раз нам пригодятся.
Системы часто имеют несколько компонентов памяти, которые необходимо соединить. Пришло время для еще одного из стандартных строительных блоков:
выхода с тремя состояниями.
В совокупности элемент памяти выглядит, как показано на рис. 3.20.

Организация памяти и обращение к памяти   123

вход

Регистры

разрешение записи

Декодер

адрес

Селектор

выход

разрешение чтения

Рис. 3.20. Элемент памяти
В элементах памяти множество элект­
рических соединений. Если мы оперируем
адрес
Память
данные
32-битными числами, нам потребуется по
32 соединения для каждого входа и выхода плюс соединения для адреса, сигналов
включение
управления и питания. Программистам не
чтение/запись
нужно беспокоиться о том, как уместить
Рис. 3.21. Упрощенная схема
схемы в ячейки или как проложить промикросхемы памяти
вода, — это делают разработчики аппаратного обеспечения. Можно сократить
количество подключений, так как вряд ли понадобится читать и записывать
в память одновременно. Тогда можно обойтись одним набором подключений
к данным плюс разрешения на чтение/запись. На рис. 3.21 показана упрощенная
схема микросхемы памяти. Она управляется элементом управления включение,
так что можно соединить вместе несколько микросхем памяти.
Видим, что на рисунке вместо отдельных сигналов используются большие
толстые стрелки для адреса и данных. Мы называем группы связанных сигналов шинами, поэтому микросхема памяти имеет адресную шину и шину
данных. Да, это аналогия с общественным транспортом для битов (bus
(шина) — автобус).
Еще одна проблема при упаковке микросхем памяти возникает, когда размер
памяти увеличивается и требуется подключение большого количества битов
адреса. Если свериться с табл. 1.2 в главе 1, нам потребуется 32 адресных соединения для компонента памяти объемом 4 ГиБ.
Проектировщики памяти и планировщики дорог решают аналогичные проблемы управления движением. Многие города организованы в сети, и точно так же
устроены внутри микросхемы памяти. На микрофотографии ЦП, показанной
на рис. 2.3, видим несколько прямоугольных областей, которые представляют
собой блоки памяти. Адрес разделен на две части: адрес строки и адрес столбца.

124   Глава 3. Последовательная логика
Адрес ячейки памяти внутренним образом определяется по пересечению строки
и столбца, как показано на рис. 3.22.
Y0
A 0 Декодер Y
1
адреса
A 1 столбца Y 2
Y3

A0
A1

Y0
A 0 Декодер Y
1
адреса
Y
2
A 1 строки
Y3

A2
A3

Массив
памяти

Рис. 3.22. Адресация строк и столбцов
Очевидно, нам не нужно беспокоиться о количестве адресных строк в 16-разрядной памяти, показанной на этом рисунке. Но что, если разрядов намного
больше? Можно вдвое сократить количество адресных строк, мультиплексируя
адреса строк и столбцов. Все, что нам потребуется, это регистры на микросхеме
памяти для их сохранения, как показано на рис. 3.23.
Строб адреса столбца
(column address strobe, CAS)

A 0/2

A 1/3

D 0 Регистр Q
0
адресов
Q
D 1 столбца 1

Y0
A 0 Декодер Y
1
адреса
Y
столбца
2
A1
Y3

D 0 Регистр Q
0
адресов
Q
1
D 1 строк

Y0
A 0 Декодер Y
1
адреса
Y
строки
2
A1
Y3

Массив
памяти

Строб адреса строки (RAS)

Рис. 3.23. Память с адресными регистрами
Поскольку адрес состоит из двух частей, отсюда следует, что производительность улучшилась бы, если бы нам нужно было изменить только одну часть,
например, установив адрес строки и затем изменив адрес столбца. Такой подход
используется в современных больших микросхемах памяти.

Организация памяти и обращение к памяти   125
Микросхемы памяти описываются по их размеру в формате глубина × ширина.
Например, микросхема 256 × 8 будет иметь 256 ячеек памяти шириной 8 бит;
микросхема 64 МиБ × 1 будет иметь 64 мебибита.

Оперативная память
Память, о которой мы уже говорили, называется памятью на основе оперативного запоминающего устройства, ОЗУ (random-access memory, RAM). С помощью
ОЗУ полная ширина любой области памяти может быть прочитана или записана
в любом порядке.
Статическая ОЗУ (static RAM, SRAM) — это дорого, но быстро. На каждый бит
требуется шесть транзисторов. Поскольку транзисторы занимают место, SRAM
не очень хорошо подходит для хранения миллиардов или триллионов битов.
Динамическая память (dinamic memory, DRAM) — хитрый прием. Электроны
хранятся в микроскопических емкостях, называемых конденсаторами, с использованием только одного транзистора в качестве крышек емкостей. Проблема
в том, что в этих емкостях происходят утечки, поэтому требуется время от времени обновлять память — регулярно пополнять емкости. Необходимо следить,
чтобы обновление не произошло в критический момент, вызвав конфликт с доступом к памяти; это было проблемой в работе с одним из первых компьютеров
на базе DRAM, DEC LSI-11. Одним из интересных побочных эффектов DRAM
является то, что емкости пропускают больше, если на них ­падает свет. Это позволяет использовать их в качестве цифровых фотоаппаратов.
DRAM применяется для больших микросхем памяти благодаря своей высокой
плотности (количеству битов на область). Большие микросхемы памяти означают множество адресов, в связи с чем микросхемы DRAM используют схему
мультипексированной адресации, описанную в предыдущем разделе. Из-за других соображений внутреннего дизайна адрес строки можно сохранить быстрее
с помощью строба адреса строки, а затем изменить адрес столбца с помощью
строба адреса столбца. Строки — часто используемый термин, но их иногда еще
называют страницами. Поэтому процесс можно сравнить с чтением книги —
сканировать страницу намного проще, чем перелистывать. Это очень важный
принцип программирования: хранение вещей, которые используются вместе,
в одном ряду значительно повышает производительность.
И SRAM, и DRAM являются энергозависимой памятью, а значит, данные
могут быть потеряны при отключении питания. Память на магнитных сердечниках — старинный энергонезависимый тип ОЗУ, в котором биты хранятся
в тороидальных (пончиковидных) железных элементах, которые показаны на
рис. 3.24. Тороиды были намагничены: одно направление означало 0, а другое — 1. Физическая природа тороидов интересна, потому что они очень устойчивы к электромагнитным помехам извне. При таком типе памяти ферритовые
сердечники располагались в сетке проводников, называемой плоскостью, через

126   Глава 3. Последовательная логика

Рис. 3.24. Память на магнитных сердечниках
которую проходили вертикальные и горизонтальные проводники. Был также
третий провод — проводник «ощущений». Он назывался так потому, что единственный способ прочитать состояние бита — попытаться его изменить, а затем
«почувствовать», что произошло. Конечно, выяснив, что бит изменился, вы
должны были вернуть его значение, иначе данные терялись — и бит оказывался
бесполезным. Вся система основывалась на множестве дополнительных схем.
Память на магнитных сердечниках на самом деле была трехмерной, поскольку
плоскости собирались в блоки.
Несмотря на то что память на магнитных сердечниках не нова, ее энергонезависимые свойства по-прежнему ценятся и продолжаются исследования по созданию
коммерчески практичной магниторезистивной памяти, которая сочетает в себе
лучшее из технологии памяти на магнитных сердечниках и оперативной памяти.

Постоянное запоминающее устройство
Постоянное запоминающее устройство, или ПЗУ (read-only memory, ROM), — не
очень точное название1. Память, которую можно только прочитать, но никогда не
записать, бесполезна. Несмотря на то что название прижилось, точнее будет сказать, что ПЗУ — это память с однократной записью. ПЗУ можно записать один
раз, а затем многократно читать. ПЗУ важно для устройств, которые должны
иметь встроенную программу, например для микроволновой печи; представьте,
что было бы, если бы вам приходилось перед приготовлением попкорна каждый
раз программировать микроволновую печь заново.
Одной из первых форм ПЗУ была карта Холлерита (Hollerith card), которая позже стала известна как перфокарта, показанная на рис. 3.25. Биты обозначались
1

Read-only memory — в переводе с английского «память только для чтения». — Примеч.
ред.

Организация памяти и обращение к памяти   127

Рис. 3.25. Перфокарта
проколами на бумаге. Да, именно так! Они были довольно дешевыми, потому что
американский изобретатель Герман Холлерит (1860–1929) умел экономить на
качестве. Холлерит изобрел карту в конце XIX века, хотя было бы точнее сказать,
что он позаимствовал идею жаккардового ткацкого станка, изобретенного Жозефом Мари Жаккардом в 1801 году. В жаккардовом ткацком станке для управления
узором ткачества использовались перфокарты. Конечно, Жаккард позаимствовал
эту идею у Базиля Бушона, который в 1725 году изобрел ткацкий станок с перфолентой. Иногда бывает трудно отличить изобретение от присвоения, потому
что будущее строится на прошлом. Помните об этом, когда слышите, как люди
выступают за более длительные и строгие законы о патентах и авторском праве;
прогресс замедлится, если мы не сможем опираться на прежние изобретения.
Ранние устройства для чтения перфокарт использовали переключатели, чтобы
считывать биты. Карты просовывались под ряд пружинящих проводов, которые проходили через отверстия и соприкасались с куском металла с обратной
стороны. Более поздние версии, направлявшие свет через отверстия на ряд
фотоприемников на обратной стороне, работали значительно быстрее.
Перфорированная бумажная лента — технология, родственная ПЗУ; рулоны
бумажной ленты с пробитыми в ней отверстиями использовались для обозначения битов (рис. 3.26). Лента имела преимущество перед карточками в том, что
в случае падения колоды карт данные искажались. В то же время лента могла
порваться, и ее было трудно ремонтировать; во время ремонта данные на ленте
могли быть повреждены.
Карты и лента работали очень медленно, потому что их нужно было физически
перемещать, чтобы прочитать.
Вариант ПЗУ под названием «веревочная ферритовая матрица» использовался
в бортовом компьютере Apollo (рис. 3.27). Поскольку такую схему можно было
записать только с помощью шитья, память была невосприимчива к помехам,
что важно в суровых условиях космоса.

128   Глава 3. Последовательная логика

Рис. 3.26. Перфорированная бумажная лента

Рис. 3.27. Веревочная ферритовая матрица на управляющем компьютере Apollo
Перфокарты и бумажная лента представляли собой последовательную память —
данные считывались по порядку. Считывающее устройство не могло работать
в обратном порядке, поэтому такая память действительно годилась только для
длительного хранения данных. Прежде чем использовать содержимое, его нужно было прочитать в оперативную память. Первое коммерческое применение
однокристального микропроцессора Intel 4004 в 1971 году вызвало потребность
в более совершенной технологии хранения программ. Первые микропроцессоры
использовались для таких устройств, как калькуляторы, которые запускали
фиксированную программу. Им на смену пришла масочная ПЗУ. Маска — это
трафарет, используемый в процессе изготовления интегральной схемы. Написав
программу, вы отправили бы битовую комбинацию производителю полупроводников, сопроводив ее немаленьким чеком. Далее эту комбинацию превратили
бы в маску, а вы бы получили микросхему, содержащую программу. Такая

Блочные устройства   129
микросхема была доступна только для чтения, потому что не было возможности изменить ее, не выписав еще один большой чек и не сделав другую маску.
Масочную ПЗУ можно было читать в произвольном порядке.
Маски были настолько дорогими, что их использование можно было оправдать
только для массового применения. Появились программируемые ПЗУ (ППЗУ,
programmable read-only memory, PROM) — микросхемы ПЗУ, которые можно
было программировать самостоятельно, но только один раз. Первоначальный
механизм ППЗУ включал плавку нихрома (никель-хромового сплава) на
кристалле. Нихром — это то же самое, из чего сделаны светящиеся провода
в тостере.
При разработке программ использовались бы огромные кучи микросхем ППЗУ.
Инженеры не потерпели таких неудобств, поэтому ППЗУ сменила стираемая
ППЗУ (erasable programmable read-only memory, EPROM). Эти микросхемы были
похожи на ППЗУ, за исключением того, что наверху у них было кварцевое окошко и их можно было стереть, поместив под специальный ультрафиолетовый свет.
Жизнь стала проще с появлением электрически стираемой ППЗУ (что за набор
слов!), или EEPROM (electrically erasable programmable read-only memory). Это
микросхема ППЗУ, которую можно стереть электрически — без света и кварцевого окна. Однако стирание EEPROM происходит сравнительно медленно, так
что делать этого не стоит. EEPROM технически представляет собой ОЗУ, так
как ее содержимое можно читать и записывать в любом порядке. Но поскольку
данные в EEPROM записываются медленно и дороже, чем в ОЗУ, они используются только вместо ПЗУ.

Блочные устройства
Чтобы обратиться к памяти, нужно время. Представьте, что вам пришлось бы
ходить в магазин каждый раз, когда понадобится стакан муки. Гораздо практичнее сходить в магазин один раз и принести домой целый пакет. В более
крупных устройствах памяти используется именно этот принцип. Представьте
себе оптовые закупки битов.
Дисковые накопители, также известные как носители информации, отлично
подходят для хранения огромных объемов данных. На момент написания этой
книги диск на 8 Тбайт стоил менее 200 долларов. Дисковые накопители часто
называют просто носителями информации. Дисководы хранят биты на вращающихся магнитных пластинах, как закуски на вращающихся подносах. Закуски
(биты) периодически перемещаются к вашему месту за столом, и вы можете
дотянуться до них рукой. В дисководе вместо руки используется головка диска.
Дисковые накопители относительно медленны по сравнению с другими типами
памяти. Если вы хотите снова прочитать только что найденную головкой часть,

130   Глава 3. Последовательная логика
вам придется подождать почти целый оборот, пока головка не найдет то же
место. Современные диски вращаются со скоростью 7200 оборотов в минуту
(об/мин), это означает, что вращение занимает чуть больше 8 миллисекунд.
Большой минус дисковых накопителей заключается в том, что они механические
и постепенно изнашиваются. Износ подшипников — одна из основных причин
выхода диска из строя. Разница между коммерческими и потребительскими
устройствами заключается прежде всего в количестве смазки в подшипнике:
производители могут брать сотни долларов за то, что стоит меньше копейки.
Дисковые накопители хранят данные, намагничивая области на диске, что делает
их энергонезависимыми, как память на магнитных сердечниках.
Дисковые накопители — это компромисс между скоростью и плотностью записи.
Они медленны из-за того, что битам нужно время, прежде чем они дойдут до
головки. Однако, поскольку данные перемещаются прямо под головку, нам не
требуется место для хранения информации об адресах и связях между данными, в отличие, например, от DRAM. На рис. 3.28 показаны внутренние части
жесткого диска. Они заключены в герметичные контейнеры, потому что пыль
и грязь могут привести к их поломке.

Рис. 3.28. Жесткий диск
Диски имеют блочную, а не байтовую адресацию. Блок (исторически называемый
сектором) — это наименьшая единица, к которой можно получить доступ. Исторически диски имели секторы по 512 байт, хотя новые устройства разделяются
на секторы по 4096 байт. Это означает, что для изменения одного байта на диске
придется прочитать весь блок, изменить один байт и перезаписать весь блок. Диски
содержат одну или несколько пластин, расположенных, как показано на рис. 3.29.
Поскольку все секторы содержат одинаковое количество битов, плотность
битов (бит/мм2) в центре каждой пластины больше, чем на внешнем крае. Это

Блочные устройства   131

Геометрический сектор
Дорожка

Сектор

Кластер

Рис. 3.29. Структура диска
неэкономно, потому что на внешних дорожках явно есть место, чтобы втиснуть
еще больше битов. Новые диски решают эту проблему, разделяя диск на набор
радиальных зон — фактически они имеют больше секторов во внешних зонах,
чем во внутренних.
Производительность дисковых накопителей описывается некоторыми цифрами.
Современные диски имеют головку на приводном рычаге, который движется
радиально по диску; положение головки разделяет диски на дорожки. Время
поиска — это время, необходимое для перемещения головки с одной дорожки
на другую. Конечно, наличие одной головки на каждую дорожку ускорило бы
поиск; такой способ подошел бы для очень старых дисков, но на современных
дисках дорожки расположены слишком близко друг к другу, чтобы это можно
было реализовать. Помимо времени поиска, указывается время, необходимое
для поворота нужной части диска таким образом, чтобы она оказалась под головкой, — это называется задержкой вращения, которая, как мы видели выше,
измеряется вмиллисекундах.
Дисковые накопители часто называют жесткими дисками. Изначально все дисковые накопители были жесткими. Различие возникло, когда на сцене появились
дешевые съемные устройства хранения данных под названием «дискеты». Они
были гибкими, поэтому другой тип и назвали «жестким», чтобы их можно было
легко отличить друг от друга.
Появился еще накопитель на магнитных барабанах — устаревшая разновидность
дисковых накопителей, которая работала согласно описанию: вращающийся
магнитный барабан с полосами головок.
Магнитная лента — еще одна технология энергонезависимого хранения, в которой используются катушки намагниченной ленты. Она намного медленнее,
чем дисковод, и на то, чтобы перемотать ленту в требуемое положение, может

132   Глава 3. Последовательная логика
понадобиться много времени. В ранних компьютерах Apple для хранения магнитной ленты использовались аудиокассеты потребительского уровня.
Оптические диски похожи на магнитные, за исключением того, что они используют свет вместо магнетизма. Они знакомы вам как CD и DVD. Большим преимуществом оптических дисков является то, что их можно массово выпускать
посредством печати. Предварительно отпечатанные диски — это ПЗУ. Версии,
эквивалентные ППЗУ, которые можно записать один раз (CD-R, DVD-R), также
доступны, как и версии, которые можно стирать и перезаписывать (CD-RW).
На рис. 3.30 показан крупный план части оптического диска.

Рис. 3.30. Данные на оптическом диске

Флеш-память и твердотельные диски
Флеш-память — самое последнее воплощение EEPROM. Она прекрасно подходит для некоторых случаев, например для музыкальных плееров и цифровых
фотоаппаратов. Принцип работы флеш-памяти основан на хранении электронов
в емкостях, подобно DRAM. В этом случае емкости имеют больший размер и построены так, чтобы не протекать. Но петли на крышках емкостей со временем
изнашиваются, если их открывать и закрывать слишком много раз. Флеш-память
стирается быстрее, чем EEPROM, но ее изготовление дешевле. Она работает как
ОЗУ для чтения, а также для записи пустого устройства, заполненного нулями.
Но хотя нули можно превратить в единицы, их нельзя обратить, предварительно
не стирая. Флеш-память внутренне разделена на блоки, и стирать можно только
блоки, а не отдельные места в памяти. Устройства флеш-памяти имеют произвольный доступ для чтения и блочный доступ для записи.
Дисковые накопители постепенно заменяются твердотельными накопителями,
которые в значительной степени представляют собой просто флеш-память,
упакованную так, чтобы выглядеть как дисковый накопитель. Сейчас их цена
за бит намного выше, чем у вращающихся дисков, но это должно вскоре измениться. Поскольку флеш-память изнашивается, твердотельные накопители
включают в себя процессор, который отслеживает использование различных
блоков и пытается выровнять их так, чтобы все блоки изнашивались с одинаковой скоростью.

Обнаружение и исправление ошибок   133

Обнаружение и исправление ошибок
Вы никогда не предугадаете, когда случайный космический луч поразит часть
памяти и испортит данные. Было бы неплохо узнать, когда это произойдет, и еще
лучше — иметь возможность устранить повреждения. Конечно, такие улучшения
стоят денег и обычно не встречаются в устройствах потребительского уровня.
Мы хотим иметь возможность обнаруживать ошибки без необходимости хранить
вторую полную копию данных. Но даже такой подход не сработает, потому что
мы не можем узнать, какая из копий верная. Можно сохранить две дополнительные копии и предположить, что соответствующая пара (если она есть) верна. Так
делают компьютеры, предназначенные для работы в очень суровых условиях.
Они также используют более дорогую схему, которая не сгорает при попадании
в нее протона. Например, космический шаттл имел резервные компьютеры
и систему автоматического выбора в случае обнаружения ошибки.
Мы можем проверить наличие 1-битной ошибки, используя метод под названием «четность». Идея в том, чтобы сложить количество битов, для которых
установлено значение 1, и использовать дополнительный бит для хранения
информации о четности или нечетности этой суммы. Можно сделать это, вычислив исключающее ИЛИ битов. Есть две формы проверки: при положительной
четности используется сумма битов, а при нечетной — дополнение к сумме
битов. Этот выбор может показаться странным, но его номенклатура основана
на количестве единиц или нулей, включая бит четности.
Левая половина рис. 3.31 показывает расчет положительной четности; мы получили четыре единицы, поэтому четность равна 0. Правая половина показывает
проверку четности; 0 означает, что данные хороши или, по крайней мере, настолько хороши, насколько можно судить по четности. Большой минус четности
заключается в том, что две ошибки наверняка будут выглядеть как правильные
данные; этот метод выявляет только нечетное количество ошибок.
0
0

0

0

1

0

0

1

0

0

1

0

1

0

0

0

0

1

1

0

0

0

1

1

0

0

1

1

0

0

0

1

1

8

7

6

5

4

3

2

1

0

8

7

6

5

4

3

2

1

0

Рис. 3.31. Генерация и проверка четности

134   Глава 3. Последовательная логика
Существуют более сложные методы, такие как коды Хэмминга, изобретенные
американским математиком Ричардом Хэммингом (Richard Hamming) (1915–
1998). Они занимают больше битов, но позволяют обнаруживать больше ошибок
и исправлять некоторые из них. Доступны микросхемы памяти для проверки
и исправления ошибок (error checking and correcting, ECC), которые используют
данный подход. Обычно они используются в крупных центрах обработки данных, а не в потребительских устройствах.
Методы наподобие четности хороши для постоянно изменяющихся данных.
Существуют менее дорогие методы, позволяющие проверять данные в статических блоках — например, компьютерных программах. Самым простым из них
является контрольная сумма, где содержимое каждой ячейки данных суммируется в некоторое n-битное значение, а биты переполнения отбрасываются.
Контрольную сумму можно сравнить с программой, обычно непосредственно
перед ее запуском. Чем больше значение контрольной суммы (то есть больше n),
тем меньше вероятность получения ложного срабатывания.
Циклический избыточный код (cyclic redundancy check, CRC) — математически более верная замена контрольных сумм. Еще один вариант — хеш-коды.
Их цель — вычислить проверочный номер, который достаточно уникален для
данных, чтобы при большинстве изменений проверка могла выявить несоответствие.

Аппаратное и программное обеспечение
Методы, используемые для создания ППЗУ, EEPROM и флеш-памяти, не
ограничиваются памятью. Скоро мы увидим, как из логических схем создается
компьютерное аппаратное обеспечение. А поскольку вы изучаете программирование, то знаете, что программы включают логику в код, и вы можете знать,
что компьютеры предоставляют логику программам через свои наборы команд.
В чем разница между аппаратным и программным обеспечением? Граница нечеткая. По большому счету, различий между ними мало, за исключением того,
что создавать программное обеспечение гораздо проще, поскольку это не требует
дополнительных затрат, кроме времени разработки.
Вы, наверное, слышали термин «прошивка», который изначально относился
к программному обеспечению в ПЗУ. Но большая часть прошивок теперь живет
во флеш- или даже в оперативной памяти, поэтому разница между обычным ПО
и прошивкой минимальна. На самом деле все еще сложнее. Раньше микросхемы
разрабатывались компьютерными гиками, которые строили схемы, наклеивая
цветную малярную ленту на большие листы прозрачной майларовой пленки.
В 1979 году американские ученые и инженеры Карвер Мид (Carver Mead)
и Линн Конвей (Lynn Conway) изменили мир своей публикацией «Introduction
to VLSI Systems», которая дала толчок развитию индустрии автоматизации

Выводы   135
электронного проектирования (electronic design automation, EDA). Проектирование микросхем превратилось в проектирование программного обеспечения.
Сегодня микросхемы разрабатываются с использованием специализированных
языков программирования, таких как Verilog, VHDL и SystemC.
В большинстве случаев программисту просто предоставляют аппаратное обеспечение. Но вы можете получить возможность поучаствовать в разработке системы,
включающей как аппаратное, так и программное обеспечение. Проектирование
интерфейса между аппаратным и программным обеспечением имеет решающее
значение. Существует бесчисленное множество примеров микросхем с непригодными для использования, непрограммируемыми и ненужными функциями.
Интегральные схемы дороги в изготовлении. Вначале все микросхемы были
полностью заказными. Микросхемы собираются слоями: сами компоненты
находятся внизу, а металлические слои — вверху, а затем они соединяются.
Вентильные матрицы были попыткой снизить стоимость для некоторых случаев использования; был доступен набор предварительно спроектированных
компонентов, и только металлические слои проектировались по заказу. Как
и в случае с памятью, они были заменены версиями, эквивалентными ППЗУ,
которые можно было программировать самостоятельно. Существовал и эквивалент стираемой ППЗУ, который можно было стереть и перепрограммировать.
Современные, программируемые пользователем вентильные матрицы (ППВМ,
field-programmable gate array, FPGA) являются эквивалентом флеш-памяти;
их можно перепрограммировать с помощью соответствующего ПО. Во многих
случаях использование ППВМ дешевле, чем использование других компонентов. ППВМ очень функциональны; например, можно получить большую
ППВМ, содержащую пару процессорных ядер ARM. Компания Intel недавно
приобрела Altera и теперь может включать ППВМ в микросхемы процессора.
Существует ненулевой шанс, что вы будете работать над проектом, содержащим
одно из этих устройств, поэтому будьте готовы превратить свое программное
обеспечение в аппаратное.

Выводы
В этой главе вы узнали, как у компьютеров появилось чувство времени. Вы познакомились с последовательной логикой, которая, наряду с комбинаторной
логикой из главы 2, предоставляет все фундаментальные строительные блоки
для аппаратного обеспечения. А еще вы кое-что узнали о том, как устроена память. В главе 4 мы объединим все эти знания и создадим компьютер.

4

Анатомия компьютера

Вы узнали о свойствах битов и способах их использования для представления разных объектов в главе 1. В главах 2 и 3 вы узнали, почему мы используем биты и как
они реализованы в аппаратном обеспечении. Вы также
рассмотрели ряд основных строительных блоков и то,
как их можно объединить в более сложные конфигурации.
В этой главе мы изучим, как объединить эти блоки в схему, которая может управлять битами. Эта схема называется компьютером.
Есть много способов собрать компьютер. Вариант, который мы создадим в этой
главе, был выбран для удобства объяснения, а не потому, что он лучше всего спроектирован. Конечно, простые компьютеры функционируют, но заставить их работать
хорошо не так просто из-за множества сложностей. Эта глава посвящена простому
компьютеру, а следующие две — некоторым дополнительным сложностям.
В современном компьютере есть три больших элемента. Это память, ввод
и вывод (input and output, I/O) и центральный процессор (ЦП). В этом разделе
рассказывается, как эти части соотносятся друг с другом. Память представлена
в главе 3, а в главе 5 компьютеры и память рассматриваются более подробно.
Ввод/вывод является предметом главы 6. ЦП находится в так называемом
«центре города» в этой главе.

Память
Компьютерам нужно место для хранения битов, которыми они управляют. Это
место — память, как вы узнали из главы 3. Теперь пора выяснить, как компьютеры ее используют.

Память   137
Память похожа на длинную улицу, полную домов. Каждый дом абсолютно
одинакового размера, и в нем есть место для определенного количества битов.
Строительные нормы в значительной степени ограничились одним байтом на
дом. И, как и на настоящей улице, у каждого дома есть адрес, который представляет собой просто номер. Если на вашем компьютере 64 МиБ памяти, это
64 × 1024 × 1024 = 67 108 864 байта (или 536 870 912 бит). Байты имеют адреса
от 0 до 67 108 863. Такая нумерация имеет смысл, в отличие от нумерации на
многих улицах в реальной жизни.
Довольно часто ссылаются на ячейку памяти, которая представляет собой просто память по определенному адресу, например «улица Памяти, 3» (рис. 4.1).

Адрес байта
Центр
города

0

1

2

3

...

n

Улица Памяти

Рис. 4.1. Улица памяти
Тот факт, что основной единицей памяти является байт, не означает, что мы
всегда рассматриваем память сквозь призму байтов. Например, 32-разрядные
компьютеры обычно организуют память в виде блоков по 4 байта, в то время
как 64-разрядные компьютеры — в виде блоков по 8 байт. Почему это имеет
значение? Объединение превращает однополосные улицы в четырех- или
восьмиполосные шоссе. Больше полос может обрабатывать больше трафика,
потому что больше битов может попасть в шину данных. Когда мы обращаемся
к памяти, нам нужно знать, к чему именно мы обращаемся. Адресация длинных
слов отличается от адресации байтов, поскольку длинное слово на 32-битном
компьютере составляет 4 байта, а на 64-битном компьютере — 8 байт. На рис. 4.2,
например, адрес длинного слова 1 содержит байтовые адреса 4, 5, 6 и 7.
Я попытаюсь объяснить иначе: улица в 32-битном компьютере содержит четыре
комплекса, а не отдельные дома, и каждый четырехэтажный дом содержит два
дуплекса. Это означает, что можно назвать адрес отдельной единицы, дуплекса
или всего здания.
Вы, возможно, заметили, что все здания стоят на шоссе, так что каждый байт движется по собственной полосе, а длинное слово занимает всю дорогу. Биты ездят
в центр города и обратно на шине (автобусе) с четырьмя сиденьями, по одному
на каждый байт. Двери расположены так, чтобы на каждую полосу приходилось
по одному месту. На большинстве современных компьютеров шина останавливается только у одного здания для каждой поездки из центра города. Это означает,
что нельзя, например, сформировать длинное слово из байтов 5, 6, 7 и 8, потому
что в таком случае шине придется совершить две поездки: в здание 0 и здание 1.

138   Глава 4. Анатомия компьютера
В старых компьютерах была сложная загрузочная док-станция, которая позволяла
это делать, но планировщики заметили, что она не так уж и полезна, и поэтому
вырезали ее из бюджета на новые модели. Попытка попасть в два здания одновременно, как показано на рис. 4.3, называется невыровненным доступом.

Адрес
длинного слова
Адрес
полуслова
Адрес байта

0

1

0
0

1
2

1

2

2
3

4

3
5

6

4
7

8

5
9

10 11

Маршрут
шины

Центр
города

Рис. 4.2. Магистраль памяти

0

1

0
0

1
1

2

2

2
3

Выровненный

4

3
5

6

4
7

8

5
9

10 11

Невыровненный

Рис. 4.3. Выровненный и невыровненный доступ
Как мы узнали из предыдущей главы, существует множество различных видов
памяти с разным соотношением цены/качества. Например, SRAM быстрая и дорогая, как автомагистрали, рядом с которыми живут политики. Диск дешевый
и медленный — как грунтовая дорога.
Кто и на каком сиденье сидит в шине? Может ли байт 0 или байт 3 занять
крайнее левое место, когда в город направляется длинное слово? Это зависит
от используемого процессора, потому что проектировщики воплотили в жизнь
оба способа. Оба работают, так что это в значительной степени теологический
спор. Фактически термин «конечный» (endian; по аналогии с названиями партий
остроконечников и тупоконечников из романа Джонатана Свифта «Путешествие
Гулливера», которые спорили о том, с какого конца лучше всего разбивать сваренное всмятку яйцо) используется для описания разницы.

Ввод и вывод   139
Байт 0 занимает крайнее правое место в машинах с прямым порядком байтов
(little-endian), таких как процессоры Intel. Он же занимает крайнее левое место
в машинах с обратным порядком байтов (big-endian) — например, в процессорах
Motorola. На рис. 4.4 сравниваются две схемы.
Байт 3

Байт 2

Байт 1

Байт 0

Байт 0

Прямой порядок

Байт 1

Байт 2

Байт 3

Обратный порядок

Рис. 4.4. Размещение при прямом и обратном порядке байтов
При передаче информации с одного устройства на другое следует помнить
о порядке байтов — вы же не хотите случайно перемешать данные. Такое уже
случалось, когда операционная система UNIX была перенесена с PDP-11 на
компьютер IBM Series/1. Программа, которая должна была выводить «Unix»,
вместо этого выводила «nUxi», поскольку байты в 16-битных словах поменялись
местами. Это было довольно забавно, поэтому в обиход вошел термин «синдром
nuxi» для обозначения проблем с порядком следования байтов.

Ввод и вывод
Компьютер, который не может взаимодействовать с внешним миром, не очень
полезен. Нам нужен способ вводить в компьютер данные и выводить их из него.
Это называется вводом/выводом, или I/O (input/output). Устройства, которые
подключаются ко вводу/выводу, называются устройствами ввода/вывода. Поскольку они находятся на периферии компьютера, их также часто называют
периферийными устройствами, или просто периферией.
Раньше у компьютеров был отдельный канал ввода/вывода, как показано на
рис. 4.5, который был похож на «улицу Памяти». Это имело смысл, когда компьютеры были огромных размеров, потому что они не помещались в маленькие
корпуса с ограниченным количеством электрических соединений. Кроме того,
«улица Памяти» была не очень длинной, поэтому не имело смысла ограничивать
количество адресов только для поддержки ввода-вывода.

Маршрут шины ввода/вывода

Центр
города

Маршрут шины памяти

Рис. 4.5. Отдельные шины памяти и ввода/вывода
Теперь «Улица Памяти» намного длиннее, как в 32-, так и в 64-разрядных
компьютерах. Она настолько длинная, что существуют адреса без домов — доступно много пустых участков. Другими словами, есть адреса, с которыми не

140   Глава 4. Анатомия компьютера
связана память. В результате имеет смысл выделить часть «улицы Памяти» для
устройств ввода/вывода. Похоже на промышленный район на окраине города.
Кроме того, поскольку в связку, имеющую ограниченное количество соединений,
втиснуто как можно больше схем, логично, чтобы ввод/вывод находился на той
же шине, что и память.
Многие компьютеры имеют стандартные слоты ввода-вывода, поэтому периферийные устройства можно подключать к ним единообразно. Это делается примерно так же, как распределялась собственность на Диком Западе; территория,
не имеющая статуса городской, разделялась на набор земельных участков, как
показано на рис. 4.6. Каждый держатель слота может использовать все адреса
в пределах границ слота. Часто в каждом слоте есть определенный адрес, который содержит какой-то идентификатор, так что центр города может провести
перепись, чтобы определить, кто проживает в каждом слоте.

Память

Центр
города

Устройство
ввода/
вывода

Устройство
ввода/
вывода

Устройство
ввода/
вывода

Маршрут шины

Рис. 4.6. Общая память и шина ввода/вывода
Мы часто используем метафору из судоходства и говорим, что все пришвартованы к портам ввода/вывода.

Центральный процессор
Центральный процессор (ЦП) — это часть компьютера, которая выполняет
фактические вычисления. По нашей аналогии, он живет в центре города. Все
остальное — актеры второго плана. ЦП состоит из множества отдельных частей,
о которых мы узнаем в этом разделе.

Арифметико-логическое устройство
Арифметико-логическое устройство (АЛУ) — одна из основных частей ЦП. Оно
умеет выполнять арифметические операции, операции булевой алгебры и др.
На рис. 4.7 показана простая схема АЛУ.
Операнды — это просто биты, которые могут представлять числа. Код операции — это число, которое сообщает АЛУ, какой оператор применять

Центральный процессор   141
к операндам. Результат, конечно же, получается, когда мы применяем оператор к операндам.
Результат
Операнд А
АЛУ
Операнд В

Коды состояний

Код операции

Рис. 4.7. Пример АЛУ
Коды состояний содержат дополнительную информацию о результате. Обычно
они хранятся в регистре кодов состояния. Регистр, который мы упоминали еще
в главе 3, — это просто особая часть памяти, которая располагается отдельно от
других — на улице с дорогими домами нетиповой застройки. Типичный регистр
кода состояния показан на рис. 4.8. Цифры над прямоугольниками — это номера
битов, что удобно для их обозначения. Обратите внимание, что некоторые биты
не используются; в этом нет ничего необычного.
7

6

5

4

3

2

1

0

N

Z

O

Отрицательный (Negative)
Нуль (Zero)
Переполнение (Overflow)

Рис. 4.8. Регистр кода состояния
N устанавливается в 1, если результат последней операции — отрицательное
число. Бит Z устанавливается в 1, если этот результат равен 0. Бит O устанавливается в 1, если результат последней операции привел к переполнению или
потере значимости.
Таблица 4.1 показывает, что может делать АЛУ.
АЛУ может показаться странным, но на самом деле оно представляет собой лишь
несколько логических элементов, управляющих селектором, который мы уже
рассматривали. На рис. 4.9 показана общая конструкция АЛУ (для простоты
мы убрали некоторые более сложные функции).

142   Глава 4. Анатомия компьютера
Таблица 4.1. Примеры кодов операций АЛУ
Код операции

Мнемокод

Описание

0000

clr

Игнорировать операнды; сделать каждый бит результата
равным 0 (очистить)

0001

set

Игнорировать операнды; сделать каждый бит результата
равным 1

0010

not

Игнорировать B; превратить нули из A в 1 и наоборот

0011

neg

Игнорировать B; получить в результате дополнение до двух
для A, –A

0100

shl

Сдвиг A влево на младшие 4 бита B (см. следующий раздел)

0101

shr

Сдвиг A вправо на младшие 4 бита B (см. следующий раздел)

0110

Не используется

0111

Не используется

1000

load

Передать операнд B в результат

1001

and

Результат равен A И B для каждого бита операндов

1010

or

Результат равен A ИЛИ B для каждого бита операндов

1011

xor

Результат равен A исключающее ИЛИ B для каждого бита
операндов

1100

add

Результат равен A + B

1101

sub

Результат равен A – B

1110

cmp

Установить коды состояний на основе B – A (сравнить)

1111

Не используется

0

clr

1

set
not

A
B
A
B
A
B

Результат

load
and
or
Код операции

Рис. 4.9. Часть внутреннего устройства АЛУ

Центральный процессор   143

Сдвиг
Возможно, вы заметили операции сдвига в табл. 4.1. Сдвиг влево перемещает
каждый бит на одну позицию влево, отбрасывая крайний левый бит и перемещая 0 в освободившуюся крайнюю правую позицию. Если сдвинуть 01101001
(10510) на 1 влево, мы получим 11010010 (21010). Это очень удобно, потому что
сдвиг влево позиции номер один умножает ее на 2.
Сдвиг вправо перемещает каждый бит вправо на одну позицию, отбрасывая
крайний правый бит и перемещая 0 в освободившееся крайнее левое положение.
Если сдвинуть 01101001 (10510) на 1 вправо, мы получим 00110100 (5210). Это
эквивалентно делению числа на 2 и отбрасыванию остатка.
Значение MSB (наибольшего значащего бита), потерянного при сдвиге влево,
или LSB (наименьшего значащего бита) — при сдвиге вправо, часто требуется
сохранить, для этого и нужен регистр кода состояния. Представим, что наш ЦП
сохраняет его в бите O.
Вы могли заметить, что, похоже, все элементы АЛУ могут быть реализованы
с помощью комбинаторной логики, за исключением этих инструкций сдвига.
Можно создать регистры сдвига из триггеров, где содержимое сдвигается на
одну битовую позицию за такт.
Последовательный регистр сдвига (показанный на рис. 4.10) работает медленно,
потому что в худшем случае ему требуется один такт на бит.
C0
D Q

C1
D Q

Cn
D Q ...

D Q

генератор
тактовых сигналов

Рис. 4.10. Последовательный регистр сдвига
Можно решить эту проблему, создав устройство быстрого (циклического) сдвига
полностью на комбинаторной логике, используя один из ранее рассмот­ренных
логических строительных блоков — селектор (см. рис. 2.47). Чтобы построить
8-битное устройство циклического сдвига, нам понадобится восемь селекторов 8 : 1.
На каждый бит имеется один селектор, как показано на рис. 4.11.
Величина сдвига вправо указана на S0-2. Видим, что без сдвига (000 для S)
входной бит 0 (I0) передается в выходной бит 0 (O0), I1 в O1 и т. д. Когда S равно
001, выходы сдвигаются вправо на единицу, потому что таким образом входы

144   Глава 4. Анатомия компьютера
подключаются к селектору. Когда S равно 010, выходы сдвигаются вправо на
два и т. д. Другими словами, мы имеем все восемь возможностей и просто выбираем необходимую.
I0
0
0
0
0
0
0
0

Селектор
8:1

A0 A1 A2

O0

I1
I0
0
0
0
0
0
0

Селектор
8:1

A0 A1 A2

O1

I2
I1
I0
0
0
0
0
0

Селектор
8:1

A0 A1 A2

O2

...

I7
I6
I5
I4
I3
I2
I1
I0

Селектор
8:1

O7

A0 A1 A2

S0
S1
S2

Рис. 4.11. Комбинаторное многорегистровое устройство циклического сдвига
Вам может быть интересно, почему я продолжаю показывать эти логические схемы, как будто они построены из старых деталей серии 7400. Такие функции, как
вентили, селекторы, демультиплексоры, сумматоры, триггеры и т. д., доступны
как предопределенные компоненты в системах проектирования интегральных
схем. Они используются так же, как и старые компоненты, за исключением того,
что вместо размещения множества деталей серии 7400, о которых я упоминал
в главе 2, на плате теперь мы собираем аналогичные компоненты в одну микросхему с помощью проектировочного программного обеспечения.
Возможно, вы заметили, что в нашем простом АЛУ нет операций умножения
и деления. Это потому, что они намного сложнее, но на самом деле не показывают нам ничего нового. Вы знаете, что умножение можно производить
повторным сложением — это его последовательная версия. Можно также построить комбинаторный множитель путем каскадного наслаивания устройств
циклического сдвига и сумматоров, имея в виду, что сдвиг влево умножает
число на 2.
Устройства сдвига — ключевой элемент в реализации арифметики с плавающей
точкой; экспоненты используются для сдвига мантисс, чтобы выровнять двоичные точки, — тогда числа можно будет складывать, вычитать и т. д.

Исполнительное устройство
Исполнительное устройство компьютера, также известное как управляющий
блок, — вот настоящий начальник. В конце концов, от АЛУ мало пользы — ему
нужно указывать, что делать. Исполнительное устройство захватывает коды
операций и операнды из нужных мест в памяти, сообщает АЛУ, какие операции

Центральный процессор   145
выполнять, и помещает результаты обратно в память. Надеюсь, он делает все это
в порядке, который служит полезной цели. (Кстати, мы используем значение
«выполнить задачу» для термина «execute» («выполнить»). На самом деле мы
не убиваем биты1.)
Как исполнительное устройство может это сделать? Мы даем ему список
инструкций, таких как «добавьте число из ячейки 10 к числу из ячейки 12
и поместите результат в ячейку 14». Где исполнительный модуль находит эти
инструкции? В памяти! Техническое название того, что мы используем, — компьютер с хранимой программой. Он был создан благодаря работе гениального
английского ученого Алана Тьюринга (1912–1954).
Да, мы получили еще один способ рассматривать и интерпретировать биты.
Инструкции — это битовые комбинации, которые говорят компьютеру, что
делать. Битовые комбинации являются частью конструкции конкретного процессора. Они не имеют общего стандарта, например представления с помощью
чисел, поэтому процессор Intel Core i7, вероятно, будет иметь другую битовую
комбинацию для инструкции inc A, чем процессор ARM Cortex-A.
Как исполнительное устройство узнает, где искать инструкцию в памяти? Оно
использует счетчик команд (часто сокращается как PC, от program counter),
который похож на почтальона или на большую стрелку с надписью «Вы здесь».
Как показано на рис. 4.12, счетчик команд — это еще один регистр, одна из
тех частей памяти, которые располагаются на особой «улице». Он состоит из
счетчика (см. «Счетчики» на с. 119), а не простого регистра (см. «Регистры» на
с. 121). Счетчик можно рассматривать как регистр с дополнительной функцией
подсчета.
...
7
6
5
4
3
2
1
0

0
Счетчик команд

Память

Рис. 4.12. Счетчик команд

1

Глагол «execute» также переводится как «казнить». — Примеч. пер.

146   Глава 4. Анатомия компьютера
Счетчик команд содержит адрес памяти. Другими словами, он указывает, или
ссылается, на место в памяти. Исполнительное устройство выбирает инструкцию из места, на которое указывает счетчик команд. Существуют специальные
инструкции, которые изменяют значение этого счетчика, — вскоре мы их
увидим. Если ни одна из таких инструкций не выполняется, счетчик команд
увеличивается (к нему добавляется размер одной инструкции) после выполнения каждой инструкции, так что следующая инструкция будет поступать из
следующей ячейки памяти. Обратите внимание, что при включении питания
ЦП получают некоторое начальное значение счетчика программ, обычно 0.
Счетчик, который мы видели на рис. 3.17, имеет входы для поддержки всех
этих функций.
Все это работает как охота за сокровищами. Компьютер переходит в определенное место в памяти и находит записку. Он читает эту записку, в которой
говорится, что нужно что-то сделать, а затем переходит в другое место, чтобы
получить следующую записку, и т. д.

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

Инструкции
Чтобы увидеть, какие инструкции можно найти в ЦП и как для них подбираются битовые комбинации, предположим, что компьютер использует 16-битные
инструкции.
Попробуем разделить инструкцию на четыре поля — код операции, адреса для
двух операндов и результат, — как показано на рис. 4.13.
15 14 13 12 11 10
Код операции

9

8

Операнд В

7

6

5

4

Операнд А

3

2

1

0

Результат

Рис. 4.13. Схема трехадресной инструкции
Это выглядит хорошей идеей, но на практике не все так гладко. Почему?
Потому что у нас есть место только для 4 бит адреса для каждого из операндов и результата. Имея 16 адресов, довольно сложно адресовать полезный
объем памяти. Можно увеличить размер инструкции, но даже если перейти
к 64-битным инструкциям, получится всего 20 бит адреса, занимающих
только один мебибайт памяти. Современные компьютеры насчитывают
гибибайты памяти.

Набор инструкций   147
Другой подход — это повтор трюка с адресацией DRAM, который мы видели на
рис. 3.23. Можно иметь регистр расширения адреса и загружать его старшими
битами адреса с помощью отдельной инструкции. Этот метод использовала Intel,
чтобы ее 32-битные компьютеры получали доступ к более чем 4 ГиБ памяти. Intel
назвала это PAE — физическое расширение адреса (от physical address extension).
Конечно, для загрузки этого регистра требуется дополнительное время и много
повторных загрузок регистров, если нам нужна память по обе стороны от границы, созданной таким образом.
Однако есть еще более важная причина, по которой трехадресный формат не работает: он рассчитывает на некую волшебную, несуществующую форму памяти,
которая позволяет одновременно обращаться к трем разным адресам. Все три
блока памяти на рис. 4.14 представляют собой одно и то же устройство памяти;
не существует трех адресных шин и трех шин данных.
Память

Память
АЛУ
Память
Регистр кодов состояния

Инструкция
Код операции

Операнд В

Операнд А

Результат

Рис. 4.14. Неработоспособная компьютерная архитектура
Эта архитектура будет работать, если один регистр будет хранить содержимое
операнда A, а другой — содержимое операнда B. Аппаратному обеспечению
нужно в таком случае сделать следующее:
1. Загрузить инструкцию из памяти, используя адрес из счетчика команд.
2. Загрузить регистр операнда A, используя адрес из части инструкции для
операнда A.
3. Загрузить регистр операнда B, используя адрес из части инструкции для
операнда B.
4. Сохранить результат в памяти, используя адрес из части инструкции для
результата.
Получается слишком сложное решение. Если каждый из этих шагов займет
один тактовый цикл, потребуются четыре цикла, чтобы что-то сделать. Следует

148   Глава 4. Анатомия компьютера
учесть тот факт, что можно получить доступ только к одной области памяти за
раз, и соответствующим образом спроектировать набор инструкций. Больше
битов адреса будет доступно, если адресовать лишь что-то одно за раз.
Это можно сделать, добавив еще один дом на регистровую улицу. Назовем этот
регистр аккумулятором, или для краткости регистром A, и он будет хранить
результат, полученный от АЛУ. Вместо того чтобы выполнять операцию между
двумя ячейками памяти, будем делать это между одной из ячеек памяти и аккумулятором. Конечно, нужно будет добавить инструкцию сохранения, которая
помещает содержимое аккумулятора в ячейку памяти. Итак, теперь можно расставить инструкции, как показано на рис. 4.15.
15 14 13 12 11 10

9

8

7

Код операции

6

5

4

3

2

1

0

Адрес

Рис. 4.15. Схема одноадресной инструкции
Это дает больше битов адреса, но для работы требуется еще больше инструкций.
Раньше мы использовали инструкцию, в которой говорилось:
C = A + B.
Вместо этого появились три инструкции:
Аккумулятор = A.
Аккумулятор = Аккумулятор + B.
C = Аккумулятор.
Вы могли заметить, что мы просто заменили одну инструкцию тремя и в результате противоречим самим себе, потому что это фактически увеличило размер
инструкции. Так и есть для этого простого случая, но в целом мы все-таки получаем некоторую выгоду. Допустим, нужно вычислить такую сумму:
D = A + B + C.
Это невозможно сделать с помощью одной инструкции, даже если она получит
доступ к трем адресам, потому что теперь нам требуются четыре адреса. Мы бы
выполнили сложение таким образом:
Промежуточный результат = A + B.
D = Промежуточный результат + C.
Если учитывать 12-битный адрес, нам понадобятся 40-битные инструкции
для обработки трех адресов и кода операции. Также нам нужны две из этих

Набор инструкций   149
инструкций — 80 бит — для вычисления D. Если использовать одноадресную
версию инструкций, достаточно четырех инструкций и 64 бит.
Аккумулятор = A.
Аккумулятор = Аккумулятор + B.
Аккумулятор = Аккумулятор + C.
D = Аккумулятор.

Режимы адресации
С помощью аккумулятора нам удалось получить 12 бит адреса. Адресовать
4096 байт намного лучше, чем 16, но и этого недостаточно. Этот способ адресации
памяти известен как прямая адресация, что означает просто адрес, указанный
в инструкции.
Можно увеличить объем памяти, добавив косвенную адресацию. В этом случае
мы получаем адрес из ячейки памяти, содержащейся в инструкции, а не из
самой инструкции непосредственно. Например, предположим, что ячейка памяти 12 содержит значение 4321, а ячейка памяти 4321 содержит 345. Если бы
мы использовали прямую адресацию, то получили бы 4321 из ячейки 12, а при
косвенной адресации — 345, содержимое ячейки 4321.
Это подходит для работы с памятью, но иногда нам просто нужно получить постоянные числа. Например, чтобы сосчитать до 10, нам нужен способ загрузить
это число. Мы можем сделать это с помощью еще одного режима адресации, называемого немедленной адресацией. При этом адрес обрабатывается просто как
число, поэтому в предыдущем примере при загрузке 12 в немедленном режиме
мы получим 12. На рис. 4.16 сравниваются эти режимы адресации.
Немедленная
Код
операции

12

Прямая
Код
операции

345

Косвенная
Код
операции

4321

12
Память
12

12

Память

Память

345

12

12

Рис. 4.16. Режимы адресации
По рисунку понятно, что немедленная адресация быстрее, чем прямая, для которой нужен повторный доступ к памяти, а косвенная происходит еще дольше,
так как требует дополнительного доступа к памяти.

150   Глава 4. Анатомия компьютера

Инструкции кода состояния
Нашему процессору все еще кое-чего не хватает — например, инструкций, которые работают с кодами состояния. Мы видели, что эти коды устанавливаются
путем сложения, вычитания и сравнения. Но нам нужны способы задать для них
известные значения и получить новые. Для этого нам пригодятся дополнительные инструкции cca (копирует содержимое регистра кода состояния в аккумулятор) и acc (копирует содержимое аккумулятора в регистр кода состояния).

Ветвление
Теперь у нас есть инструкции, которые могут делать все, что угодно, но мы можем
только выполнять весь список инструкций от начала до конца. Толку немного.
Нам бы пригодились программы, которые принимают решения и выбирают
части кода для выполнения. Они бы получали инструкции, меняющие значение
счетчика команд. Такие инструкции называются инструкциями ветвления и вызывают загрузку счетчика команд с новым адресом. Ветвление само по себе не
полезнее возможности выполнить список инструкций. Однако оно выполняется
не всегда — инструкции проверяют коды состояний и выполняются только в том
случае, если условия истинны. В противном случае счетчик команд обычно
увеличивается на единицу, и следующей выполняется инструкция, идущая за
инструкцией ветвления. Инструкциям ветвления нужно дополнительно несколько битов для хранения условия, как показано в табл. 4.2.
Таблица 4.2. Условия инструкции ветвления
Код

Мнемокод

Описание

000

bra

Всегда выполняется

001

bov

Выполняется, если установлен бит кода состояния O (переполнение)

010

beq

Выполняется, если установлен бит кода состояния Z (нуль)

011

bne

Выполняется, если сброшен бит кода состояния Z

100

blt

Выполняется, если установлен бит кода состояния N (отрицательный),
а Z сброшен

101

ble

Выполняется, если установлены N или Z

110

bgt

Выполняется, если N и Z сброшены

111

bge

Выполняется, если N сброшен или установлен Z

Иногда требуется явно изменить содержимое счетчика команд. Для этого есть
две специальные инструкции: pca (копирует текущее значение счетчика команд
в аккумулятор) и apc (копирует содержимое аккумулятора в счетчик команд).

Набор инструкций   151

Итоговый набор инструкций
Соберем все эти функции в итоговый набор инструкций, как показано на
рис. 4.17.
15 14 13 12 11 10
Режим

9

8

7

Код операции

6

5

4

3

2

1

0

Адрес

Рис. 4.17. Окончательная схема инструкции
Мы рассмотрели три режима адресации, а это значит, что нам нужно 2 бита для
выбора режима. Неиспользуемая четвертая битовая комбинация применяется
для операций, не связанных с памятью.
Режим адресации и код операции преобразуются в инструкции, как видно из
табл. 4.3.
Таблица 4.3. Режимы адресации и коды операций
Код
операции

Адресация
Прямая (00)

Косвенная (01)

Немедленная (10)

0000

load

load

load

0001

and

and

and

set

0010

or

or

ore

not

0011

xor

xor

xor

neg

0100

add

add

add

shl

0101

sub

sub

sub

shr

0110

cmp

cmp

cmp

acc

0111

store

store

cca

1000

bra

bra

bra

apc

1001

bov

bov

bov

pca

1010

beq

beq

beq

1011

bne

bne

bne

1100

blt

blt

blt

1101

ble

bge

ble

1110

bgt

bgt

bgt

1111

bge

bge

bge

Не указан (11)

152   Глава 4. Анатомия компьютера
Обратите внимание, что условия ветвления объединены с кодами операций. Коды
операций для режима адресации 3 используются для операций, в которых задействован только аккумулятор. Побочным эффектом полной реализации является
то, что коды операций не совсем соответствуют АЛУ, показанному в табл. 4.1.
В этом нет ничего необычного — просто требуется дополнительная логика.
Инструкции сдвига влево и вправо используют незанятые биты для подсчета
количества сдвигаемых позиций, как показано на рис. 4.18.
15 14 13 12 11 10

9

8

7

6

5

4

3

2

1

0

1

1

0

1

0

0

Счетчик

shl

1

1

0

1

0

1

Счетчик

shr

Рис. 4.18. Схема инструкции сдвига
Теперь можно фактически указать компьютеру что-то сделать, написав программу,
которая представляет собой просто список инструкций, выполняющих некоторую
задачу. Вычислим все числа Фибоначчи (итальянский математик, 1175–1250)
до 200. Числа Фибоначчи — интересная вещь; количество лепестков на цветках,
например, — это тоже числа Фибоначчи. Первые два числа Фибоначчи — это 0
и 1. Следующее число в последовательности получается путем сложения двух
предыдущих: 0, 1, 1, 2, 3, 5, 8, 13 и т. д. Процесс подсчета показан на рис. 4.19.
Начало

Задать первое число равным 0

Задать второе число равным 1

Задать третье число равным сумме первого ивторого

Задать первое число равным второму

Задать второе число равным третьему

Да

Третье число меньше 200?
Нет
Конец

Рис. 4.19. Блок-схема программы для вычисления последовательности Фибоначчи

Окончательный проект   153
Короткая программа, представленная в табл. 4.4, реализует этот процесс. Столбец
с инструкциями разделен на поля, как показано на рис. 4.17. Адреса в комментариях представляют собой десятичные числа.
Таблица 4.4. Программа на машинном языке для вычисления
последовательности Фибоначчи
Адрес

Инструкция

Описание

0000

10 0000 0000000000

Очистить аккумулятор (немедленно загрузить 0)

0001

00 0111 0001100100

Сохранить аккумулятор (0) в ячейке памяти 100

0010

10 0000 0000000001

Загрузить 1 в аккумулятор (немедленно загрузить 1)

0011

00 0111 0001100101

Сохранить аккумулятор (1) в ячейке памяти101

0100

00 0000 0001100100

Загрузить аккумулятор из ячейки памяти 100

0101

10 0100 0001100101

Добавить содержимое ячейки памяти 101 в аккумулятор

0110

00 0111 0001100110

Сохранить аккумулятор в ячейке памяти 102

0111

00 0000 0001100101

Загрузить аккумулятор из ячейки памяти 101

1000

00 0111 0001100100

Сохранить его в ячейке памяти 100

1001

00 0000 0001100110

Загрузить аккумулятор из ячейки памяти 102

1010

00 0111 0001100101

Сохранить его в ячейке памяти 101

1011

10 0110 0011001000

Сравнить содержимое аккумулятора с числом 200

1100

00 0111 0000000100

Получить новое число, если последнее было меньше
200, путем перехода к адресу 4 (0100)

Окончательный проект
Объединим все, чему мы научились, в настоящий компьютер. Нам понадобится
немного «клея», чтобы все заработало.

Регистр команд
Можно подумать, что компьютер просто выполняет
программу Фибоначчи по одной инструкции за раз,
но это не так. За кулисами происходит еще кое-что.
Что нужно компьютеру для выполнения инструкции?
Конечный автомат выполняет действия в два этапа,
показанные на рис. 4.20.

Получить

Выполнить

Рис. 4.20. Цикл
получения-выполнения

154   Глава 4. Анатомия компьютера
Первое, что нужно сделать, — извлечь инструкцию из памяти. Только получив
инструкцию, можно перейти к ее выполнению.
Выполнение инструкций обычно включает доступ к памяти. Это означает, что
нужно хранить инструкцию где-то рядом, пока память используется для другой задачи. На рис. 4.21 добавим в ЦП регистр команд для хранения текущей
инструкции.
Счетчик команд

Адресная шина

Память

Шина данных

Регистр команд

Рис. 4.21. Добавление регистра команд

Передача данных и управляющие сигналы
Мы подошли к сложной части. Нам нужен способ передать содержимое счетчика
команд в адресную шину памяти и данные памяти в регистр команд. Можно
выполнить аналогичное упражнение, чтобы определить все связи, необходимые
для реализации полного набора команд, как подробно описано в табл. 4.4. В итоге получим рис. 4.22, который, вероятно, сбивает с толку. Но на самом деле он
просто объединяет все, что мы видели раньше: несколько регистров, селекторов,
АЛУ и буферный элемент с тремя состояниями.
Несмотря на то что схема выглядит довольно сложно, обратите внимание на ее
сходство с картой. Да, эта схема даже проще, чем карта города. Селектор адреса —
это просто трехсторонний перекресток, а селектор данных — четырехсторонний.
От адресной шины и шины данных отходят соединения для таких инструментов,
как устройства ввода/вывода, которые мы обсудим в главе 6.
Единственный новый компонент — это регистр косвенных адресов. Он необходим
для хранения косвенных адресов, извлеченных из памяти, аналогично тому, как
регистр команд хранит извлеченные из памяти команды.
Для простоты на рис. 4.22 опущен системный генератор тактовых сигналов,
который используется для всех регистров и памяти. В случае с обычным регистром просто предположим, что регистр загружается по следующему тактовому
сигналу, если генератор включен. Точно так же счетчик команд и память выполняют то, что их управляющие сигналы говорят им делать на каждом такте. Все
остальные компоненты, такие как селекторы, являются чисто комбинаторными
и не используют генераторы сигналов.

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

Окончательный проект   155

Счетчик команд

0

enable

clear enable Id/cnt

1

Регистр косвенных адресов

Адресная шина

enable

Регистр команд

Шина данных

r/w

2

enable

Память

address source
instruction

0
1
2

Операнд В

Коды
состояния

Регистр кодов состояния

3

АЛУ
data source

Операнд А

Результат

enable

Аккумулятор

opcode
enable
opcode

Шина данных
enable

Рис. 4.22. Передача данных и управляющие сигналы (clear — сброс,
enable — включение, Ld/cnt — загрузка/счетчик, r/w — чтение/запись,
address source — источник адресов, data source — источник данных,
instruction — команда, opcode — код операции)
Выборка данных — общая для всех инструкций. Речь идет о следующих сигналах:
address source должен быть установлен для выбора счетчика команд;

память должна быть активирована, а сигнал r/w должен быть установлен
на чтение (1);
регистр команд должен быть включен.

156   Глава 4. Анатомия компьютера
В следующем примере мы сохраним содержимое аккумулятора по адресу памяти,
на который указывает адрес из инструкции, — то есть используем косвенную
адресацию. Выборка работает так же, как раньше.
Получение косвенного адреса из памяти:
address source должен быть настроен на выбор регистра инструкции,

который получает адресную часть инструкции;

память включена, а сигнал r/w установлен на чтение (1);
включен регистр косвенных адресов.
Сохранение аккумулятора в этом адресе:
address source должен быть настроен на выбор регистра косвенных

адресов;

шина данных должна быть enable;
память включена, а сигнал r/w установлен на запись (0);
счетчик команд увеличивается.
Поскольку выборка и выполнение инструкций выполняются в несколько шагов, нам нужен счетчик для их отслеживания. Содержимое счетчика, а также
части кода операции и режима в команде — все, что нам нужно для генерации
всех управляющих сигналов. Нам понадобятся два бита счетчика, потому что
для выполнения самых сложных инструкций необходимы три состояния, как
показано на рис. 4.23.

Сброс

Генератор
тактовых сигналов

Счетчик

Счетчик команд clear
Счетчик команд enable
Счетчик команд ld/cnt
Адрес команд enable
address source

Случайная
логика

instruction

Регистр команд enable
Память r/w
Память enable
opcode

Регистр кодов состояния enable
Регистр аккумулятора enable
Шина данных enable
data source

Рис. 4.23. Управление движением со случайной логикой

Окончательный проект   157
Это большая коробка, полная так называемой случайной логики. Все логические
схемы, рассмотренные ранее, следуют определенному шаблону. Функциональные блоки, такие как селекторы и регистры, собираются из более простых блоков.
Иногда, например при реализации блока управления движением, имеется набор
входов, которые необходимо сопоставить с набором выходов для выполнения
нерегулярной задачи. Схема выглядит как крысиное гнездо соединений — потому она и называется «случайной».
Однако существует еще один способ реализовать блок управления движением.
Вместо случайной логики можно использовать кусок памяти. Адрес будет формироваться из выходов счетчика и части кода операции и режима в команде,
как показано на рис. 4.24.

Opcode

Режим

счетчик 0

A0

D0

Счетчик clear

счетчик 1

A1

instruction10

A2

D1
D2
D3
D4

Счетчик команд clear
Адрес команд enable
Счетчик команд ld/cnt
Косвенный адрес enable

instruction11

A3

instruction12

A4

instruction13

A5

instruction14

A6

instruction15

A7

256x19
Память

address source

D 5–6
D7

Регистр команд enable

Память r/w
Память enable

D8
D9

opcode

D 10–13
D 14
D 15
D 16
D 17–18

Регистр кодов состояния enable
Регистр аккумулятора enable
Шина данных enable
data source

Рис. 4.24. Управление движением на основе памяти
Каждое 19-битное слово в памяти расположено, как показано на рис. 4.25.
DS 1

DS 0

DBE ARE CCRE OP 3

18

17

16

15

14

13

OP 2

12

OP 1

11

OP 0

10

ME

R/W

IRE

9

8

7

LD/
CNT

AS 1

AS 0

IAE

PCE

PCC

CC

6

5

4

3

2

1

0

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

158   Глава 4. Анатомия компьютера
Такой тип реализации называется микрокодированием, а содержимое памяти называется микрокодом. Да, мы используем маленький компьютер как часть большого.
Разберем часть микрокоманд, показанную на рис. 4.26, — она подходит для реализации рассмотренных ранее примеров.
1
DS

0
DS

E
DB

E
AR

RE P
CC O

3

P

O

2

P

O

1

P

O

0

M

W
E
R/
IR

E

LD

/

T

CN

AS

1

AS

0

E

IA

E

PC

C

PC

CC

Хранение

0

0

1

0

0

0

0

0

0

1

0

0

0

0

1

0

1

0

1

Косвенная
адресация

0

0

0

0

0

0

0

0

0

1

1

0

0

0

1

1

0

0

0

0

0

0

0

0

0

0

0

0

1

1

0

0

1

0

1

0

0

0

18

17 16 15 14 13 12 11 10

9

8

7

6

5

4

3

2

1

0

Получение

Рис. 4.26. Пример микрокода
Как и следовало ожидать, хорошей идеей легко злоупотребить. Существуют
машины с нанокодированным блоком, который реализует микрокодированный
блок, который, в свою очередь, реализует набор команд.
Использование ПЗУ для памяти микрокода имеет определенный смысл, потому
что в противном случае нам пришлось бы хранить копию микрокода в другом
месте и для загрузки микрокода потребовалось бы дополнительное аппаратное
обеспечение. Однако бывают ситуации, когда использование ОЗУ или сочетания
ПЗУ и ОЗУ оправданно. Некоторые процессоры Intel имеют записываемый
микрокод, который можно изменять, чтобы исправлять ошибки. Некоторые
машины, например из серии HP-2100, имели перезаписываемое управляющее
запоминающее устройство, которое представляло собой ОЗУ микрокода — его
можно было использовать для расширения базового набора команд.
Даже если машины имеют перезаписываемый микрокод, доступ к нему для
пользователей обычно закрыт. Производители не заинтересованы в том, чтобы
пользователи зависели от этого микрокода при написании своих приложений,
так как в этом случае производителям будет сложно вносить в него изменения.
Кроме того, микрокод с ошибками может повредить машину — например, одновременно включить и память, и шину данных в ЦП, соединив вместе каскадные
выходы таким образом, что транзисторы перегорят.

Наборы команд RISC и CISC
Проектировщики писали для компьютеров команды, казавшиеся полезными, но
приводившие к созданию довольно сложных машин. В 1980-х годах американские
ученые-компьютерщики Дэвид Паттерсон (David Patterson) из Беркли и Джон
Хеннесси (John Hennessey) из Стэнфорда провели статистический анализ программ и обнаружили, что многие сложные команды использовались редко. Они

Наборы команд RISC и CISC   159
первыми разработали машины, содержащие только часто используемые программами инструкции; менее используемые были удалены и заменены комбинациями
других команд. Такие устройства были названы RISC-машинами — компьютерами с сокращенным набором команд (от reduced instruction set computer). Старые
разработки получили название CISC-машин — компьютеров со сложным набором
команд (от complicated instruction set computer).
Одной из отличительных черт RISC-машин является то, что они имеют архитектуру типа «загрузка — сохранение». Это означает, что в них используется две категории инструкций: одна для доступа к памяти, а вторая — для всего остального.
Конечно, со временем использование компьютеров вышло на новый уровень.
Первоначальные статистические исследования Паттерсона и Хеннесси были проведены до того, как компьютеры стали широко использоваться для таких вещей,
как прослушивание аудио и просмотр видео. Статистика по более новым программам побуждает разработчиков добавлять новые инструкции для RISC-машин.
Современные RISC-машины на самом деле сложнее прежних CISC-машин.
Одной из машин CISC, оказавших большое влияние на индустрию, была PDP-11
от Digital Equipment Corporation. В ней было восемь универсальных регистров
вместо единственного аккумулятора, который мы использовали в примерах.
Эти регистры могут использоваться для косвенной адресации. Кроме того, поддерживаются режимы автоинкремента и автодекремента для увеличения или
уменьшения значений в регистрах до или после использования. Это позволило
создать несколько очень эффективных программ. Например, предположим, что
нужно скопировать n байт памяти, начиная с адреса источника, в память, начиная
с адреса назначения. Можно поместить адрес источника в регистр 0, адрес назначения в регистр 1 и счетчик в регистр 2. Мы не будем рассматривать все используемые биты, потому что нет никакой реальной необходимости изу­чать полный
набор инструкций PDP-11. Таблица 4.5 показывает, что делают эти команды.
Таблица 4.5. Программа копирования памяти PDP-11
Адрес

Описание

0

Скопировать содержимое ячейки памяти, адрес которой находится в регистре 0,
в ячейку памяти, адрес которой находится в регистре 1, затем добавить 1 к каждому регистру

1

Вычесть 1 из содержимого регистра 2, сравнить результат с 0

2

Перейти к ячейке 0, если результат не был равен 0

Почему мы вообще должны об этом задумываться? Язык программирования C,
являющийся продолжением языка B (который был продолжением BCPL),
был разработан на PDP-11. Использование указателей в языке C, абстракции

160   Глава 4. Анатомия компьютера
косвенной адресации более высокого уровня, в сочетании с функциями из B,
такими как операторы автоинкремента и автодекремента, хорошо сопоставляются с архитектурой PDP-11. C стал очень популярным и повлиял на проекты
многих других языков, включая C++, Java и JavaScript.

Графические процессоры
Вы, наверное, слышали о графических процессорах, или GPU (от graphics
processing unit). По большей части их изучение выходит за рамки данной книги, но я вкратце о них расскажу.
Графика — это раскраска картины по номерам в масштабе. Нет ничего необычного в том, чтобы нарисовать 8 миллионов цветных пятен и раскрашивать их
60 раз в секунду, если вам нужно получить качественное видео. Однако для этого
необходимо около полумиллиарда обращений к памяти в секунду.
Работа с графикой — это специализированная задача, которая не требует всех
функций универсального процессора. Ее легко можно распараллелить: рисование нескольких точек за раз может улучшить производительность.
Графические процессоры имеют две особенности. Во-первых, они включают
большое количество простых процессоров. Во-вторых, у них гораздо более
широкие шины памяти, чем у обычных процессоров, что означает, что они получают доступ к памяти намного быстрее. Графические процессоры используют
пожарный шланг вместо садового.
Со временем графические процессоры приобрели более универсальные функции. Были продуманы варианты стандартных языков программирования для
работы с графическими процессорами — теперь они используются для определенных классов приложений, которым доступны все преимущества своей
архитектуры. На момент написания этой книги графических процессоров на
рынке не хватало, потому что их раскупили для майнинга биткоинов.

Выводы
В этой главе мы создали настоящий компьютер, используя строительные блоки,
представленные в предыдущих главах. Несмотря на простоту схем, машину,
которую мы разработали, действительно можно построить и запрограммировать. Однако в ней отсутствуют некоторые элементы, используемые в реальных
компьютерах, — стеки и оборудование для управления памятью. Мы рассмотрим
их в главе 5.

5

Архитектура компьютера

В главе 4 был рассмотрен проект простой компьютерной системы, и мы обсудили, как ЦП взаимодействует
с памятью и устройствами ввода/вывода благодаря
адресным шинам и шинам данных. Однако это не конец
истории. За прошедшие годы компьютеры стали работать
быстрее благодаря множеству усовершенствований. При
этом они стали потреблять меньше энергии, а программировать
на них стало проще. Эти улучшения привели к значительному усложнению
архитектуры.
Архитектура компьютера означает расположение различных компонентов
в компьютере, а не то, есть ли у него дорические или ионические колонны либо
индивидуальный оттенок бежевого, как тот, который создал некий американский
предприниматель Стив Джобс (1955–2011) для оригинального компьютера
Macintosh. За прошедшие годы было испробовано множество различных архитектур. Что-то сработало, а что-то нет. Кроме того, эта тема широко освещалась
во многих книгах.
В этой главе основное внимание уделяется архитектурным улучшениям, связанным с памятью. Микрофотография современного микропроцессора показывает, что основная часть площади микросхемы отведена под обработку памяти.
Это настолько важно, что заслуживает отдельной главы. Мы также коснемся
некоторых других различий в архитектурах, таких как проектирование набора
команд, дополнительных регистров, управления питанием и более сложных
исполнительных устройств. И мы обсудим поддержку многозадачности — возможности запускать несколько программ одновременно (хотя бы иллюзорной).

162   Глава 5. Архитектура компьютера
Выполнение нескольких программ подразумевает существование некой управляющей программы — операционной системы (ОС), которая контролирует их
выполнение.

Основные архитектурные элементы
Две наиболее распространенные архитектуры — это архитектура фон Неймана
(названная в честь гениального венгеро-американского ученого Джона фон Неймана, 1903–1957) и гарвардская (названная в честь компьютера Harvard Mark I,
который, конечно же, был машиной с гарвардской архитектурой). Мы уже видели все их части по отдельности; на рис. 5.1 показано, как они организованы.
фон Неймана

гарвардская

АЛУ

АЛУ

ЦП
Память

ЦП

Исполнительное
устройство

Ввод/вывод

Память для
инструкций

Исполнительное
устройство

Память для
данных

Ввод/вывод

Рис. 5.1. Архитектуры фон Неймана и гарвардская
Обратите внимание, что единственное различие между ними — это способ
организации памяти. При прочих равных архитектура фон Неймана немного
медленнее, потому что она не может получать доступ к инструкциям и данным
одновременно, поскольку использует только одну шину памяти. Гарвардская
архитектура не имеет такого ограничения, но требует дополнительного аппаратного обеспечения для второй шины памяти.

Ядра процессора
Обе архитектуры на рис. 5.1 имеют один ЦП, который, как мы видели в главе 4,
представляет собой комбинацию АЛУ, регистров и исполнительного устройства. Многопроцессорные системы с несколькими процессорами появились
в 1980-х годах как способ получить более высокую производительность, чем
можно было бы достичь с помощью одного процессора. Однако, как оказалось,
это не так просто. Разделение одной программы так, чтобы ее можно было распараллелить для использования нескольких процессоров, — в целом до сих пор
не решенная задача, хотя некоторые варианты решений иногда работают хорошо,
например для определенных типов сложной математики. Тем не менее распараллеливание полезно, если запускается более одной программы одновременно. Оно

Основные архитектурные элементы   163
весьма пригодилось на заре создания графических рабочих станций, поскольку
система X Window была настолько ресурсоемкой, что для ее запуска требовался
отдельный процессор.
Уменьшение геометрических размеров процессоров снижает затраты. Микросхемы изготавливаются на кремниевых пластинах, а уменьшение размеров схем
означает, что на одной пластине помещается больше микросхем. Раньше более
высокая производительность достигалась за счет быстродействия процессора,
что означало увеличение тактовой частоты. Но более быстрые машины требовали большей мощности, что в сочетании с меньшими геометрическими размерами давало больше тепла на единицу площади. Процессоры достигли предела
мощности около 2000 года — удельную мощность невозможно было увеличить
без превышения температуры плавления.
В некотором роде спасение было найдено в меньших геометрических размерах.
Изменилось определение ЦП; то, что мы раньше называли ЦП, теперь называется ядром процессора. Многоядерные процессоры стали обычным явлением.
Существуют даже системы с несколькими многоядерными процессорами,
в основном в центрах обработки данных.

Микропроцессоры и микрокомпьютеры
Еще одно независимое архитектурное отличие заключается в механической
упаковке. На рис. 5.1 показаны процессоры, подключенные к памяти и вводу/
выводу. Когда память и ввод/вывод не находятся в том же физическом корпусе,
что и ядра процессора, мы называем это микропроцессором. Если же все встрое­
но в одну схему, мы используем термин микрокомпьютер. Это не совсем четко
определенные термины, и в их использовании есть некоторые пробелы. Некоторые считают микрокомпьютер компьютерной системой, построенной на базе
микропроцессора, и используют термин «микроконтроллер» для обозначения
того, что я только что определил как микрокомпьютер.
Микрокомпьютеры, как правило, менее мощны, чем микропроцессоры, потому
что такие вещи, как внутренняя память, занимают много места. В этой главе мы
не будем уделять много внимания микрокомпьютерам, потому что у них нет
сложных проблем с памятью. Однако, как только вы научитесь программировать, стоит взять в руки что-нибудь вроде Arduino — небольшого компьютера
с гарвардской архитектурой на базе микросхемы микрокомпьютера Atmel AVR.
Arduino отлично подходят для создания всевозможных игрушек и прочего.
Подводя итог: микропроцессоры обычно являются компонентами более крупных
систем, а микрокомпьютеры обычно применяются в чем-то более тривиальном —
например, в посудомоечных машинах.
Есть еще один вариант — интегральные, или встроенные, системы (system on
a chip, SoC). Приемлемое, но опять же нечеткое определение: SoC — это более

164   Глава 5. Архитектура компьютера
сложный микрокомпьютер. Вместо того чтобы иметь относительно простой
интегральный ввод/вывод, SoC может включать в себя, например, интерфейс
Wi-Fi. SoC используются в таких устройствах, как сотовые телефоны. Существуют даже SoC, которые включают программируемые пользователем вентильные
матрицы (ППВМ), которые можно дополнительно настроить.

Процедуры, подпрограммы и функции
Многие инженеры страдают странной разновидностью лени. Если они не хотят
совершать некое действие, они вложат всю свою энергию в создание того, что
выполнит задачу за них, даже если для этого потребуется больше работы, чем
для самого дела. Программисты хотят избежать повторного написания одного
и того же фрагмента кода. На это есть веские причины, помимо лени. Например,
код без повторов занимает меньше места, и, если в коде есть ошибка, ее нужно
исправить только один раз.
Функция (или процедура, или подпрограмма) является основой повторного использования кода. Для вас все эти термины означают одно и то же; это просто
региональные языковые различия. Мы будем использовать понятие «функция»,
потому что оно больше всего похоже на то, что вы, возможно, изучали на уроках
математики.
Конструкции в большинстве языков программирования похожи. Например,
в JavaScript можно написать код, показанный в листинге 5.1.
Листинг 5.1. Пример JavaScript-функции
function
cube(x)
{
}

return (x * x * x);

Этот код создает функцию cube, которая принимает единственный параметр
с именем x и возвращает его значение в кубе. На клавиатуре нет символа умножения (×), поэтому во многих языках программирования вместо умножения
используется *.
Теперь напишем фрагмент программы, как в листинге 5.2.
Листинг 5.2. Пример вызова JavaScript-функции
y = cube(3);

Приятная особенность функции заключается в том, что мы можем выполнять,
или вызывать, функцию cube несколько раз без необходимости писать ее снова.
Мы можем вычислить cube(4) + cube(6), не повторяя код возведения числа

Процедуры, подпрограммы и функции   165
в куб. Это простой пример, но подумайте, насколько удобна эта возможность
для более сложных фрагментов кода.
Как это работает? Нам нужен способ запустить код функции, а затем вернуться
к месту ее вызова. Чтобы вернуться, нам нужно знать, откуда мы пришли, —
эта информация хранится в счетчике команд (который мы рассматривали на
рис. 4.12 на с. 145). Таблица 5.1 показывает, как выполнить вызов функции,
используя пример набора инструкций из раздела «Набор инструкций» на с. 146.
Таблица 5.1. Выполнение вызова функции
Адрес

Инструкция

Операнд

Комментарии

100

pca

101

add

5 (немедленная
адресация)

Адрес для результата вычисления
(100 + 5 = 105)

102

store

200 (прямая адресация)

Хранит адрес возврата в памяти

103

load

3 (немедленная
адресация)

Помещает число для cube(3) в аккумулятор

104

bra

300 (прямая адресация)

Вызывает функцию cube

Счетчик команд -> аккумулятор

105

Продолжает выполнение с этого же места
после вызова функции


200

Зарезервированная ячейка в памяти


300





Напоминание о функции cube


310

Функция cube

bra

200 (косвенная
адресация)

Ответвление к сохраненному адресу возврата

Что тут происходит? Сначала мы вычисляем адрес, по которому нужно продолжить выполнение после возврата из функции cube. Для этого потребуется
несколько инструкций; кроме того, нужно загрузить число, которое будет возведено в куб. Это еще пять инструкций, поэтому мы сохраняем этот адрес в ячейке
памяти 200. Выполнение переходит к функции, а когда функция завершается,
выполнение косвенно переходит через ячейку 200, так что мы оказываемся
в ячейке 105. Этот процесс выполняется, как показано на рис. 5.2.

166   Глава 5. Архитектура компьютера

Сохранить адрес возврата
Загрузить аргумент
Перейти к функции
Продолжить после функции
функция cube
...
Адрес возврата

Косвенный переход (возврат)

Рис. 5.2. Поток вызова функции
Число действий слишком велико, особенно для повторного выполнения, поэтому многие машины добавляют вспомогательные инструкции. Например,
у процессоров ARM есть инструкция «перейти со ссылкой» (Branch with Link,
BL), которая объединяет переход к функции с сохранением адреса следующей
инструкции.

Стеки
Функции не ограничиваются простыми фрагментами кода, как в только что
рассмотренном примере. Функции обычно вызывают другие функции, а те вызывают сами себя.
Подождите-ка, что это сейчас было? Функция вызывает сама себя? Это называется рекурсией, и она действительно полезна. Рассмотрим пример. Ваш телефон, вероятно, использует сжатие JPEG (Joint Photographic Experts Group) для
уменьшения размера файлов фотографий. Чтобы увидеть, как работает сжатие,
начнем с квадратного черно-белого изображения на рис. 5.3.
7
6
5
4
3
2
1
0
0

1

2

3

4

5

6

7

Рис. 5.3. Незатейливый смайлик

Стеки   167
Можно решить проблему сжатия, используя рекурсивное деление: мы смотрим
на изображение и, если оно не одного цвета, делим его на четыре части, затем
снова проверяем и так далее, пока части не станут размером в один пиксель.
Листинг 5.3 представляет функцию subdivide, которая обрабатывает часть
изображения. Она написана на псевдокоде, языке программирования, похожем
на естественный язык, — он прекрасно подходит для примеров. Функция принимает координаты x и y левого нижнего угла квадрата вместе с размером (нам
не нужны и ширина, и высота, поскольку изображение квадратное). «Принимает» — это просто сокращение от того, что в математике называется передачей
аргументов в функцию.
Листинг 5.3. Функция subdivide
функция
subdivide(x, y, размер)
{
ЕСЛИ (размер ≠ 1 И пиксели в квадрате не имеют одинаковый цвет) {
половина = размер ÷ 2
subdivide(x, y, half)
нижний левый квадрант
subdivide(x, y + half, half)
верхний левый квадрант
subdivide(x + half, y + half, half) верхний правый квадрант
subdivide(x + half, y, half)
нижний правый квадрант
}
ИНАЧЕ {
сохранить данные о квадрате
}
}

Функция subdivide разделяет изображение на блоки одного цвета, начиная
с нижнего левого квадранта, затем верхнего левого, верхнего правого и, наконец,
нижнего правого. На рис. 5.4 серым цветом показаны объекты, которые необходимо разделить, и объекты одного цвета — черным или белым.

Рис. 5.4. Разделение изображения
То, что мы получили, похоже на то, что компьютерщики называют деревом, а математики — направленным ациклическим графом (НАГ). Следите за стрелками.
В этой структуре стрелки не идут вверх, поэтому невозможно получить цикл.
Узлы, из которых не выходят стрелки, называются концевыми, или листовыми,
узлами, и они обозначают конец линии, как листья — конец линии на ветке дерева. Если прищуриться и сосчитать их на рис. 5.4, можно увидеть, что сплошных

168   Глава 5. Архитектура компьютера
квадратов всего 40, а на исходном изображении — 64, то есть хранить нужно
меньше информации. Это и называется сжатием.
По какой-то причине, вероятно, из-за того что так легче рисовать (или потому что
они редко выходят на улицу), компьютерные гики всегда помещают корень дерева
наверху и направляют дерево вниз. Этот конкретный вариант называется квадродеревом, потому что каждый узел делится на четыре части. Квадродеревья — это
пространственные структуры данных. Ханан Самет (Hanan Samet) превратил
их в дело всей своей жизни и написал несколько отличных книг на эту тему.
Возникает проблема с реализацией функций, показанных в предыдущем разделе.
Поскольку имеется только одно место для хранения возвращаемого значения,
такие функции не могут вызывать сами себя, потому что это значение будет
перезаписано, а путь возврата потерян.
Нам нужно иметь возможность хранить несколько адресов возврата, чтобы
рекурсия работала. Также нам необходим способ связать адреса возврата с соответствующими вызовами функций. Посмотрим, можно ли найти закономерность
в том, как было разделено изображение. Мы спускались вниз по дереву вдоль
каждой ветви и возвращались назад, только когда путь вниз уже был невозможен.
Это называется обходом в глубину, в противоположность обходу в ширину, когда
путь начинается с определенной вершины, исследуются все ее соседи, а затем
происходит переход к вершинам следующего уровня. Каждый раз при спуске на
уровень ниже нужно помнить, откуда мы начали спуск, чтобы вернуться назад.
Как только мы вернемся, это место можно больше не хранить в памяти.
Нам нужно что-то вроде приспособлений, которые удерживают стопки тарелок
в кафетерии. Вызывая функцию, мы наклеиваем адрес возврата на тарелку и кладем ее в стопку. Возвращаясь, мы достаем эту тарелку. Иначе говоря, это стек. Есть
еще более умные слова: структура LIFO (last in, first out — «последним пришел —
первым ушел»). Объекты помещаются в стек и извлекаются из него. Попытка
поместить объекты в стек, в котором нет места, называется переполнением стека.
Попытка извлечь что-то из пустого стека — это исчерпание стека.
Все эти действия можно реализовать программно. В нашем предыдущем примере вызова функции в табл. 5.1 каждая функция может взять сохраненный
адрес возврата и поместить его в стек для последующего извлечения. К счастью,
большинство компьютеров имеют аппаратную поддержку стеков, потому что
стеки очень важны. Эта поддержка включает в себя регистры лимитов, чтобы
программе не приходилось постоянно проверять возможное переполнение.
В следующем разделе мы поговорим о том, как процессоры обрабатывают исключения, такие как превышение лимитов.
Стеки используются не только для адресов возврата. Наша функция subdivide
использовала локальную переменную, в которой мы один раз вычислили половину размера, а затем использовали ее восемь раз для ускорения программы.

Стеки   169
Невозможно просто перезаписывать ее каждый раз при вызове функции. Вместо
этого локальные переменные также хранятся в стеке. Это делает каждый вызов
функции независимым от вызовов других функций. Набор объектов, хранящихся в стеке для каждого вызова, представляет собой фрейм стека. На рис. 5.5
показан пример функции из листинга 5.3.
Стек
7

subdivide(0, 0, 8) адрес возврата

6

половина = 4

5

subdivide(4, 4, 4) адрес возврата

4

половина = 2

3

subdivide(4, 4, 2) адрес возврата

2

половина = 1

1

subdivide(5, 5, 1) адрес возврата

0
0

1

2

3

4

5

6

7

Рис. 5.5. Фреймы стека
Мы идем по пути, обозначенному жирными черными квадратами. Видим, что
каждый вызов генерирует новый фрейм стека, который включает как адрес возврата, так и локальную переменную.
Некоторые компьютерные языки, например forth и PostScript, основаны на стеке
(см. врезку «Различные представления уравнений»), как и несколько классических калькуляторов HP.
Стеки не ограничиваются компьютерными языками. Японский язык основан
на стеке: существительные помещаются в стек, а глаголы воздействуют на них.
Загадочные высказывания магистра Йоды также следуют этой схеме.
РАЗЛИЧНЫЕ ПРЕДСТАВЛЕНИЯ УРАВНЕНИЙ
Существует множество способов упорядочивания операторов и операндов.
Вы, вероятно, привыкли заниматься математикой, используя так называемую
инфиксную запись. Инфиксная запись помещает операторы между операндами,
например 4 + 8. В инфиксной записи для группировки требуются круглые скобки,
например (1 + 2) × (3 + 4).
Польский логик Ян Лускашевич (Jan Łuskasiewicz) изобрел префиксное представление в 1924 году. Оно также известно как польская запись, по национальности
ученого. В польской записи оператор ставится перед операндами, например + 4 8.
Преимущество польской записи заключается в том, что при такой записи не требуются скобки. Предыдущий инфиксный пример будет записан как × + 1 2 + 3 4.

170   Глава 5. Архитектура компьютера
Американский математик Артур Бёркс (Arthur Burks) предложил обратную
польскую запись (reverse Polish notation, RPN), также называемую постфиксным
представлением, в 1954 году. RPN помещает оператор после операндов, как
в записи 4 8 +, поэтому предыдущий пример будет выглядеть как 1 2 + 3 4 + ×.
RPN легко реализовать с помощью стеков. Операнды помещаются в стек. Операторы извлекают операнды из стека, выполняют операцию, а затем помещают
результат обратно в стек.
В калькуляторах HP RPN есть клавиша ввода, которая помещает операнд в стек
в неоднозначных ситуациях; иначе никак не определить, что 1 и 2 — это отдельные
операнды, а не число 12. На таком калькуляторе уравнение решается с использованием последовательности клавиш 1 ENTER 2 + 3 ENTER 4 + ×. Калькулятор
инфиксной записи потребует больше нажатий на клавиши.
Пример уравнения будет выглядеть так: 1 2 add 3 4 add mul в языке программирования PostScript. Никакого специального ENTER не требуется, потому что вместо
него используется пробел.

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

Печь печенье

Есть ли
кто-то возле
двери?

Да

Открыть дверь

Нет
Перейти
кследующей
задаче

Рис. 5.6. Выпечка печенья № 1

Прерывания   171
Это может сработать, если ваш гость очень терпелив. Но допустим, что вам доставили посылку, за которую нужно расписаться. Курьер не будет ждать 45 минут, если только не почувствует запах печенья и не понадеется на угощение.
Попробуем другую схему, как на рис. 5.7.
Начало

Достать миксер

Есть ли
кто-то возле
двери?

Да

Открыть дверь

Нет
Взбить две пачки масла

Есть ли
кто-то возле
двери?

Да

Открыть дверь

Нет
Добавить
¾ стакана сахара
...

Рис. 5.7. Приготовление печенья № 2
Данный метод называется опросом. Он работает, но не очень хорошо. У вас
меньше шансов пропустить курьера, но вы тратите слишком много времени на
походы к двери.
Можно разделить каждую из задач по приготовлению на более мелкие подзадачи
и проверять дверь между ними. Это повысит шансы на получение посылки, но
в какой-то момент вы потратите больше времени на походы к двери, чем на готовку.
Это частая и важная проблема, у которой, действительно, нет программного
решения. Невозможно заставить схему хорошо работать, изменив структуру
программы. Нужно придумать способ прервать запущенную программу, чтобы

172   Глава 5. Архитектура компьютера
она могла отреагировать на что-то внешнее, требующее внимания. Пришло
время добавить некоторые аппаратные функции в исполнительное устройство.
Практически каждый современный процессор включает в себя блок прерываний.
Обычно он имеет контакты (пины) или электрические соединения, которые
прерывают выполнение при правильном перемещении. Пин (pin) — это разговорный термин, обозначающий какое-либо соединение с микросхемой электрическим способом. Раньше в микросхемах были детали, похожие на булавки
(pin — «булавка»), но по мере того, как устройства и инструменты становились
меньше, появилось много других вариантов. Многие процессорные микросхемы,
особенно микрокомпьютеры, содержат встроенные периферийные устройства
(внутрипроцессорные устройства ввода/вывода), которые имеют внутреннее
подключение к системе прерывания.
Объясню, как это работает. Периферийное устройство, требующее внимания,
генерирует запрос на прерывание. Процессор (обычно) завершает инструкцию,
выполняемую в данный момент. Затем он приостанавливает текущую программу
и переключается на совершенно другую программу, называемую обработчиком
прерываний. Обработчик прерываний делает все, что ему нужно, и основная
программа продолжает работу с того места, где она остановилась. Обработчики
прерываний — это функции.
Обработчиком прерываний в процессе приготовления печенья является дверной
звонок. Вы с удовольствием готовите печенье, пока вас не прервет звонок в дверь,
хотя, если вас постоянно прерывают опросчики, это раздражает. Следует учесть
несколько моментов. Во-первых, время реакции на прерывание. Если вы долго
болтаете с курьером, печенье может подгореть; вам необходимо убедиться, что
вы можете своевременно обрабатывать прерывания. Во-вторых, нужен способ
сохранить состояние перед ответом на прерывание, чтобы вернуться к готовке
после его обработки. Например, если в прерванной программе что-то было в регистре, обработчик прерывания должен сохранить содержимое этого регистра,
а затем восстановить его перед возвратом в основную программу.
Система прерывания использует стек, чтобы сохранить место в прерванной
программе. Задача обработчика прерывания — записать все, что ему может понадобиться. Таким образом, обработчик может сэкономить абсолютный минимум,
необходимый для быстрой работы.
Как компьютер узнает, где найти обработчик прерывания? Обычно существует
набор зарезервированных адресов памяти для векторов прерываний, по одному
для каждого поддерживаемого прерывания. Вектор прерывания — это просто указатель, адрес ячейки памяти. Это похоже на вектор в математике или
физике — на стрелку, которая говорит: «Иди отсюда туда». Когда происходит
прерывание, компьютер ищет этот адрес и передает ему управление.
Многие устройства включают векторы прерываний для исключений, в том числе для переполнения стека и использования недопустимого адреса, например

Относительная адресация   173
адреса, выходящего за пределы физической памяти. Переадресация исключений
обработчику прерывания часто позволяет ему исправить проблемы, чтобы программа продолжила работу.
Как правило, существуют всевозможные специальные средства управления
прерываниями, например способы включения и выключения определенных прерываний. Часто используется маска, чтобы сказать что-то вроде «не прерывай,
пока дверца духовки открыта». На машинах с несколькими типами прерываний
зачастую указывается порядок приоритетов, так что самые важные события
обрабатываются в первую очередь. Это означает, что обработчики прерываний
с более низким приоритетом могут быть прерваны самостоятельно. Большинство
устройств имеют один или несколько встроенных таймеров, которые можно
настроить для генерации прерываний.
Операционные системы, описанные в следующем разделе, часто не позволяют
получить доступ к физическим (аппаратным) прерываниям для большинства
программ. Они предоставляют взамен виртуальную или программную систему
прерывания. Например, операционная система UNIX имеет сигнальный механизм.
Многие современные системы называют этот механизм событиями.

Относительная адресация
Что нужно для одновременного запуска нескольких программ?
Для начала нам понадобится программа-диспетчер, которая знает, как переключаться между программами. Мы будем называть ее операционной системой
или ядром операционной системы. Чтобы отличать ОС и программы, которые
она контролирует, назовем ОС системной программой, а все остальное — пользовательскими программами, или процессами. Простая ОС работает примерно
так, как показано на рис. 5.8.
Здесь ОС использует таймер, чтобы указать, когда нужно переключаться между
пользовательскими программами. Этот метод планирования называется квантованием времени, потому что он дает каждой программе отрезок времени для
выполнения. Состояние или контекст пользовательской программы относится
к содержимому регистров и любой памяти, которую программа использует,
включая стек.
Это работает, но довольно медленно. На загрузку программы нужно время.
Мы бы ускорили работу, если бы программы можно было загружать в память,
насколько позволяет пространство, и хранить их там, как показано на рис. 5.9.
В этом примере пользовательские программы загружаются в память одна за
другой. Но подождите, как это может работать? Как объяснялось в разделе «Режимы адресации» на с. 149, наш компьютер-образец использовал абсолютную
адресацию, это означает, что адреса в инструкциях относятся к определенным

174   Глава 5. Архитектура компьютера
ячейкам памяти. Запустить программу, ожидающуюадрес 1000, по другому
адресу, например 2000, не получится.
Загрузить
пользовательскую программу

Восстановить состояние

Запустить
пользовательскую программу
Адрес

Прервать выполнение
по таймеру

3000

Остановить
пользовательскую программу

2000
1000

Сохранить состояние

0

Рис. 5.8. Простая операционная
система

Пользовательская программа n
...
Пользовательская программа 2
Пользовательская программа 1
Операционная система

Рис. 5.9. Несколько программ
в памяти

Некоторые компьютеры решают эту проблему, добавляя индексный регистр
(рис. 5.10) — регистр, содержимое которого добавляется к адресам для формирования действительных адресов. Если пользовательская программа ожидает
запуска по адресу 1000, ОС может передать в индексный регистр адрес 2000,
прежде чем запускать ее по адресу 3000.
Адрес
+

Действительный адрес

Индексный регистр

Рис. 5.10. Индексный регистр
Еще один способ решить проблему — использование относительной адресации.
Вместо того чтобы располагать адреса в инструкциях относительно 0 (начало
памяти в большинстве устройств), можно помещать их относительно адресов
их инструкций. Вернитесь к табл. 4.4 на с. 153. Обратите внимание, что вторая
инструкция содержит адрес 100 (110100 в двоичном формате). В случае с относительной адресацией мы используем адрес +99, поскольку инструкция

Блок управления памятью   175
находится по адресу 1, а адрес 100 находится на расстоянии 99. Аналогично последняя инструкция — это ответвление к адресу 4, который станет ответвлением
с адресом −8 в относительной адресации. Подобные вещи кажутся кошмарными в двоичном формате, но современные языковые инструменты делают всю
арифметику за нас. Относительная адресация позволяет перемещать программу
в любое место в памяти.

Блок управления памятью
Многозадачность перестала быть роскошью сегодня, когда все устройства подключены к интернету, потому что задачи обмена данными постоянно выполняются в фоновом режиме, то есть в дополнение к тому, что делает пользователь.
Немного помогают регистры индексов и относительная адресация, но этого
недостаточно. Что произойдет, если в какой-то программе возникнут ошибки?
Например, что если ошибка в пользовательской программе 2 (рис. 5.9) заставит
ее перезаписать что-то в пользовательской программе 1 или, что еще хуже, в ОС?
Что, если кто-то намеренно написал программу, чтобы шпионить за другими
программами, работающими в системе, или изменять их? Вообще желательно
изолировать каждую программу, чтобы сделать эти сценарии невозможными.
С этой целью большинство микропроцессоров теперь включают в себя блок
управления памятью (memory management unit, MMU). MMU — пример очень
сложного аппаратного обеспечения.
Системы с MMU различают виртуальные и физические адреса. MMU преобразует виртуальные адреса, задействованные программами, в физические адреса,
используемые памятью, как показано на рис. 5.11.

Программа

Виртуальный
адрес

MMU

Физический
адрес

Память

Рис. 5.11. Преобразование адреса в MMU
Чем это отличается от индексного регистра? Блоки управления памятью используют не всю ширину адреса. Здесь происходит разделение виртуального
адреса на две части. Нижняя часть идентична физическому адресу. Верхняя
часть преобразовывается через часть ОЗУ, называемую таблицей переадресации
страниц, пример которой представлен на рис. 5.12.
В этом примере память разбита на 256-байтные страницы. Содержимое таб­
лицы переадресации страниц определяет фактическое расположение каждой
страницы в физической памяти. Это позволяет взять программу, которая ожидает запуска с адреса 1000, и поместить ее в адрес 2000 или другой, если она
выровнена по границе страницы. И хотя виртуальное адресное пространство

176   Глава 5. Архитектура компьютера
кажется программе непрерывным, его не нужно отображать на непрерывные
страницы физической памяти. Можно даже переместить программу в другое
место в физической памяти во время ее работы. Мы можем предоставить одной
или нескольким взаимодействующим программам общую память, отобразив
части их виртуального адресного пространства на одну и ту же физическую
память. Обратите внимание, что содержимое таблицы переадресации страниц
становится частью контекста программы.
Виртуальный адрес
A 15 A 14 A 13 A 12 A 11 A 10 A 9 A 8 A 7 A 6 A 5 A 4 A 3 A 2 A 1 A 0

A 15 A 14 A 13 A 12 A 11 A 10 A 9 A 8 A 7 A 6 A 5 A 4 A 3 A 2 A 1 A 0
Физический адрес
A0

Y0

A1

Y1

A2

Y2

A3

Y3

A4

Y4

A5

Y5

A6
A7

A 15 A 14 A 13 A 12 A 11 A 10 A 9 A 8

...
Y 255

Декодер адресов

Таблица переадресации страниц

Рис. 5.12. Простая таблица переадресации страниц для 16-битного компьютера
Вы могли заметить, что таблица переадресации страниц выглядит как фрагмент
памяти. И вы будете правы. Наверняка вы ждете, что я скажу, что это непросто. Так и есть. В нашем примере используются 16-битные адреса. Что будет
в случае с современными компьютерами с 64-битными адресами? Если мы разделим адрес пополам, нам понадобится 4 ГиБ таблицы переадресации страниц,
и размер страницы также будет равен 4 ГиБ, что не очень хорошо, поскольку это
больше, чем есть во многих системах. Можно уменьшить размер страницы, но
это увеличит размер самой таблицы переадресации. С этим нужно что-то делать.
MMU в современном процессоре имеет ограниченный размер таблицы пере­
адресации страниц. Полный набор записей таблицы хранится в основной памяти

Виртуальная память   177
или на диске, если память заканчивается. MMU загружает подмножество запи­
сей таблицы переадресации страниц в свою таблицу по мере необходимости.
Некоторые проекты MMU добавляют в свои таблицы переадресации страниц
дополнительные управляющие биты — например, бит, не выполняющий никаких
действий. Когда такой бит помещается на страницу, ЦП не будет выполнять инструкции с этой конкретной страницы. Это не позволяет программам загружать
свои собственные данные, потому что это представляет собой угрозу безопасности.
Другой общий управляющий бит делает страницы доступными только для чтения.
MMU генерируют исключения «отказ страницы», когда программа пытается
получить доступ к адресу, не сопоставленному с физической памятью. Это полезно, например, в случае переполнения стека. Вместо того чтобы прерывать
выполняющуюся программу, ОС может использовать MMU для отображения
некоторой дополнительной памяти, чтобы увеличить пространство стека, а затем возобновить выполнение пользовательской программы.
MMU практически сглаживают различия в архитектуры фон Неймана и гарвардской. Такие системы имеют единую шину, как в архитектуре фон Неймана,
но могут предоставлять отдельную память для команд и данных.

Виртуальная память
Операционные системы управляют распределением ограниченных аппаратных
ресурсов между конкурирующими программами. Например, на рис. 5.8 мы видели, как ОС управляет доступом к самому процессору. Память также является
управляемым ресурсом. Операционные системы используют MMU для предоставления виртуальной памяти пользовательским программам.
Ранее мы видели, что MMU может связывать виртуальные адреса программы
с физической памятью. Но виртуальная память полезна не только для этого.
Механизм отказа страницы позволяет программам думать, что у них может быть
столько памяти, сколько им понадобится, даже если это превышает ее физический
объем. Что произойдет, если запрошенная память больше доступного объема? ОС
перемещает содержимое страниц памяти, которые в настоящее время не нужны,
в более крупное, но более медленное запоминающее устройство, чаще всего на
диск. Когда программа пытается получить доступ к выгруженной памяти, ОС
делает все, что ей нужно, чтобы освободить место, а затем копирует запрошенную
страницу обратно. Этот процесс известен как подкачка по требованию. На рис. 5.13
показана система виртуальной памяти с одной выгруженной страницей.
При подкачке сильно страдает производительность системы, но это все же лучше, чем невозможность запустить программу из-за нехватки памяти. Системы
виртуальной памяти используют ряд уловок, чтобы минимизировать снижение
производительности. Одна из них — это алгоритм удаления наиболее давно

178   Глава 5. Архитектура компьютера
использованных элементов (least recently used, LRU), отслеживающий доступ
к страницам. Самые востребованные страницы хранятся в физической памяти,
а наименее часто используемые выгружаются.
Виртуальная
память

Таблица
переадресации
страниц MMU

Физическая
память

Область подкачки
на съемном или жестком диске
устройства

Страница 0
Страница 1
Страница 2

...
Содержимое
страницы n
Страница n

Рис. 5.13. Виртуальная память

Пространство системы и пользователя
Системы многозадачности создают для каждого процесса иллюзию, что это
единственная программа, запущенная на компьютере. MMU помогают развить эту иллюзию, предоставляя каждому процессу собственное пространство
адресов. Но эту иллюзию трудно поддерживать, если дело касается устройств
ввода/вывода. Например, ОС использует таймер, чтобы напомнить самой
себе, когда переключаться между программами, показанными на рис. 5.8.
ОС решает установить таймер для генерации прерывания один раз в секунду, но, если одна из пользовательских программ изменит его значение на
прерывание один раз в час, все пойдет не так, как ожидалось. Точно так же
MMU не обеспечит серьезной изоляции между программами, если какая-либо
пользовательская программа может изменить собственную конфигурацию.
Многие процессоры включают дополнительное аппаратное обеспечение, которое решает эту проблему. В регистре есть бит, который указывает, находится ли
компьютер в системном или пользовательском режиме. Некоторые инструкции,
например те, которые имеют дело с вводом/выводом, считаются привилегированными и могут выполняться только в системном режиме. Специальные
команды, называемые ловушками или системными вызовами, позволяют программам пользовательского режима делать запросы к программам системного
режима, то есть к операционной системе.
Такое ранжирование имеет несколько преимуществ. Во-первых, оно защищает
ОС от действий пользовательских программ, а эти программы — друг от друга.

Иерархия памяти и производительность   179
­ о-вто­рых, поскольку программы пользователя не имеют доступа к определенВ
ным вещам, таким как MMU, операционная система может управлять выделением ресурсов для них. Системное пространство — это место, где обрабатываются
аппаратные исключения.
Любые программы, которые вы создаете для телефона, ноутбука или настольного
компьютера, будут запускаться в пользовательском пространстве. Прежде чем
вносить изменения в программы, работающие в системном пространстве, нужно
стать действительно хорошим программистом.

Иерархия памяти и производительность
Когда-то процессоры и память работали с одинаковой скоростью и на земле
царил мир. Однако скорость процессоров постоянно увеличивалась, и, хотя
память тоже становилась быстрее, она не могла угнаться за ними. Архитекторы
придумали всевозможные уловки, чтобы эти быстрые процессоры не сидели без
дела в ожидании памяти.
Виртуальная память и подкачка вводят понятие иерархии памяти. Хотя для
пользовательской программы вся память выглядит одинаково, то, что происходит за кулисами, сильно влияет на производительность системы. Или,
перефразируя Джорджа Оруэлла, все обращения к памяти равны, но некоторые равнее.
Компьютеры быстры. Они могут выполнять миллиарды инструкций в секунду.
Но вряд ли бы мы могли что-то сделать, если бы ЦП приходилось ожидать этих
инструкций или данных, которые нужно извлечь или сохранить.
Мы выяснили, что процессоры включают в себя очень быструю и дорогостоящую
память, называемую регистрами. Ранние компьютеры имели лишь несколько регистров, тогда как некоторые современные машины содержат их сотни. Но в целом
соотношение регистров к памяти стало меньше. Процессоры обмениваются данными с основной памятью, обычно с DRAM, которая практически в десять раз
медленнее процессора. Запоминающие устройства, такие как съемные и жесткие
диски, имеют скорость в одну миллионную от скорости процессора.
Пришло время провести аналогию с едой, любезно предоставленную моим другом Клемом. Регистры похожи на холодильник: в них не так много места, но все
содержимое можно достать быстро. Основная память похожа на продуктовый
магазин: в ней намного больше места для хранения, но требуется время, чтобы
добраться туда. Запоминающее устройство похоже на склад: места еще больше,
но находится он еще дальше.
Расширим эту аналогию. Вы часто открываете холодильник ради какого-то одного продукта. Отправляясь в магазин, вы наполняете продуктами несколько пакетов. Склад доставляет продукты в магазин целыми грузовыми автомобилями.

180   Глава 5. Архитектура компьютера
Компьютеры действуют так же. Небольшие блоки данных перемещаются между
процессором и основной памятью. Большие блоки перемещаются между основной памятью и диском. Загляните в «The Paging Game» Джеффа Берримана
(Jeff Berryman) — он придумал забавное объяснение того, как все это работает.
Опустив множество подробностей, предположим, что процессор работает примерно в 10 раз быстрее, чем основная память. Это приводит к тому, что в ожидании памяти тратится много времени, поэтому было добавлено дополнительное
оборудование (более быстрая встроенная память) для «кладовки», или кэша.
Кэш намного меньше магазина, но гораздо быстрее при работе на полной скорости процессора.
Как заполнить кладовку продуктами из магазина? Еще в разделе «Оперативная память» на с. 125 мы увидели, что DRAM лучше работает при обращении
к столбцам, чем к строкам. Исследуя, как работают программы, вы заметите, что
они обращаются к последовательным ячейкам памяти, если не используется
ветвление. При этом изрядное количество данных, используемых программой,
имеет тенденцию скапливаться вместе. Это явление применяется для повышения
производительности системы. Аппаратное обеспечение контроллера памяти ЦП
заполняет кэш из последовательных столбцов в строке, потому что чаще всего
требуются данные из последовательных расположений. Вместо одной коробки
хлопьев мы кладем в пакеты сразу несколько и приносим их домой. Используя
самый высокоскоростной режим доступа к памяти, процессоры обычно идут
впереди, даже когда происходит промах кэша, вызванный непоследовательным
доступом. Промах кэша (cache miss) — это не участница конкурса красоты мисс
Кэш; это состояние, когда ЦП ищет в кэше что-то, чего там нет, и пытается извлечь это «ничего» из памяти. Аналогично, попадание в кэш (cache hit) — это
когда ЦП находит в кэше то, что ищет. Должны же быть и хорошие термины.
Существует несколько уровней кэш-памяти, и они становятся больше и медленнее по мере удаления от ЦП (даже если они находятся на одной микросхеме).
Они называются кэшами L1, L2 и L3, где L означает уровень (от level). Это как
запасной морозильник в гараже и на складе. При этом существуют диспетчеры,
отвечающие за работу службы контроля авиаперелетов. Есть множество логических схем, задача которых — упаковывать и распаковывать пакеты с продуктами,
коробки и грузовики разных размеров, чтобы все это работало. На самом деле все
это занимает значительную часть чипа. Иерархия памяти показана на рис. 5.14.
Дополнительные сложные настройки еще больше повысили производительность. Компьютеры включают в себя схему предсказателя переходов, которая
угадывает результат выполнения инструкций условного ветвления, так что
правильные данные могут быть предварительно загружены из памяти и в кэш,
готовые к работе. Есть даже схема для обработки внеочередного исполнения. Это
позволяет процессору выполнять инструкции в наиболее эффективном порядке,
даже если это не тот порядок, в котором они встречаются в программе.

Сопроцессоры   181

Дорого ибыстро

ЦП
Регистры
Кэш L1
Кэш L2
Кэш L3
Основная память

Медленно идешево

Запоминающие
устройства

Рис. 5.14. Иерархия памяти
Поддержание когерентности кэша — особенно серьезная проблема. Представьте
систему, состоящую из двух процессорных микросхем, каждая с четырьмя ядрами.
Одно из этих ядер записывает данные в область памяти — точнее, в кэш, откуда
они в конечном итоге попадут в память. Как другое ядро или процессор узнает, что
пришла нужная версия данных из этой области памяти? Самый простой способ
называется сквозной записью, это означает, что записи поступают непосредственно
в память и не кэшируются. Но это уменьшает многие преимущества кэширования, поэтому существует много дополнительного аппаратного обеспечения для
управления кэшем — его обсуждение выходит за рамки данной книги.

Сопроцессоры
Ядро процессора — довольно сложный элемент схемы. Вы можете освободить
ядра процессора для общих вычислений, разделив общие операции по более простым частям оборудования, называемым сопроцессорами. Раньше сопроцессоры
существовали только потому, что на одной микросхеме не хватало места для
всех элементов. Например, были представлены сопроцессоры для вычислений
с плавающей точкой, если на основном процессоре нельзя было уместить соответствующие аппаратные средства. Сегодня существуют встроенные сопроцессоры под многие задачи, включая специализированную обработку графики.
В этой главе мы говорили о загрузке программ в память для запуска, это обычно
означает, что программы поступают из некоторой медленной и дешевой памяти,
такой как дисковый накопитель. И мы увидели, что системы виртуальной памяти
могут читать и записывать информацию на диски как часть подкачки. Из раздела «Блочные устройства» на с. 129 мы узнали, что диски не имеют байтовой
адресации — они передают блоки размером 512 или 4096 байт. Это означает,
что нужно копировать много информации между основной памятью и диском,
поскольку никаких других вычислений не требуется. Копирование данных из
одного места в другое — одна из самых больших затрат процессорного времени.
Некоторые сопроцессоры ничего не делают, кроме перемещения данных. Они

182   Глава 5. Архитектура компьютера
называются блоками прямого доступа к памяти (direct memory access, DMA).
Их можно настроить для выполнения таких операций, как «переместить много
всего отсюда туда и сообщить, когда операция будет завершена». ЦП перекладывают большую часть рутинной работы на блоки прямого доступа к памяти,
что делает ЦП свободным для выполнения более полезных операций.

Организация данных в памяти
Из программы в табл. 4.4 мы узнали, что память используется не только для
инструкций, но и для данных. В нашем случае это статические данные, что
означает, что необходимый объем памяти уже известен при написании программы. Ранее в этой главе мы видели, что программы также используют память
для стеков. Эти области данных необходимо расположить в памяти так, чтобы
между ними не возникало конфликтов.
На рис. 5.15 показана типичная организация устройств с архитектурой фон Неймана и гарвардской без MMU. Как видно, единственное отличие состоит в том, что на
машинах с гарвардской архитектурой инструкции находятся в отдельной памяти.
Стек

Статические данные

Стек

Статические данные

Инструкции

Инструкции

Архитектура
фон Неймана

Гарвардская
архитектура

Рис. 5.15. Организация памяти
Существует еще один способ использования памяти
программами. Большинству программ приходится
иметь дело с динамическими данными, размер которых
неизвестен до запуска программы. Например, система
обмена мгновенными сообщениями не знает заранее,
сколько сообщений ей нужно хранить или сколько памяти потребуется для каждого сообщения. Динамические
данные обычно помещаются в память над статической
областью, называемую кучей, как показано на рис. 5.16.
Куча увеличивается по мере того, как требуется больше
места для динамических данных, в то время как стек
растет по направлению вниз. Важно убедиться, что
стек и куча не пересекаются. Бывают незначительные

Стек

Куча
Статические данные
Инструкции

Рис. 5.16.
Организация памяти
с кучей

Запуск программ   183
вариации; некоторые процессоры резервируют адреса памяти в начале или
в конце памяти для векторов прерываний и регистров, управляющих периферийными устройствами ввода/вывода.
Такое расположение памяти можно встретить при использовании микрокомпьютеров, поскольку они обычно не имеют блоков MMU. Когда задействованы
MMU, инструкции, данные и стек связываются с разными страницами физической памяти, размер которых можно регулировать по мере необходимости. Но та
же структура памяти используется для виртуальной памяти, предоставленной
программам.

Запуск программ
Вы видели, что компьютерные программы состоят из множества частей. В этом
разделе вы узнаете, как все они объединяются.
Ранее я говорил, что программисты используют функции для повторного
применения кода. И это еще не всё. Существует множество функций, которые
полезны для более чем одной программы, например функции сравнения двух
текстовых строк. Хорошо было бы иметь возможность использовать эти сторонние функции, вместо того чтобы каждый раз писать собственные. Один из
способов сделать это — сгруппировать связанные функции в библиотеки. Доступно большое количество библиотек для всего на свете, от обработки строк
до сложных математических вычислений и декодирования MP3.
Помимо библиотек, нестандартные программы обычно собираются по частям.
Можно поместить всю программу в один файл, однако есть несколько веских
причин этого не делать. Главная из них заключается в том, что это упрощает
одновременную работу нескольких человек над одной и той же программой.
Но разбиение программ на части означает, что нужен какой-то способ связать
или соединить все разрозненные части. Для этого каждая часть программы обрабатывается в промежуточном формате, предназначенном для этой цели, а затем
запускается специальная программа-компоновщик, которая устанавливает все
соединения. Многие промежуточные форматы файлов были созданы и улучшены по мере разработки программ. Формат исполняемых и связываемых файлов
(Executable and Linkable Format, ELF) в настоящее время — самый популярный
вариант. Этот формат включает разделы, похожие на объявления о поиске
товаров. В разделе продаж можно найти что-то вроде объявления, в котором
говорится: «У меня есть функция с именем cube». Точно так же можно увидеть
объявление «Ищу переменную с именем date» в разделе «Требуется».
Компоновщик (linker) — это программа, которая связывает между собой
все объявления, в результате чего получается программа, которая действительно может быть запущена. Но, конечно, при этом возникают сложности

184   Глава 5. Архитектура компьютера
с производительностью. Раньше вы относились к библиотекам как к одному из
файлов, наполненных функциями, и связывали их с остальной частью программы. Это называлось статическим связыванием. Однако примерно в 1980-х годах
люди заметили, что многие программы используют одни и те же библиотеки,
что было прекрасным свидетельством ценности библиотек. Но они увеличивали
размер каждой программы, которая их использовала, и появилось много копий
одних и тех же библиотек, использующих ценную память. Это привело к созданию общих библиотек. Можно использовать MMU, чтобы несколько программ
могли обращаться к одной копии библиотеки, как показано на рис. 5.17.
Программа 1

Физическая память

Программа 2

Библиотека

Рис. 5.17. Общая библиотека
Имейте в виду, что инструкции из такой библиотеки являются общими для программ, которые к ней обращаются. Необходимо проектировать библиотечные
функции так, чтобы они использовали кучу и стек вызывающих программ.
У программ есть точка входа — адрес первой инструкции в программе. Эта
инструкция не первая исполняемая при запуске программы, как это ни парадоксально. Когда все части программы связываются в исполняемый файл,
включается дополнительная библиотека среды выполнения (runtime library).
Код в ней запускается до достижения точки входа.
Библиотека среды выполнения отвечает за настройку памяти, что означает
создание стека и кучи. Она также устанавливает начальные значения для элементов в области статических данных. Эти значения хранятся в исполняемом
файле и должны быть скопированы в статические данные после получения
памяти из системы.
Такая библиотека выполняет гораздо больше функций, кроме описанных выше,
особенно для сложных языков. К счастью, сейчас вам не нужно изучать это
подробно.

Мощность запоминающих устройств
До сих пор мы подходили к памяти с точки зрения производительности. Однако нужно учитывать кое-что еще — перемещение данных в памяти требует

Выводы   185
некоторых усилий. Для настольных компьютеров это не проблема, но не нужно
забывать о мобильных устройствах. И хотя время автономной работы не является проблемой в центрах обработки данных, например в тех, которые используются крупными интернет-компаниями, использование дополнительной
мощности на тысячах машин в сумме влияет на систему в целом.
Сбалансировать потребление мощности и производительность непросто. Имейте
в виду оба этих параметра при написании кода.

Выводы
Вы узнали, что работать с памятью не так просто, как можно было подумать после
прочтения главы 4. Вы почувствовали, насколько дополнительные сложности
влияют на простые процессоры и использование памяти. Теперь вы получили
довольно полное представление о том, что можно найти в современном компьютере, за исключением ввода/вывода, о котором речь пойдет в главе 6.

6

Разбор связей

Компьютеры работают не просто так. Они принимают
входные данные из различных источников, выполняют
вычисления и производят выходные данные, которые
используются огромным количеством устройств. Компьютеры могут общаться с людьми, разговаривать друг
с другом или управлять предприятиями. Рассмотрим их
работу подробнее.
Я кратко упомянул ввод и вывод (input/output, I/O) в разделе «Ввод и вывод» на
с. 139, имея в виду ввод и вывод данных из ядра процессора. Сделать это не так
уж и сложно; все, что нам нужно, — несколько триггеров-защелок (см. «Триггерызащелки» на с. 113) для вывода и буферов с тремя состояниями (см. рис. 2.38) для
ввода. Раньше считалось, что каждый компонент устройства ввода/вывода будет
подключен к какому-либо биту на триггере-защелке или буфере, а компьютер будет выступать в роли кукловода, отвечающего за артикуляцию всех конечностей.
Снижение стоимости процессора изменило это. Многие сложные в прошлом
устройства ввода/вывода теперь имеют собственные микропроцессоры. Например, можно купить трехосный акселерометр или датчик температуры с хорошим
цифровым выходом всего за несколько долларов. Мы не будем обсуждать подобные устройства, потому что они не интересны с точки зрения программирования — интерфейс просто читает и записывает байты, как описано в специ­
фикации устройства. Слишком простые вещи мы не учитываем. Вы можете
написать код для устройства со встроенным процессором. Если вы возьметесь
за разработку очередной расчески с выходом в интернет, то, скорее всего, у вас
волосы встанут дыбом от сложности ее алгоритма управления.

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

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

Порты ввода/вывода
Самый простой способ заставить компьютер с чем-то разговаривать — это
подключить его к порту ввода/вывода. Например, компания Atmel (является
частью компании Microchip) производит семейство небольших микроконтроллеров AVR. Они включают в себя большое количество встроенных устройств
ввода/вывода. На рис. 6.1 мы подключаем некоторые устройства к порту B.

V CC
+
V


AVR
GND

PB 7
PB 6
PB 5
PB 4
PB 3
PB 2
PB 1
PB 0
LED

Рис. 6.1. Светодиод и переключатель, подключенные к порту B
Переключатель на рис. 6.1 уже должен быть вам знаком по главе 2. LED — это
аббревиатура от английского light-emitting diode — светодиод. Диод — это полупроводниковое устройство, которое работает как турникет в парке развлечений:
он пропускает электричество только в одном направлении, обозначенном полой
стрелкой. У светодиодов есть приятный побочный эффект — они светятся.
Обратите внимание на резистор, включенный последовательно со светодиодом.
Он предназначен для ограничения силы тока, протекающего через светодиод,
чтобы ни он сам, ни PB0 не сгорели. Номинал резистора можно рассчитать,
используя закон Ома, введенный в главе 2. Допустим, V составляет 5 вольт.
Одна из характеристик кремниевых бутербродов, о которых шла речь в разделе

188   Глава 6. Разбор связей
«Транзисторы» на с. 94, заключается в том, что падение напряжения на одном
из них составляет 0.7 вольта. В техническом описании микроконтроллера
AVR указано, что выходное напряжение для логической 1, когда V составляет
5 вольт, равно 4.2 вольта. Мы хотим ограничить ток до 10 мА (0.01 А), потому
что этого ожидает светодиод; AVR может потреблять 20 мА. Закон Ома гласит,
что сопротивление — это напряжение, деленное на ток, поэтому (4.2 – 0.7) ÷
0.01 = 350 Ом. Как видите, PB7 может переключаться между напряжениями
логического 0 и 1. Когда PB0 установлен в 0, электричество не проходит. Электрический ток течет через светодиод, когда PB0 установлен в 1, что заставляет
диод светиться. Убедитесь, что вы прочитали техническое описание светодиода
или любого другого используемого компонента, потому что некоторые характеристики, такие как падение напряжения, могут отличаться.
Порт B настраивается и управляется тремя регистрами, как показано на рис. 6.2.
DDRB (data direction register), регистр направления передачи данных, определяет, является ли каждый вывод входом или выходом. PORTB — это триггер-­
защелка, которая хранит выходные данные. PINB считывает значения на выводах
PORTB.
7

6

5

4

3

2

1

0

DDB7

DDB6

DDB5

DDB4

DDB3

DDB2

DDB1

DDB0

7

6

5

4

3

2

1

0

PORTB7

PORTB6

7

6

5

4

3

2

1

0

PINB7

PINB6

PINB5

PINB4

PINB3

PINB2

PINB1

PINB0

PORTB5 PORTB4

PORTB3

PORTB2 PORTB1

PORTB0

DDRB
PORTB
PINB

Рис. 6.2. Регистры PORTB в AVR
Может показаться, что все действительно сложно, но, как видно на рис. 6.3, это
просто еще одна схема стандартных строительных блоков: демультиплексоров,
триггеров и буферов с тремя состояниями.
DDRB — это регистр направления передачи данных для порта B. Запись 1 в любой бит превращает связанный вывод в выход; если установлено значение 0 — во
вход. PORTB — это выходная часть порта. Запись 0 или 1 в любой бит задает соответствующему выходу низкий или высокий логический уровень. Чтение PINB
обес­печивает состояние связанных выводов, поэтому, если выводы 6 и 0 подтянуты вверх, а остальные — вниз, будет прочитано значение 01000001, или 0x41.
Как видите, вводить и выводить данные из микроконтроллера довольно просто.
Можно прочитать положение переключателя, посмотрев на PINB7 в регистре
PINB. Включить и выключить светодиод можно, записав данные в PORTB0
в регистре PORTB. Чтобы развлечь себя и своих друзей, можно написать простую программу, которая будет мигать светодиодом.

Низкоуровневый ввод/вывод   189

PINB n
Шина данных

PB n

n

PORTB n
D Q
Адресная шина
R/W

DDRB n
D Q

Рис. 6.3. Конструкция порта B в микроконтроллере AVR

Нажми на кнопку
На многих устройствах есть кнопки или переключатели. Из-за их конструкции
компьютеру не так-то просто их прочитать, как может показаться. Простая кнопка состоит из пары электрических контактов и металлической части, которая
соединяет их при нажатии кнопки. Взгляните на схему на рис. 6.4.
+
V


R

Процессор
IRQ

Рис. 6.4. Простая схема кнопки
R — это то, что называется подтягивающим резистором, как мы видели ранее на
рис. 2.37. Когда кнопка не нажата, резистор подтягивает напряжение на входе
запроса прерывания процессора (interrupt request, IRQ) до напряжения, подаваемого от источника питания V, что делает его логической 1. Когда кнопка нажата,
резистор ограничивает ток, протекающий в цепи от источника питания, чтобы
он не вышел из строя, — тогда на входе IRQ будет установлен логический 0.
Это выглядит просто, пока не взглянуть на рис. 6.5. Можно подумать, что после нажатия и отпускания кнопки сигнал IRQ будет выглядеть как на картинке
слева, но на самом деле он больше похож на сигнал справа.
Что происходит? Когда металл, соединенный с кнопкой, соприкасается с контактами, он дребезжит и на короткое время теряет с ними связь. Он может

190   Глава 6. Разбор связей
дребезжать некоторое время, прежде чем успокоится. Поскольку мы соединили
кнопку с генерирующим прерывание входом процессора, одно нажатие кнопки может привести к нескольким прерываниям. Наверняка это не то, чего вы
ожидали, и дребезг кнопки нужно устранить. (Можно приобрести кнопки без
дребезга, но они обычно дороже.)
Напряжение

Напряжение
Время
Нажать

Время
Отпустить

Нажать

Отпустить

Рис. 6.5. Дребезг контактов кнопки
Простой способ устранения дребезга — настроить обработчик прерывания
на таймер, а затем проверить состояние кнопки после истечения таймера,
как показано на рис. 6.6. Здесь есть два варианта: установить таймер при
первом прерывании или менять запущенный таймер на новый при каждом
прерывании.
Таймер
Напряжение
Время
Нажать

Тест

Отпустить

Рис. 6.6. Таймер для устранения дребезга кнопки
Это рабочий, но не самый лучший подход. Трудно выбрать значение таймера,
потому что время дребезга кнопки может впоследствии измениться из-за механического износа. Наверняка у вас даже был ненавистный будильник, в котором
кнопки были изношены до такой степени, что установить время было сложно.
Кроме того, на большинстве устройств имеется более одной кнопки, и маловероятно, что у процессора достаточно входов прерываний, чтобы их можно было
перемещать. Можно построить схему для разделения прерываний, но лучше
всего сделать это бесплатно программным способом. В большинстве систем
существует таймер, который может генерировать периодические прерывания.
Можно использовать это прерывание для устранения дребезга кнопки.
Предположим, что у нас есть восемь кнопок, подключенных к порту ввода/вывода, как на рис. 6.1, и что состояние порта ввода/вывода доступно
в переменной с именем INB — 8-битном unsigned char . Можно построить
фильтр с конечной импульсной характеристикой, или КИХ (FIR, finite impulse

Низкоуровневый ввод/вывод   191
response) из массива filter, как показано на рис. 6.7. КИХ — это очередь; на
каждом такте таймера мы отбрасываем самый старый элемент и переходим
на новый. Мы выполняем операцию ИЛИ над всеми элементами массива,
чтобы сформировать текущее состояние (current) как часть очереди из двух
элементов; current перемещается в предыдущее значение (previous), прежде
чем мы вычисляем новое current. Все, что нужно сделать, — выполнить операцию XOR для состояний current и previous, чтобы узнать, какие кнопки
изменили состояние.
1

1

1

1

1

1

0

0

0

0

0

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

0
INB

1
filter [0]

0
filter [1]

1
filter [2]

1
current

1
previous

Рис. 6.7. Устранение дребезга кнопки с помощью КИХ-фильтра
Все это очень просто переводится в код, как показано на языке программирования C в листинге 6.1.
Листинг 6.1. Устранение дребезга кнопки с помощью КИХ-фильтра
unsigned
unsigned
unsigned
unsigned

char
char
char
char

filter[FILTER_SIZE];
changed;
current;
previous;

previous = current;
current = 0;
for (int i = FILTER_SIZE - 1; i > 0; i--) {
filter[i] = filter[i - 1];
current |= filter[i];
}
filter[0] = INB;
current |= filter[0];
changed = current ^ previous;

FILTER_SIZE — количество элементов в фильтре, выбор которого зависит от

уровня шума кнопок и частоты прерываний таймера.

192   Глава 6. Разбор связей

Да будет свет
Во многих приборах есть дисплей. Я не имею в виду компьютерные экраны —
скорее речь о будильниках и посудомоечных машинах. Часто они имеют несколько световых индикаторов и иногда несколько простых числовых дисплеев.
Распространенным типом простых индикаторов является семисегментный
дисплей, показанный на рис. 6.8. Эти дисплеи имеют семь светодиодов, расположенных в виде восьмерки, а иногда и дополнительную десятичную точку.
A
F

A

B

B

C

D

E

F

G

DP

G
E

C
D

DP

Общая схема

Рис. 6.8. Семисегментный дисплей
Для восьми светодиодов на дисплее требуется 16 электрических соединений
(контактов). Но обычно они строятся не так; на одном конце каждого светодиода
есть вывод, а на другом — общее соединение. Поскольку нам нужно управлять
только одним концом, чтобы включить или выключить светодиод, это общее
соединение позволяет сэкономить на выводах, что снижает стоимость дисплея.
На рис. 6.8 показан дисплей с общим катодом, в котором все катоды связаны
вместе, а каждый анод имеет свои собственные выводы.
Можно просто подключить аноды к выходным контактам на процессоре, а катоды — к заземлению либо отрицательному полюсу источника напряжения
или источника питания. Высокий логический уровень (1) на выводе микроконтроллера включит соответствующий светодиод. На практике большинство
процессоров не обеспечивают достаточный ток для работы, поэтому используется дополнительная схема драйвера. Часто используются выходы с открытым
коллектором (показаны на рис. 2.36).
Программное обеспечение для управления одним из этих дисплеев довольно
простое. Все, что нам нужно, — таблица, которая отображает числа (и, возможно, буквы) в соответствующих освещаемых сегментах. Но неудивительно, что
при этом возникают осложнения. Единственный дисплей редко встречается;
например, у будильника их четыре. Хотя можно подключить каждый дисплей
к собственному порту ввода/вывода, маловероятно, что их будет так много.
Решение состоит в том, чтобы мультиплексировать дисплеи, подключив аноды
к порту A и катоды к порту B, как показано на рис. 6.9.
Аноды дисплея подключены параллельно; все сегменты A соединены между
собой, все сегменты B соединены между собой и т. д. Выводы общих катодов

Низкоуровневый ввод/вывод   193

A 0–7

Процессор
B0
B1
B2
B3

Рис. 6.9. Мультиплексные дисплеи
каждого дисплея подключаются к собственному выходу микроконтроллера.
Сегмент дисплея может загореться только в том случае, если на его анод подана 1, а на катод — 0. Вы можете спросить, почему, например, сегменты A и B не
загорятся, если A будет равно 1, а B равно 0. Помните, что буква D в аббревиатуре
LED означает диод, а диоды — это улицы с односторонним движением тока.
Дисплеи работают благодаря инерционности зрительного восприятия. Не обязательно держать дисплей постоянно включенным, чтобы воспринимать его как
включенный. Наши глаза и мозг будут считать, что он горит, если он включен
всего на 1/24 секунды. Тот же эффект используется в фильмах и видеороликах.
Все, что нам нужно сделать, — выбрать, какой дисплей включен, установив соответствующий катодный вывод на 0, а аноды сегмента — на то, что мы хотим отобразить1. Мы можем переключать отображение в обработчике прерывания таймера,
аналогичном тому, который мы использовали в предыдущем примере с кнопкой.

Свет, камера, мотор…
Обычно устройства включают в себя как кнопки, так и дисплеи. Как оказалось,
можно сэкономить пару выводов, мультиплексируя кнопки и дисплеи. Допустим,
мы имеем 12-кнопочную клавиатуру телефонного типа в дополнение к четырем
дисплеям, как показано на рис. 6.10.
Что мы получили благодаря всей этой сложности? Теперь можно использовать только три дополнительных вывода для двенадцати кнопок. Все кнопки
подтягиваются к логической единице с помощью подтягивающих резисторов.
Нажатие кнопки не имеет никакого эффекта, если не выбран ни один дисплей,
потому что все выходы B также равны 1. Если выбран крайний левый дисплей,
B0 имеет низкий уровень, и нажатие любой кнопки в верхнем ряду приведет к понижению уровня соответствующего входа C и т. д. Поскольку дисплей и кнопки
1

Этот принцип работы семисегментного дисплея называется динамической индика­
цией. — Примеч. науч. ред.

194   Глава 6. Разбор связей
+
A 0–7

B0
B1
Процессор
B2
B3

1

2

3

4

5

6

7

8

9

*

0

#

C0
C1
C2

Рис. 6.10. Мультиплексированные кнопки и дисплеи
сканируются одним и тем же набором сигналов, код, выполняющий сканирование, может быть объединен в обработчике прерывания таймера.
Обратите внимание, что на рис. 6.10 представлена упрощенная схема. На практике
контакты B должны быть устройствами с открытым коллектором или открытым
стоком (см. «Варианты выходов» на с. 101); в противном случае, если были нажаты
две кнопки в разных строках, но в одних и тех же столбцах, мы бы соединили 1 с 0,
что могло бы повредить электронные компоненты. Однако обычно это так не реализуется, поскольку вышеупомянутая схема драйвера дисплея делает это за нас.
Можно узнать, спроектировано ли устройство так, как показано на рис. 6.10, нажав одновременно несколько кнопок и наблюдая за дисплеями. Дисплеи будут
выглядеть странно. Подумайте почему.

Светлые идеи
Ваш будильник может иметь функцию регулировки яркости дисплея. Как это
устроено? Можно изменять режимработы дисплея согласно рис. 6.11.
Каждый дисплей светится четверть времени, как показано в левой части рис. 6.11.
Правая часть демонстрирует, что каждый дисплей светится только одну восьмую
времени; ни один из дисплеев не горит половину времени. В результате дисплеи
справа кажутся примерно вдвое тусклее дисплеев слева. «Яркость» связана со
средним временем работы дисплея. Обратите внимание, что зависимость между
режимом работы и воспринимаемой яркостью вряд ли будет линейной.

Низкоуровневый ввод/вывод   195

B0

B0

B1

B1

B2

B2

B3

B3
t0

t1

t2

t3

t0

t1

t2

t3

t4

t5

t6

t7

Рис. 6.11. Режим работы

2n оттенка серого
Обычная задача датчика — определить положение вращающегося вала, например двигателей, колес и ручек. Можно определить положение, используя
переключатели на валу или черные и белые точки, которые можно прочитать
с помощью фотодатчика. Какой бы подход мы ни выбрали, каждое положение
вала будет закодировано как двоичное число. Кодировщик может выглядеть
как на рис. 6.12, если нас интересуют восемь различных позиций. Если белые
секторы равны 0, а черные секторы — 1, то видим, как будет читаться значение
позиции. Радиальные линии не являются частью кодировщика; они нужны
только для того, чтобы диаграмму было легче понять.
Как обычно, схема кажется простой, но это не так. В этом случае проблема заключается в механических допусках. Обратите внимание, что даже при идеально
выровненном кодировщике у нас все равно будут проблемы, связанные с различиями в задержке распространения в схеме, считывающей каждый бит. Что
произойдет, если кодировщик выровнен не идеально, как на рис. 6.13?
2

1

2

1

3

0

3

0

4

7

4

7

5

6

Рис. 6.12. Двоичный датчик
угла поворота

5

6

Рис. 6.13. Погрешность центровки
датчика угла поворота

Вместо того чтобы прочитать ожидаемое 01234567, мы получаем 201023645467.
Американский физик Фрэнк Грей (Frank Gray) (1887–1969) из Bell Telephone
Laboratories рассмотрел эту проблему и придумал способ кодирования, в котором
для каждой позиции изменяется только значение одного бита. Для 3-битного
кодировщика, который мы рассматривали, одноименный код Грея равен 000, 001,
011, 010, 110, 111, 101, 100. Код можно легко преобразовать в двоичный с помощью
небольшой таблицы. На рис. 6.14 показана версия датчика с кодом Грея.

196   Глава 6. Разбор связей

1

3
2

0

6

4
5

7

Рис. 6.14. Датчик угла поворота с кодом Грея

Квадратура
В 2-битных кодах Грея есть уловка, которую можно использовать, когда не
обязательно знать абсолютное положение чего-либо, но нужно знать, когда положение изменяется и в каком направлении. Некоторые ручки на приборной
панели автомобиля, например регулятор громкости стереосистемы, скорее всего,
будут работать таким образом. Можно это проверить, если поворот ручки при
выключенном зажигании не дает никакого эффекта после запуска двигателя.
Метод называется квадратурным кодированием, потому что он использует четыре состояния. Двухбитный код Грея повторяется несколько раз. Например,
существуют дешевые квадратурные кодировщики, которые работают до 1/4096
оборота. Для квадратуры нужны всего два датчика, по одному на каждый бит.
Абсолютный кодировщик на 4096 позиций потребует 12 датчиков.
Квадратурный сигнал показан на рис. 6.15.
По часовой стрелке

Против часовой стрелки
X
Y
0

1

3

2

0

1

3

2

0

1

Рис. 6.15. Квадратурный сигнал
Как видите, когда вал вращается по часовой стрелке, получается последовательность 0132; против часовой стрелки — 2310. Можно сформировать 4-битное
число из текущей и предыдущей позиций. Это число указывает направление
вращения, как показано в табл. 6.1.
Обратите внимание, что это конечный автомат, где комбинированное значение
является состоянием.
Что получится, если взять пару квадратурных энкодеров, расставить их под
углом 90 градусов друг к другу и воткнуть в середину резиновый шарик? Компьютерная мышь.

Низкоуровневый ввод/вывод   197
Таблица 6.1. Обнаружение квадратурного вращения
Текущее
значение

Предыдущее
значение

Комбинированное
значение

Расшифровка

00

00

0

Неверные данные

00

01

1

По часовой стрелке

00

10

2

Против часовой стрелки

00

11

3

Неверные данные

01

00

4

Против часовой стрелки

01

01

5

Неверные данные

01

10

6

Неверные данные

01

11

7

По часовой стрелке

10

00

8

По часовой стрелке

10

01

9

Неверные данные

10

10

a

Неверные данные

10

11

b

Против часовой стрелки

11

00

c

Неверные данные

11

01

d

Против часовой стрелки

11

10

e

По часовой стрелке

11

11

f

Неверные данные

Параллельная связь
Параллельная связь — это расширение ситуации, которую мы видели ранее,
включая и выключая светодиоды. Мы могли подключить восемь светодиодов
к порту B и мигать кодами символов ASCII. Параллельное соединение означает,
что у нас есть провод для каждого компонента и мы можем управлять ими всеми
одновременно.
На старых компьютерах может быть параллельный порт IEEE 1284. Такие порты
обычно использовались для принтеров и сканеров до появления универсальной
последовательной шины (Universal Serial Bus, USB). И да, на параллельном порте
было восемь строк данных, поэтому можно было отправлять коды символов ASCII.
В связи со всем этим возникает проблема: как узнать, достоверны ли данные?
Допустим, вы отправляете символы ABC. Как узнать, какой символ появится
следующим? Отследить это не получится, потому что последовательность

198   Глава 6. Разбор связей
может измениться на AABC. Один из способов — получить еще один сигнал
прерывания для проверки состояния. В IEEE 1284 для этой цели предусмотрен
стробирующий сигнал. На рис. 6.16 данные в битах с 0-го по 7-й действительны
всякий раз, когда стробирующий импульс имеет низкий уровень или равен 0.
0x42
B

0x6f
o

0x6f
o

0x21
!

Бит 7
Бит 6
Бит 5
Бит 4
Бит 3
Бит 2
Бит 1
Бит 0
Строб

Время

1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0

Рис. 6.16. Синхронизация передачи параллельных данных
с помощью стробирующих импульсов
Еще один параллельный интерфейс, который практически ушел на второй
план, — это IDE. Он использовался для передачи данных на старых дисковых
накопителях.
Параллельные интерфейсы дороги, потому что для них требуется много выводов
I/O, выводов разъемов и проводов. Параллельный порт имел 25-контактный
разъем и большой толстый кабель. У IDE было 40 проводов. Существует предел
скорости передачи сигнала по проводу, и при его превышении потребуется еще
больше проводов.

Последовательная связь
Предпочтительно иметь возможность устанавливать соединение, используя как
можно меньше проводов, потому что они стоят денег, особенно когда речь идет
о больших расстояниях. Два провода — это минимальное необходимое количество, потому что, как вы узнали в главе 2, нам нужен путь для обратного электрического сигнала. Этот обратный путь опущен на диаграммах для простоты.
Как отправить восемь сигналов по одному проводу? Подсказка есть на диаграмме времени на рис. 6.16. Несмотря на то что каждый бит находится на своем
собственном проводе, символы разнесены во времени. Биты мы тоже можем
разнести по времени.

Низкоуровневый ввод/вывод   199
Я говорил о регистрах сдвига в разделе «Сдвиг» на с. 143. На передающей стороне стробирующий, или тактовый, сигнал сдвигает все биты на одну позицию
и отправляет бит, оказывающийся в конце, в линию. На принимающей стороне
генератор тактовых сигналов сдвигает все биты на одну позицию и переводит
состояние канала связи в новую освобожденную позицию, как показано на
рис. 6.17.
Ввод

Вывод

Регистр сдвига
передатчика

Данные

Регистр сдвига
приемника

Генератор
тактовых сигналов

Рис. 6.17. Последовательная связь с использованием регистров сдвига
Можно использовать счетчик, чтобы получить уведомление, когда дойдем до
8 бит, и тогда обработать значение. Этот подход требует двух проводов, а не
одного, и он часто приводит к ошибкам. При этом нужно, чтобы передатчик
и приемник были синхронизированы (in sync, что не имеет ничего общего с одноименной поп-группой). Достаточно пропустить один тактовый сигнал, и все
пойдет прахом. Можно добавить третий провод для сообщения о новом символе,
но наша цель — минимизировать количество проводов.
Давным-давно (в начале XX века) телеграф скрестили с пишущей машинкой,
что привело к появлению телетайпа — телеграфного буквопечатающего аппарата, который позволял печатать символы на удаленной печатной машинке.
Изначально телетайпы использовались для передачи информации о фондовом
рынке по телеграфным проводам.
Данные отправлялись по последовательному протоколу (набору правил), который работал с использованием только одного провода в дополнение к обратному
проводу. В этом протоколе замечательно то, что он работал как таймер на соревнованиях по плаванию. Все участники запускают свои личные таймеры при
срабатывании стартового пистолета, и они находятся достаточно близко друг
к другу, чтобы это сработало. Рисунок 6.18 иллюстрирует работу протокола.
Первый символ
0
Начало

1

2

3

4

5

Второй символ
6

7

0

1

2

3

4

5

6

Стоп Начало
Время

Рис. 6.18. Передача данных с помощью меток и пробелов

7
Стоп

200   Глава 6. Разбор связей
Линия здесь находится в состоянии 1, или высоком, когда ничего не происходит.
Высокое состояние называется меткой, а низкое — пробелом. Это именование
появилось из-за того, что раннее телеграфное оборудование либо делало отметку, либо оставляло пустое место на полосе бумаги. Переход из высокого уровня
в низкий, отмеченный как «Начало» на рис. 6.18, работает как стартовый пистолет и называется стартовым битом. После стартового бита отправляются 8 бит
данных. Символ заканчивается парой старших стоповых битов. Каждому биту
отводится одинаковое количество времени. Могут возникнуть ошибки синхронизации, но все, что нужно сделать передатчику в таком случае, — остановиться
на время отправки одного символа, и приемник синхронизируется заново. Мы
разделяем время так, чтобы получить слот для каждого бита, а затем мультиплексируем данные по одному проводу. Этот метод, называемый мультиплексированием с временным разделением, может быть реализован с использованием
селектора (см. «Построение селекторов» на с. 108) вместо регистра сдвига.
Кстати, скорость в битах в секунду известна как скорость в бодах, названная
в честь французского инженера Эмиля Бодо (Émile Baudot) (1845–1903).
Телетайпы были потрясающими машинами. В них не было никакой электроники, и они работали за счет вращения вала двигателя. Электромагнит отпускал
вал, когда появлялся начальный бит, чтобы тот мог вращаться. В каждом месте
вращения для смены положения бита перемещались всевозможные кулачки,
рычаги и толкатели, и в конечном итоге металлический символ дотрагивался
до окрашенной ленты, а затем и до листа бумаги. Вы знали, что поступило новое
сообщение, если аппарат начинал грохотать. Клавиатура работала аналогично.
Нажатие клавиши запускало вращение вала, который перемещал электрический
контакт, в зависимости от того, какие клавиши были нажаты, для генерации
кода ASCII.
Еще один крутой трюк, называемый полудуплексным соединением, заключается
в том, что передатчик и приемник используют один и тот же провод. Одновременно говорить можно только по одному из них, иначе в результате получится
тарабарщина. Вот почему радисты произносили что-то вроде «прием». Вы знаете
все о полудуплексной связи, если когда-либо пользовались рацией. Если несколько передатчиков активны одновременно, возникает коллизия — искажение
данных. Полнодуплексное соединение — это два провода, по одному на каждое
направление.
Все схемы для реализации этого принципа в итоге стали доступны в одной интегральной микросхеме под названием UART, что означает Universal Asynchronous
Receiver-Transmitter — универсальный асинхронный приемник-передатчик. UART
также можно реализовать в программном обеспечении, используя так называемый бит-бэнгинг (bit-banging).
Стандарт под названием RS-232 определил уровни напряжения, используемые
для меток и пробелов на старых последовательных портах, а также множество

Низкоуровневый ввод/вывод   201
дополнительных сигналов управления. Сейчас он в значительной степени заменен USB, хотя вариант под названием RS-485, который использует дифференциальную передачу сигналов (см. рис. 2.32) для большей помехоустойчивости,
используется в промышленных средах. Параллельный интерфейс IDE для дисков был заменен последовательным эквивалентом SATA. Электроника сейчас
достаточно быстрая, чтобы выполнять операции последовательно, хотя раньше
они происходили параллельно. К тому же провода остаются дорогими. В мире
не хватает меди, которая добывается как полезное ископаемое и используется
в качестве проводника. Основной источник меди сейчас — переработка существующих медных изделий. Чипы в основном состоят из кремния, который
в большом количестве содержится в песке.
Существует ряд последовательных интерфейсов, предназначенных для подключения периферийных устройств к небольшим микрокомпьютерам. К ним
относятся SPI, I2C, TWI и OneWire.

Поймать волну
Один из минусов передачи данных с метками и пробелами заключается в том,
что она не подходит для очень больших расстояний. Данный подход не работает при использовании телефонных линий по причинам, выходящим за
рамки этой книги. Это очень важно, потому что после замены телеграфа более
совершенными технологиями связь на дальних расстояниях стала возможна
только с помощью телефона или радио. Проблема передачи данных с метками и пробелами решается с помощью того же метода, который заставляет
работать радио.
На Вселенную влияют самые разные волны. Они бывают морскими, звуковыми, световыми, также существуют микроволны и множество других разновидностей. Волна-основа — это синусоида. Все другие формы волн представляют
собой комбинации синусоидальных волн. Вы получите синусоидальную волну,
построив график зависимости высоты точки на окружности от угла, — нечто
похожее на рис. 6.19.
1
0
–1


90°

180°

270°

360°

720°

Рис. 6.19. Синусоидальная волна
Высота синусоиды — это амплитуда. Число пересечений с нулем в одном
направлении в секунду — это частота, измеряемая в герцах, названных по

202   Глава 6. Разбор связей
имени немецкого физика Генриха Герца (1857–1894). Герц сокращенно обозначается как Гц и является синонимом циклов в секунду. Расстояние между
двумя пересечениями с нулем в одном направлении — это длина волны. Они
связаны следующим образом:
v
λ= .
f
В этом уравнении λ — длина волны в метрах, f — частота в герцах, а v — скорость
волны в среде, в которой она движется. Радиоволны движутся со скоростью света. Чем выше частота, тем короче длина волны. В качестве ориентира — частота
ноты до первой октавы составляет около 261 Гц.
Если вы поразмышляете об этом еще немного, то поймете, что свойства разных волн отличаются. Звуковые волны не распространяются очень далеко, их
останавливает вакуум, но они огибают препятствия на пути. Световые волны
проходят очень длинный путь, но их останавливают стены. Некоторые частоты
радиоволн проходят сквозь стены, а другие — нет. Между ними много различий.
Пришло время серфить. Найдем нужную волну и покатаемся. Назовем эту волну несущей и будем модулировать или изменять ее в зависимости от нужного
сигнала, например формы волны с метками и пробелами.
AT&T представила набор данных Bell 103A в начале 1960-х годов. Он обеспечивал полнодуплексную связь на колоссальной скорости 300 бод по телефонной
линии с использованием четырех звуковых частот; каждый конец соединения
получил свою пару сигналов с метками и пробелами. Это называется частотной манипуляцией (frequency shift keying, FSK), потому что частота сдвигается
вместе с метками и пробелами (рис. 6.20).
Начало

0

1

2

3

4

5

6

7

Конец

Рис. 6.20. Частотная манипуляция — буква A в ASCII
Принимающая сторона должна превратить звук обратно в метки и пробелы, что
называется демодуляцией в противоположность модуляции. Устройства, которые это делают, называются модемами. Странные шумы, которые вы слышите,
когда кто-то выходит в интернет по диал-апу или отправляет факс в дурацком
фильме, — это частоты, используемые модемами.

Универсальная последовательная шина
Стандарт USB не так интересен, но о нем стоит упомянуть, потому что он очень
распространен. В нем больше несовместимых и сложных в использовании

Сети   203
разъемов, чем в любом другом стандарте, и, возможно, он более важен для зарядки устройств, чем для передачи данных.
USB заменил многие громоздкие разъемы на компьютерах, бывшие в обиходе
в середине 1990-х годов, такие как PS/2, RS-232 и параллельные порты с одним
четырехпроводным разъемом. Мы использовали два провода питания и витую
пару для передачи данных с применением дифференциальной сигнализации.
USB повторяет модель, с которой мы столкнемся в ближайшем будущем:
«не могу остановиться на достигнутом», так что теперь USB Type-C имеет до
24 проводов, что совсем немного по сравнению со старым параллельным портом.
USB все же не так прост. Существует контроллер, который отвечает за обмен
данными с конечными точками. Передача данных структурирована; это не просто копание в непонятной информации. Используется обычная техника: данные
передаются пакетами, которые похожи на пакеты, отправляемые по почте. Пакеты
содержат заголовок и необязательную полезную нагрузку. Заголовок — это, по сути,
информация, которую можно найти на посылке: откуда она пришла, куда идет,
класс почтовых отправлений и т. д. Полезная нагрузка — это содержимое пакета.
USB обрабатывает аудио и видео посредством изохронной передачи. Конечная
точка может запросить зарезервировать определенную часть полосы пропускания
(скорости передачи данных), что дает гарантию передачи данных. Контроллер
отклоняет запрос, если пропускной способности недостаточно.

Сети
Трудно составить четкое представление о современном мире сетевых технологий, не зная их истоков. Меня сводит с ума, когда моя дочь говорит: «Wi-Fi не
работает» или «Интернет не работает», потому что это не одно и то же. Попытки
объяснить ей это наталкиваются на фирменное подростковое закатывание глаз
и взмах волос.
Для описания сетей используются две общие классификации. Локальная сеть
(local area network, LAN) — это сеть, охватывающая небольшое пространство,
например дом или офис. Глобальная сеть (wide area network, WAN) охватывает
большую географическую область. Эти термины несколько расплывчаты, поскольку нет точного определения малых и больших областей.
Первоначальное представление сети — это телеграфная сеть, которая превратилась в сеть телефонную. Я еще не веду речь о компьютерной сети, потому что
компьютеров в то время не существовало. Первоначальная телефонная сеть была
сетью с коммутацией каналов. Во время звонка между двумя сторонами их провода фактически соединялись вместе, образуя цепь. Выражение «с коммутацией»
означало, что это соединение существовало только во время разговора. После
завершения вызова можно было создавать новые цепи.

204   Глава 6. Разбор связей
За некоторыми исключениями, такими как все еще существующие стационарные
телефоны, телефонная система теперь представляет собой сеть с коммутацией
пакетов. Я упомянул пакеты в последнем разделе. Обмен данными делится на
пакеты, которые включают адреса отправителя и получателя. Пакеты могут
совместно использовать провода с мультиплексированием с временным разделением (описано ранее в разделе «Последовательная связь» на с. 198), что
позволяет более эффективно использовать каналы; это стало возможным, когда
количество данных, которые можно было передать по проводам, стало больше,
чем нужно только для голоса.
Одна из первых компьютерных сетей входила в состав Semi-Automatic Ground
Environment (SAGE), системы обороны времен холодной войны. Она использовала модемы в телефонной сети для связи между объектами.
Многие организации начали экспериментировать с локальными сетями в конце
1960-х годов. Например, моя лаборатория в Bell разрабатывала графические терминалы, которые были подключены к компьютеру Honeywell DDP-516 нашего
отдела через локальную сеть, называемую кольцом. В то время периферийные
устройства, такие как ленточные накопители и принтеры, были очень дорогими,
и у большинства отделов не было своей собственной периферии. Но все нужные
устройства были доступны в главном вычислительном центре. Наш компьютер
был подключен к модему, и когда ему требовалось что-то, чего у него не было,
он просто звонил в компьютерный центр. Фактически это была глобальная
сеть. Мы могли не только посылать документы на печать, но и отправлять программы для запуска — и компьютерный центр соединялся с нашей машиной
для передачи результатов.
Аналогичная деятельность происходила во многих исследовательских лабораториях и компаниях. Было изобретено много разных локальных сетей. Однако
все они существовали в собственных вселенных и не могли разговаривать друг
с другом. Модемы и телефонные линии были основой для глобальной связи.
Набор компьютерных программ, разработанных в Bell Labs, под названием UUCP
(от UNIX-to-UNIX copy) был выпущен для внешнего мира в 1979 году. UUCP
позволял компьютерам звонить друг другу для передачи данных или удаленного
запуска программ. Он лег в основу первых систем электронной почты и новостей,
таких как USENET. Эти системы представляли собой интересную лазейку. Если
вы хотите отправить данные внутри страны, они будут перескакивать с компьютера на компьютер, пока не дойдут до места назначения. Это обычно позволяло
избежать платы за междугороднюю телефонную связь.
Между тем ARPA (Advanced Research Projects Agency — Управление перспективных исследований Министерства обороны США) финансировало разработку
ARPANET, глобальной сети с коммутацией пакетов. ARPANET превратилась
в интернет в 1990-х годах. Сегодня большинство людей воспринимают интернет как должное и, как и моя дочь, вероятно, думают, что это синоним сети.

Сети   205
Но его настоящая природа указана прямо в названии: сокращение «интер» — от
inter — «взаимодействие», «нет» — от net — «сеть». Интернет — это сеть сетей,
глобальная сеть, которая соединяет локальные сети.

Современные локальные сети
Многие другие вещи, которые мы считаем само собой разумеющимися в наши
дни, были изобретены в исследовательском центре Xerox в Пало-Альто (Palo
Alto Research Center, PARC) в середине 1970-х годов. Например, американский инженер-электротехник по имени Боб Меткалф (Bob Metcalfe) изобрел
Ethernet, который представляет собой локальную сеть, потому что не рассчитан
на большие расстояния.
ПРИМЕЧАНИЕ
Более подробную информацию об истории PARC можно найти в книге Адель Голдберг (Adele Goldberg) «A History of Personal Workstations» (Addison-Wesley, 1988).
Изначально Ethernet был полудуплексной системой. Каждый компьютер был
подключен к одному проводу. У каждого компьютерного сетевого интерфейса
был уникальный 48-битный адрес, называемый адресом управления доступом
к среде, или MAC-адресом (Media Access Control, MAC), и он все еще актуален
сегодня. Данные организованы в пакеты, называемые фреймами, размером около
1500 байт. Фреймы имеют заголовок, который включает адрес отправителя, адрес
получателя и некоторые проверки ошибок (например, проверки циклическим
избыточным кодом, или CRC, как описано в разделе «Обнаружение и исправление ошибок» на с. 133) вместе с полезными данными.
Обычно один компьютер говорит, а другие слушают. Компьютеры, не совпадающие с MAC-адресом получателя, игнорируют данные. Каждая машина слушала,
что происходило, и не передавала данные, если это делал кто-то другой. Когда
компьютеры начинали передавать данные одновременно, коллизия приводила
к искажению пакетов — та же ситуация, что и при коллизиях полудуплексных
систем. Одним из больших нововведений Меткалфа стал принцип «случайный
откат и повторный запуск». Машина, которая пыталась «поговорить», ожидала
в течение случайного времени, а затем пыталась повторно отправить пакеты.
Ethernet все еще используется сегодня, хотя и не в полудуплексной версии.
Теперь компьютеры подключены к маршрутизаторам, которые отслеживают,
какая машина в каком соединении находится, и направляют пакеты в нужные
места. При этом коллизии больше не возникают. Wi-Fi — это, по сути, версия
Ethernet, в которой вместо проводов используется радио. Bluetooth — еще одна
популярная система LAN. Представьте его как версию USB, которая сменила
провода на радио.

206   Глава 6. Разбор связей

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

TCP/IP
Протокол управления передачей/интернет-протокол (Transmission Control
Protocol/Internet Protocol, TCP/IP) — это пара протоколов, на которых построен интернет. IP получает пакеты для передачи из одного места в другое. Эти
пакеты, называемые дейтаграммами, похожи на телеграммы для компьютеров.
Как и в случае с настоящими телеграммами, отправитель не знает, когда адресат
получил сообщение и пришло ли оно ему вообще. TCP накладывается поверх IP
и обеспечивает надежную доставку пакетов. Это довольно сложная работа, потому что большие сообщения охватывают множество пакетов, которые, возможно,
поступают не по порядку, поскольку могли идти по разным маршрутам, — это похоже на заказ нескольких вещей и их отправку в разных коробках. Коробки могут
не прибыть в один и тот же день, или их могут доставлять разные перевозчики.

IP-адреса
Каждый компьютер в интернете имеет уникальный адрес, известный как IPадрес. В отличие от MAC-адресов IP-адреса, не привязаны к оборудованию
и могут изменяться. Система IP-адресов — это иерархическая система, в которой
кто-то выдает блоки адресов кому-то, кто также выдает блоки адресов, и т. д., пока
блок адресов не перейдет к тому, кто предоставит компьютеру собственный адрес.
Интернет в основном работает по IPv4, IP версия 4, которая использует 32-битный адрес. Адреса представлены в октетной записи xxx.xxx.xxx.xxx, где каждый
xxx — это 8 из 32 бит, записанных в десятичном формате. Это более 4 миллиардов
адресов, но и этого недостаточно. Теперь, когда у всех есть адреса настольных
компьютеров, ноутбуков, планшетов, мобильных телефонов и других устройств,
свободных адресов не осталось. Следовательно, мир постепенно переходит
на IPv6, работающий с 128-битными адресами.

Система доменных имен
Как вас найти, если ваш адрес изменится? Этим занимается система доменных
имен (Domain Name System, DNS), которая похожа на телефонную книгу — для
тех, кто помнит, что это такое. DNS сопоставляет имена с адресами. Она знает,
что сайт whitehouse.gov имеет IP-адрес 23.1.225.229, в то время когда я пишу эти

Аналоговые устройства в цифровом мире   207
строки. Это что-то вроде адресной книги в телефоне, которую нужно постоянно
обновлять; DNS позаботится обо всем при переезде.

Всемирная паутина
Многие другие протоколы построены на основе TCP/IP, например Simple Mail
Transfer Protocol (SMTP), обеспечивающий работу электронной почты. Одним
из наиболее часто используемых протоколов является HTTP — сокращение
от HyperText Transfer Protocol, который используется для веб-страниц, наряду
с HTTPS, где S означает «безопасный» (от secure).
Гипертекст — это просто текст со ссылками. Американский инженер Вэнивар
Буш (Vannevar Bush) (1890–1974) придумал его в 1945 году. Тим БернерсЛи (Tim Berners-Lee), ученый из CERN (European Organization for Nuclear
Research), изобрел Всемирную паутину, чтобы физики могли обмениваться
информацией.
Стандарт HTTP определяет, как веб-браузеры взаимодействуют с веб-серверами.
Веб-браузеры — это то, что вы используете для просмотра веб-страниц. Вебсерверы отправляют эти страницы по запросу. Веб-страницы обнаруживаются
и выбираются по унифицированному указателю ресурсов (Uniform Resource
Locator, URL), адресу веб-сайта в адресной строке браузера. Это способ поиска нужной информации, включающий доменное имя компьютера в интернете
и описание того, где найти информацию на компьютере.
Веб-страницы обычно создаются на HTML (сокращение от HyperText Markup
Language — «язык гипертекстовой разметки»), наиболее распространенном
языке для их написания. К HTML с течением времени прилипло множество
всего, и теперь это довольно адская смесь. Подробнее об этом в главе 9.

Аналоговые устройства в цифровом мире
Компьютеры используются во многих развлекательных устройствах, от аудио­
плееров до телевизоров. Возможно, вы заметили, что цифровые фотографии выглядят не очень хорошо, если их увеличить выше определенного предела. Наше
восприятие звука и света непрерывно, но компьютеры не могут хранить непрерывные данные. Данные нужно дискретизировать, что означает необходимость
снимать показания в определенные моменты во времени и/или в пространстве.
Затем из этих выборок требуется восстановить аналоговый (непрерывный)
сигнал для воспроизведения.
Дискретизация не нова. Даже во времена немого кино сцена снималась со
скоростью около 16 кадров в секунду. Существует целая область науки под
названием дискретная математика, которая занимается дискретизацией. Дискретно, разумеется.

208   Глава 6. Разбор связей
ПРИМЕЧАНИЕ
В интернете есть хорошее видео под названием Episode 1: A Digital Media Primer for
Geeks. Оно представляет собой понятное введение в дискретизацию. Есть и вторая
серия — она тоже неплоха, но может немного запутать. Хотя все сказанное там
технически верно, все описанные принципы относятся только к моно-, а не к стерео­
сигналам. Ведущий подразумевает, что они подходят для стерео, но это не так.
Мы говорили о различиях между аналоговыми и цифровыми сигналами еще
в главе 2. Эта книга посвящена цифровым компьютерам, и многие реальные
приложения требуют, чтобы компьютеры генерировали или интерпретировали
аналоговые сигналы или делали и то и другое. В следующих разделах обсуждается, как это происходит.

Цифро-аналоговое преобразование
Как сгенерировать аналоговое напряжение на основе цифрового числа? Быстрый
и правильный ответ: с помощью цифро-аналогового преобразователя. Как его
построить?
Вернемся к рис. 6.1, где представлен светодиод, подключенный к порту ввода/вывода. На рис. 6.21 светодиод подключается к каждому из восьми контактов порта B.
PB 7
PB 6
PB 5
PB 4
PB 3
PB 2
PB 1
PB 0

Рис. 6.21. Цифро-аналоговый преобразователь с использованием светодиодов
Теперь можно генерировать девять различных уровней света — от восьми выключенных светодиодов до восьми включенных. Но 9 уровней из 8 бит — не
очень хорошее использование битов; из 8 бит мы должны получить 256 уровней.
Как? Так же, как и с числами. На рис. 6.22 один светодиод подключается к биту 0,
два — к биту 1, четыре — к биту 2 и т. д.
Это целая куча светодиодов. Повесьте эту схему на воздушный шар, чтобы получился светодиодный дирижабль (LED zeppelin). Двигаясь дальше, вы поймете,
что это отражение двоичного представления чисел. Бит 1 дает в два раза больше
света, чем бит 0, бит 2 — в четыре раза больше и т. д.

Аналоговые устройства в цифровом мире   209

. . . всего 128 светодиодов . . .
PB 7
. . . всего 64 светодиода . . .
PB 6
. . . всего 32 светодиода . . .
PB 5
. . . всего 16 светодиодов . . .
PB 4
PB 3
PB 2
PB 1
PB 0

Рис. 6.22. Улучшенный цифро-аналоговый преобразователь
с использованием светодиодов
Мы использовали пример со светодиодами, чтобы осветить работу цифро-аналогового преобразователя. Настоящий цифро-аналоговый преобразователь
(ЦАП) вырабатывает напряжение вместо света. Термин «разрешение» в общих
чертах используется для описания числа «шагов», которое может произвести
ЦАП. Я говорю «в общих чертах», потому что обычно говорят, что ЦАП имеет,
например, разрешение 10 бит, но на самом деле это означает, что он имеет разрешение «1 часть из 210». Чтобы дать полностью правильное определение, разрешение — это максимальное напряжение, которое ЦАП может производить,
деленное на количество шагов. Например, если 10-битный ЦАП может выдавать
максимум 5 В, то он имеет разрешение примерно 0.005 В1.
На рис. 6.23 показано условное обозначение, используемое для ЦАП.

D 0–n

Vвыхода

Рис. 6.23. Условное обозначение ЦАП

1

5/1024 В. — Примеч. науч. ред.

210   Глава 6. Разбор связей
С помощью ЦАП можно генерировать аналоговые сигналы. Именно так работают
аудиоплееры и музыкальные синтезаторы. Все, что нужно делать, — регулярно
менять входы ЦАП. Например, с помощью 8-битного ЦАП, подключенного к порту B, можно генерировать сигнал пилообразной формы, показанный на рис. 6.24.
int i = 0;
при (true)
PORTB = i++;

Рис. 6.24. Синтезированный сигнал пилообразной формы
Для более сложных сигналов устройства обычно включают память, куда можно
записывать данные, которые затем считываются дополнительными схемами. Это
обеспечивает постоянную скорость передачи данных, не зависящую от того, чем
в этот момент занимается ЦП. Типичный способ реализации этого — создание
конфигурации FIFO («first in, first out» — «первым пришел — первым ушел»),
как показано на рис. 6.25. Обратите внимание, что FIFO — это то же самое, что
и очередь в программировании.
Память
Шина данных Вход

Выход

Высшая точка

ЦАП

Аналоговый
выход

Низшая точка

Рис. 6.25. FIFO с высшими и низшими точками
С памятью FIFO связаны два триггера: высшая и низшая точки, которые заимствуют свою терминологию из приливов и отливов (high-water mark — высшая
отметка прилива, low-water mark — низшая отметка отлива). Низшая точка
вызывает прерывание, когда очередь FIFO почти пустая; высшая срабатывает,
когда очередь почти заполнена. Таким образом, программное обеспечение более высокого уровня может сохранять память заполненной для непрерывного
вывода. Хотя это не совсем FIFO, потому что новые данные смешиваются со
старыми. Именно так работают водонапорные башни; когда вода опускается
ниже низшей отметки уровня воды, включается насос для наполнения бака; при
достижении высшей отметки насос отключается. FIFO действительно удобны
для объединения вещей, работающих с разной скоростью.

Аналого-цифровое преобразование
Аналого-цифровое преобразование — это обратный процесс, выполняется с помощью аналого-цифрового преобразователя, или АЦП, который сложнее ЦАП.

Аналоговые устройства в цифровом мире   211
Первая проблема, с которой сталкивается АЦП, — как заставить аналоговый
сигнал оставаться неподвижным, потому что невозможно измерить колеблющийся сигнал. (Вы понимаете, о чем речь, если когда-нибудь пытались измерить
температуру у маленького ребенка.) На рис. 6.26 требуется взять выборку формы
входного сигнала — больше одной, если нужно, чтобы оцифрованная версия
напоминала аналоговый оригинал. Это делается с помощью схемы, называемой
«выборка и хранение», которая является аналоговым эквивалентом цифрового
триггера-защелки (см. «Триггеры-защелки» на с. 113).
Аналоговый вход

Хранилище

Аналоговая выборка

Выборка

Рис. 6.26. Выборка и хранение
Когда мы принимаем выборку, замыкая переключатель, текущее значение аналогового сигнала сохраняется в хранилище. Имея стабильный сигнал в хранилище, мы можем его измерить, чтобы сгенерировать цифровое значение. Нам
нужно что-то, что сравнивает сигнал с порогом, подобным тому, что мы видели
в правой половине рис. 2.7 в главе 2. К счастью, аналоговая схема, называемая
компаратором, может определить, когда одно напряжение больше другого. Это
похоже на логический вентиль, за исключением того, что можно задать значение
порога. Схематический символ компаратора показан на рис. 6.27.
+

Выход



Рис. 6.27. Аналоговый компаратор
Выход равен 1, если сигнал на входе «+» больше или равен сигналу на входе «–».
Можно использовать стек компараторов с разными опорными напряжениями
на входах — для создания быстродействующего преобразователя, как показано
на рис. 6.28.
Эта система называется быстродействующим преобразователем, потому что
выдает результаты быстро, в мгновение ока. Как вы можете видеть, выходы
следующие: 00000000 для напряжения менее 0.125 В, 000000001 для напряжения от 0.125 до 0.250 В, 00000011 для напряжения от 0.250 до 0.375 В и т. д. Это
работает, но с той же проблемой, что и у ЦАП на рис. 6.25: неэффективным использованием битов. Быстродействующие преобразователи также относительно
дороги из-за большого количества компараторов, но они прекрасно подходят,
когда требуется экстремальная скорость. Как создать более дешевый АЦП,
который лучше использует биты?

212   Глава 6. Разбор связей

1.000V

+


D7

0.875V

+


D6

0.750V

+


D5

0.625V

+


D4

0.500V

+


D3

0.375V

+


D2

0.250V

+


D1

0.125V

+


D0

Данные из выборки ихранения

Рис. 6.28. Быстродействующий преобразователь
В быстродействующем преобразователе применялся набор фиксированных
опорных напряжений, по одному на каждый компаратор. Можно использовать
один компаратор при регулируемом опорном напряжении. Как его получить?
При помощи ЦАП!
На рис. 6.29 видно, что компаратор используется для проверки значения выборки в хранилище по сравнению со значением ЦАП. После сброса счетчик ведет
отсчет, до тех пор пока значение ЦАП не достигнет значения выборки, — затем
счетчик отключается и выдает результаты. Таким образом, счетчик содержит
оцифрованное значение образца.
Аналоговый вход

Хранилище
Выборка

+

Выход


Vвыхода
D 0–n

Q 0–n Включение
Счетчик
Сброс

Рис. 6.29. Аналого-цифровой преобразователь

Результат

Аналоговые устройства в цифровом мире   213
Увидеть, как это работает, можно на рис. 6.30. Аналоговый сигнал колеблется,
но выходной сигнал хранилища остается стабильным после получения выборки. Затем счетчик очищается и ведет отсчет, до тех пор пока выходной сигнал
ЦАП не достигнет значения выборки. После чего счетчик остановится, и мы
получим результаты.
Хранилище
Аналоговый вход

Получить Очистить
выборку счетчик

Результат

Рис. 6.30. АЦП в работе
Этот АЦП называется преобразователем сравнения с пилообразным сигналом,
из-за того что выходной сигнал ЦАП генерирует пилообразное изменение. Одна
из проблем, связанных с таким преобразователем, заключается в том, что его
работа может занять много времени, поскольку время преобразования является
линейной функцией значения дискретизированного сигнала. Если дискретизированный сигнал имеет максимальное значение и у нас есть n-битный АЦП,
преобразование может занять 2n тактов.
Один из способов обойти это ограничение — использовать преобразователь последовательного приближения, который выполняет двоичный поиск аппаратно,
как показано на рис. 6.31.

Аналоговый вход

Получить
выборку

Очистить Результат
счетчик

Рис. 6.31. АЦП последовательного приближения в работе
Первый генератор тактовых сигналов устанавливает ЦАП на половину полного диапазона. Поскольку это меньше дискретизированного сигнала, он

214   Глава 6. Разбор связей
настраивается на четверть диапазона. Это чересчур много, поэтому теперь его
нужно уменьшить на одну восьмую диапазона, что, в свою очередь, слишком
мало, поэтому его увеличивают на одну шестнадцатую полного диапазона —
и это дает результат. В худшем случае требуется log2 n тактов. Это действительно
упростило задачу.
Термин «разрешение» используется для АЦП аналогично тому, как он применяется для ЦАП. Условное обозначение представлено на рис. 6.32.

Vвхода

D 0–n

Рис. 6.32. Условное обозначение АЦП

Цифровое аудио
Аудио включает в себя дискретизацию в одном измерении, то есть вычисление
амплитуды или высоты сигнала в определенные моменты времени. Посмотрите
на синусоидальный сигнал на рис. 6.33. У нас есть прямоугольный сигнал с некоторой частотой дискретизации, и мы записываем высоту сигнала на каждом
нарастающем фронте с помощью аналого-цифрового преобразователя.
0.743

0.588

–0.866

–0.407

0.951

0.208

–0.995

0.000

1
0
–1
Время

Рис. 6.33. Дискретизация синусоидальной волны
Получив набор образцов, мы должны иметь возможность восстановить исходный сигнал, подав их на ЦАП. Давайте попробуем это сделать, как показано на
рис. 6.34.
1
0
–1
Время

Рис. 6.34. Восстановленная синусоида из выборок
Ой, получилось ужасное искажение. Похоже, нам понадобится намного больше
выборок, чтобы улучшить результат, как на рис. 6.35.

Аналоговые устройства в цифровом мире   215

1
0
–1
1
0
–1
Время

Рис. 6.35. Высокочастотная выборка и реконструкция
Однако нам это не нужно. Выборки и реконструкции на рис. 6.33 и 6.34 на
самом деле достаточно. Я объясню почему, но имейте в виду: впереди тяжелая
теория.
Синусоидальную волну относительно легко описать, как упоминалось в разделе
«Поймать волну» на с. 201. Но нам нужен способ представить более сложные
формы волны, такие как на рис. 6.31.
На графиках показана зависимость амплитуды от времени, но можно построить
его по-другому. Взгляните на партитуру на рис. 6.36.

Частота

Время

Рис. 6.36. Музыкальная партитура
Видим, что ноты зависят от времени, но, помимо этого, происходит что-то еще.
В каждый момент времени играются не просто ноты, а аккорды, состоящие из
нескольких нот. Посмотрим на первый аккорд с нотами G4 (400 Гц), B4 (494 Гц)
и D5 (587 Гц). Представьте, что мы играем аккорд на синтезаторе, который может генерировать синусоидальные волны для нот. На рис. 6.37 видно, что, хотя
каждая нота является синусоидальной волной, сам аккорд представляет собой
более сложную форму волны — сумму трех нот. Оказывается,любую форму
волны можно представить как взвешенную (умноженную на некоторый масштабный коэффициент) сумму набора синусоидальных волн. Например, если
прямоугольная волна на рис. 6.33 имеет частоту f, ее можно представить как
сумму синусоидальных волн:

216   Глава 6. Разбор связей

D 5 (587 Гц)

B 4 (494 Гц)

G 4 (400 Гц)

Соль-мажорный аккорд
(G major)

Рис. 6.37. Форма волны соль-мажорного аккорда
Если у вас хороший слух, вы можете прослушать такой аккорд и выделить
составляющие ноты. Немузыкальным людям приходится полагаться на математическую акробатику под названием «преобразование Фурье», изобретенную французским математиком и физиком Жан-Батистом Жозефом Фурье
(1768–1830), который также открыл парниковый эффект. Все графики, которые
мы видели в этом разделе, показывают зависимость амплитуды от времени.
Преобразование Фурье позволяет построить график зависимости амплитуды
от частоты. Благодаря этому можно взглянуть на зависимость с другой стороны. Преобразование Фурье для соль-мажорного аккорда будет выглядеть, как
на рис. 6.38.
G4

B4

D5

400

494

587

Амплитуда
0

Частота

Рис. 6.38. График преобразования Фурье
для соль-мажорного аккорда
Вы, наверное, видели подобное раньше, даже не подозревая об этом. Во многих
медиаплеерах добавлены приятные глазу анализаторы спектра, отображающие
громкость в различных частотных диапазонах с использованием преобразования
Фурье. Анализаторы спектра возникли как сложное электронное оборудование.
Теперь их можно реализовать на компьютерах с помощью алгоритма быстрого
преобразования Фурье (БПФ). Одно из самых крутых применений анализа
Фурье — орган Hammond B-3.

Аналоговые устройства в цифровом мире   217

ОРГАН ХАММОНДА B-3
Hammond B-3 — удивительный пример применения теории электромагнитных
волн и анализа Фурье. Он работает так: двигатель приводит в движение вал, на
котором смонтировано 91 «фоническое колесо». С каждым фоническим колесом
связан звукосниматель, аналогичный тому, что используется на электрогитарах,
который генерирует определенную частоту, определяемую неровностями на
тональных колесах. Поскольку все фонические колеса установлены на одном
валу, они не могут разладиться по отношению друг к другу.
Нажатие клавиши на B-3 не просто генерирует частоту, создаваемую фоническим
колесом. Существуют девять восьмипозиционных «тяг», которые используются
для смешивания сигнала, производимого «основным» тоном (играемой нотой),
с сигналами других фонических колес. Тяги устанавливают уровень субоктавы,
пятой, основной, 8-й, 12-й, 15-й, 17-й, 19-й и 22-й гармоник.
Создаваемый звук представляет собой взвешенную сумму этих девяти сигналов,
установленную с помощью регуляторов, аналогично тому, как мы составили сольмажорный аккорд на рис. 6.37.

Частота

Полосовой

Частота

Рис. 6.39. Фильтры

Коэффициент
преобразования

Частота

Высоких частот

Коэффициент
преобразования

Низких частот

Коэффициент
преобразования

Коэффициент
преобразования

Еще одна особенность многих медиаплееров — графический эквалайзер,
позволяющий настроить звук по собственному вкусу. Графический эквалайзер — это набор настраиваемых фильтров, устройств, выделяющих
или исключающих определенные частоты. Они похожи на передаточные
функции, которые мы видели в разделе «Цифровые устройства в аналоговом мире» на с. 80, но применяются для частоты, а не для напряжения или
света. Существуют два основных типа фильтров: фильтр нижних частот,
который пропускает все, что ниже определенной частоты, и фильтр верхних
частот, который пропускает все, что выше определенной частоты. Их можно
комбинировать для создания полосовых фильтров, которые включают все,
что находится между низкими и высокими частотами, или режекторных
фильтров, исключающих определенную частоту. На рис. 6.39 видно, что края
фильтра нечеткие; они спадают. Идеальных фильтров не существует. Обратите внимание на то, что защита от дребезга кнопок на рис. 6.7 представляет
собой фильтр нижних частот.
Режекторный

Частота

218   Глава 6. Разбор связей
Можно, например, применить фильтр нижних частот к соль-мажорному аккорду,
как показано на рис. 6.40. Применение фильтра эффективно умножает кривые;
фильтр регулирует уровень звука на разных частотах.

0

G4

B4

D5

400

494

587

Рис. 6.40. График преобразования Фурье с фильтрацией нижних частот
соль-мажорного аккорда
Как вы понимаете, фильтрованный аккорд звучит уже не так. B4 стал немного
тише, а D5 практически не слышно.
Почему все это важно? На рис. 6.41 показано преобразование Фурье восстановленной синусоидальной волны из рис. 6.34. Я не уточнял детали на этом
рисунке, поэтому предположим, что это синусоидальная волна 400 Гц, дискретизированная с частотой 3 кГц.
400

1000

0

2000

3000

4000

5000

6000

7000

Рис. 6.41. График преобразования Фурье восстановленной синусоидальной волны
Обратите внимание, что ось X уходит в бесконечность с частотами, кратными частоте дискретизации, плюс или минус частота дискретизированного
сигнала.
Что произойдет, если взять восстановленную синусоидальную волну и применить фильтр нижних частот, как показано на рис. 6.42?
400

0

1000

2000

3000

4000

5000

6000

7000

Рис. 6.42. График преобразования Фурье восстановленной синусоидальной волны
с фильтром нижних частот

Аналоговые устройства в цифровом мире   219
Все искажения исчезают; осталась только синусоидальная волна 400 Гц. Похоже,
что выборка работает, если применить соответствующую фильтрацию. Но как
выбрать частоту дискретизации и фильтр?
Гарри Найквист (Harry Nyquist) (1889–1976), шведский инженер-электронщик,
придумал теорему, согласно которой, если необходимо точно захватить сигнал,
выборку следует проводить с частотой, по крайней мере вдвое превышающей
максимальную. Это хорошая теория, но, поскольку электроника не следует
идеальной математике, она помогает производить выборку быстрее, чем необходимо для получения хорошо звучащего результата. Диапазон человеческого
слуха составляет от 20 до 20 000 Гц.
Исходя из всего этого, мы должны иметь возможность захватывать все, что
слышим, с частотой дискретизации 40 кГц. Что, если мы случайно получим звук
с частотой 21 кГц, который не дискретизирован согласно теореме Найквиста?
В этом случае мы получаем свертку, или наложение, спектров. Представьте,
что частота дискретизации представляет собой зеркало, в котором отражается
любая информация, превышающая эту частоту. Рассмотрев еще раз рис. 6.41,
видим, что в частоте дискретизации есть помехи с плюс или минус дискретной
частотой. Поскольку частота дискретизации намного больше, чем дискретная
частота, эти помехи находятся далеко друг от друга. Входной сигнал с частотой
21 кГц, выбранный на частоте 40 кГц, будет подвержен помехам на частоте 19 кГц
(40 – 21). Этот ошибочный сигнал называется наложением. Мы не получаем то,
что предоставили. Перед дискретизацией необходимо применить фильтр нижних
частот, чтобы избежать наложения спектров.
Компакт-диски принимают 16-битные выборки с частотой 44 100 Гц — разумеется, умноженной на 2, потому что это стерео. Это дает чуть больше 175 Кбайт
в секунду — довольно много данных. Некоторые стандартные частоты дискретизации звука составляют 44,1, 48, 96 и 192 кГц. Зачем проводить выборку с более
высокой частотой, если это приведет к получению гораздо большего количества
данных, а Найквист говорит, что в этом нет необходимости?
Частоту и амплитуду сигнала, дискретизированного около частоты Найквиста,
можно восстановить, а вот фазу — нет. Еще один новый термин! Представьте
фазу как небольшой сдвиг во времени. Вы можете видеть на рис. 6.43, что более
толстый сигнал отстает (а не опережает) от более тонкого сигнала на 45 градусов, из-за чего он появляется немного позже по времени.
1
0
–1
0° 45° 90°

180°

270°

360°

450°

540°

Рис. 6.43. Разность фаз сигналов

630°

720°

220   Глава 6. Разбор связей
Разве это важно? Ну, вообще нет, если мы не говорим о стерео. Разность фаз
вызывает временную задержку между сигналом, достигающим левого и правого
уха, — благодаря этому можно определить, где в пространстве находится источник звука, как показано на рис. 6.44.
Слева

Вцентре

Звук

Справа

Звук

Звук

Рис. 6.44. Разница фаз в реальной жизни
ДИСКРЕТИЗАЦИЯ И ФИЛЬТРАЦИЯ ДЛЯ ЧМ-СТЕРЕО
ЧМ-стерео — интересное применение дискретизации и фильтрации. Это также
отличный пример того, как новые функциональные возможности были встроены
в систему, которая никогда не была для того предназначена, с обратной совместимостью, а это означает, что и старая система по-прежнему работала.
Вернувшись к рис. 6.20, видим, как биты могут использоваться для модуляции
частоты. ЧМ означает «частотная модуляция» (FM, frequency modulation). FMрадио работает путем модуляции несущей частоты аналоговым сигналом вместо
цифрового.
Несущие частоты FM-радиостанций распределяются каждые 100 кГц. Вы видели
на рис. 6.41, что выборка генерирует дополнительные частоты до бесконечности;
то же самое происходит с модуляцией. В результате к модулированному сигналу
должен применяться фильтр нижних частот, иначе возникнут помехи на других
станциях. Вы видели спад фильтра на рис. 6.39. Чем круче спад, тем сильнее
фильтр искажает фазу, что отрицательно сказывается на звуке. Это показано
в части радиоспектра на рис. 6.45.

98.3

98.4

98.5

Рис. 6.45. Радиоспектр

Аналоговые устройства в цифровом мире   221
До стерео звуковая информация в монофоническом ЧМ-сигнале занимала место
примерно на 15 кГц выше несущей частоты. Приемник удалял несущую частоту,
в результате чего получался исходный звук. Эту характеристику нужно было сохранить при переходе на стерео; в противном случае перестали бы работать все
существующие приемники.
На рис. 6.46 представлен обзор того, как работает ЧМ-стерео. Прямоугольный
сигнал 38 кГц используется для попеременной выборки левого и правого каналов.
Генерируется контрольный сигнал с частотой 19 кГц, который синхронизируется
с прямоугольным сигналом дискретизации. Контрольный сигнал смешивается на
низком уровне, который трудно услышать через музыку, и объединяется с выборками для создания транслируемого составного сигнала.
Левый
38 kHz
дискретизация

левый

правый

левый

правый

левый

правый

левый

Дискретный
сигнал

правый

Правый
контрольный
сигнал в 19 кГц

+
=

Контрольный
сигнал
Составной
сигнал

Рис. 6.46. Генерация ЧМ-сигнала
Посмотрим на результат анализа Фурье на рис. 6.47 и увидим, что первый набор
частот слева представляет собой сумму левого и правого каналов — именно то,
что нужно для монозвука. Для старых приемников это не проблема. Следующий
набор частот — это разница между левым и правым каналами, которую не уловил
бы старый моноприемник. Однако стереоприемник может использовать простую
арифметику для разделения левого и правого каналов, производящих стереозвук.
Носитель

Л+П

+19 кГц

+38 кГц

+57 кГц

+76 кГц

+100 кГц

Л-П

Рис. 6.47. ЧМ-стереоспектр

Мы лучше распознаем высокие частоты, потому что они имеют более короткие
длины волн по сравнению с толщиной человеческой головы. Если бы голова
была настолько узкой, что уши находились бы в одном месте, временной задержки не было бы. Большеголовые люди лучше воспринимают стерео! Это одна
из причин, почему можно обойтись одним сабвуфером: нельзя точно сказать,

222   Глава 6. Разбор связей
откуда исходит звук, потому что длина волны настолько велика по сравнению
с толщиной головы, что разницу фаз невозможно обнаружить.
Когда вы слушаете стереозвук, разница фаз между звуками, выходящими из
динамиков, создает изображение, способность «видеть», где музыканты находятся в пространстве. Изображение «мутное» без точной фазы. Таким образом,
причиной более высоких частот дискретизации является лучшее воспроизведение фазового и стереоизображения. Вы можете никогда этого не заметить, если
для прослушивания музыки всегда будете использовать дешевые наушники на
сотовом телефоне.
Ранее я упоминал, что звук включает в себя большой объем данных. Было бы
неплохо иметь возможность сжимать эти данные, чтобы они занимали меньше
места. Есть два класса сжатия: без потерь и с потерями. Сжатие без потерь
сохраняет все исходные данные. В результате объекты можно сжимать только
примерно до половины их первоначального размера. Самым популярным сегодня сжатием без потерь является FLAC, сокращение от Free Lossless Audio Codec.
Кодек (codec) — это устройство кодирования-декодирования, похожее на модем,
которое знает, как переводить что-либо из одной системы кодирования в другую.
MP3, AAC, Ogg и им подобные — кодеки сжатия с некоторыми потерями точности.
Они работают на психоакустических принципах. Люди, изучавшие работу уха
и мозга, решили, что есть определенные вещи, которые услышать невозможно, например что-то тихое, происходящее сразу после громкого удара в барабан. Кодеки
работают, удаляя эти звуки, что дает гораздо лучшую степень сжатия, чем в случае
FLAC. Вот только уши не у всех одинаковы. Я считаю, что MP3 звучит ужасно.

Цифровые изображения
Визуальные изображения сложнее аудио, поскольку подразумевают дискретизацию двумерного пространства. Цифровые изображения представлены в виде
прямоугольных массивов элементов изображения, или пикселей. Каждый пиксель цветного изображения представляет собой триаду из красного, зеленого
и синего цветов. Стандартные дисплеи, доступные сегодня, имеют по 8 бит красного, зеленого и синего цветов. Обычное представление изображено на рис. 1.20.
Компьютерные дисплеи используют цветовую систему сложения, которая
воспроизводит практически любой цвет, комбинируя (или добавляя, отсюда
и название) различное количество красного, зеленого и синего основных цветов. Это отличается от субтрактивной цветовой системы, используемой для
печати, которая создает цвета путем смешивания разного количества голубого,
пурпурного и желтого основных цветов.
Дискретизация изображения сродни помещению москитной сетки поверх
изображения и записи цвета в каждом квадрате. Это несколько сложнее из-за
поточечной выборки, что означает, что записывается не весь квадрат, а только

Аналоговые устройства в цифровом мире   223
точка в центре каждого из них. На рис. 6.48 показана дискретизация изображения
с трех экранов с разным разрешением.

Рис. 6.48. Дискретизация изображения с разным разрешением
Видим, что дискретизированное изображение выглядит лучше на экранах с более
высоким разрешением, но, конечно, это значительно увеличивает объем данных.
Однако даже при наличии экрана с высоким разрешением мы все равно получаем неровные края. Это происходит из-за недостаточной выборки и наложения
спектров согласно Найквисту, хотя сопутствующая математика слишком сложна
для этой книги. Как и в случае со звуком, иногда помогает фильтрация. Один из
способов фильтрации — супердискретизация, или взятие нескольких выборок
на квадрат и их усреднение, как показано на рис. 6.49.

Рис. 6.49. Супердискретизация

224   Глава 6. Разбор связей
При увеличении картинка неприглядная, но если вы отойдете подальше, то
заметите, что все не так уж плохо. Если задуматься, супердискретизация эквивалентна увеличению частоты дискретизации, что мы уже рассматривали для
звука на рис. 6.35.
Изображения становятся все больше и занимают много места. Неизвестно, останется ли когда-нибудь место для хранения всех фотографий и видео с котиками
в мире. Как и в случае со звуком, нужно, чтобы изображения занимали меньше
места, чтобы как можно больше разместить их в том же объеме памяти и быстрее
передавать их по сети. Это снова решается путем сжатия.
Самый распространенный формат сжатия изображений сейчас — JPEG, стандарт,
созданный группой экспертов Joint Photographic Experts Group. В его основе
лежит сложная математика, которую я здесь не буду описывать. Грубо говоря,
работа JPEG заключается в том, что он ищет соседние пиксели, которые довольно
близки по цвету, и сохраняет описание этой области вместо отдельных пикселей,
которые он содержит. Возможно, у вас есть камера с настройкой качества изображения; этот параметр регулирует определение «довольно близки по цвету».
Это цветная версия примера из раздела «Стеки» на с. 166.
JPEG использует знания о человеческом восприятии аналогично аудиокодекам
с потерями. Например, он использует тот факт, что наш мозг более чувствителен
к изменениям яркости, чем к изменениям цвета.

Видео
Видео, еще один шаг вперед в многомерном пространстве, — это последовательность двумерных изображений, дискретизированных через равные промежутки
времени. Временной интервал — функция зрительной системы человека. Старые
фильмы обходились частотой 24 кадра в секунду (к/с); средний человек сегодня
вполне доволен 48 кадрами в секунду.
Дискретизация видео не сильно отличается от дискретизации изображений, за
исключением того, что различные помехи визуально раздражают, и поэтому их
необходимо минимизировать. Проблема в том, что расположенные по краям
помехи дискретизации, которые мы видели на рис. 6.48, не стоят на месте во
время движения объектов.
Чтобы лучше понять это, взгляните на рис. 6.50, где показана диагональная
линия, которая со временем перемещается слева направо. Перемещается только
доля пикселя за кадр, а это означает, что выборка не всегда одинакова. Это все
еще похоже на приближение линии, но каждая из линий представляет собой
отдельное приближение. Это заставляет края «плавать», что визуально мешает.
Фильтрация с использованием супердискретизации — один из способов уменьшить такие неприятные визуальные помехи.

Аналоговые устройства в цифровом мире   225

Кадр 0

Кадр 1

Кадр 2

Кадр 3

Время

Рис. 6.50. Пример «плавания» краев
Видео производит намного больше данных, чем изображения или аудио. Видео
в формате UHD имеет разрешение 3840 × 2160 пикселей. Умножьте это на 3 байта
на пиксель и 60 кадров в секунду, и вы получите колоссальные 1 492 992 000 байт
в секунду! Очевидно, что сжатие очень важно для видео.
Ключ к сжатию видео — наблюдение о том, что только часть изображения обычно
меняется от кадра к кадру. Посмотрите на рис. 6.51, на котором мистер Сигма
идет забрать посылку. Как видите, очень малая часть изображения меняется
между кадрами. Можно хранить или передавать намного меньше данных, если
использовать только данные из области изменений. Этот метод называется
сжатием движения.

Компания
распределения
Гаусса

Компания
распределения
Гаусса

Компания
распределения
Гаусса

Позвоните
в звонок

Позвоните
в звонок

Позвоните
в звонок

Кадр 1

Кадр 2

Область изменений
Область изменений

Рис. 6.51. Межкадровое движение

226   Глава 6. Разбор связей
Один из минусов представления видео как набора изменений исходного изображения заключается в том, что иногда данные могут искажаться. Вероятно,
вы видели раздражающие помехи на цифровом телевидении или при воспроизведении поврежденного видеодиска.
Нам нужен способ восстановить данные. Это достигается путем регулярного
включения в них ключевых кадров. Ключевой кадр — это полное изображение.
Даже если ошибки накапливаются из-за поврежденных измененных данных,
данные восстанавливаются при обнаружении следующего ключевого кадра.
Алгоритмы обнаружения различий между кадрами сложны и требуют больших
вычислительных ресурсов. Новые стандарты сжатия, такие как MPEG4, включают поддержку многослойной обработки, в которой используется тот факт,
что большая часть видео теперь генерируется компьютером. Многослойная
обработка работает так же, как старая нарисованная вручную анимация на
целлулоидных пленках, которую мы обсуждали в главе 1, где объекты, нарисованные на прозрачных пленках, перемещались по неподвижному фоновому
изображению.

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

Терминалы
Не так давно клавиатура, мышь и дисплей или сенсорный экран, к которым вы
так привыкли, были невообразимой роскошью.
Было время, когда мы взаимодействовали с компьютером, записывая программу
или данные на бумаге, используя специальные формы для кодирования. Затем
данные нужно было передавать тому, кто пользуется клавишным перфоратором, чтобы превратить формы в стопку перфокарт (см. рис. 3.25). Мы брали
эти карты, стараясь не уронить, и передавали оператору компьютера, который
вставлял их в устройство для чтения карт. Оно, в свою очередь, считывало их
в компьютер и запускало программу. Мы использовали этот подход, известный
как пакетная обработка, потому что компьютеры были очень медленными и дорогими, что делало компьютерное время действительно ценным — пока ваши
карты перфорировались, выполнялась чья-то программа.
Компьютеры стали быстрее, меньше и дешевле. К концу 1960-х годов в вашей
компании или отделе вполне мог появиться небольшой компьютер. Маленький,

Устройства взаимодействия с человеком   227
примерно как автофургон. Компьютерное время стало чуть менее дефицитным.
Произошло очевидное: люди начали подключать компьютеры к телетайпам.
Телетайпы назывались терминалами, потому что они находились в конце линии (от terminal — конечный пункт). Особенно популярная модель, Teletype
ASR-33, включала в себя клавиатуру, принтер, перфоратор для бумажной
ленты (рис. 3.26) и устройство для чтения бумажной ленты. Бумажная лента
была эквивалентом карты памяти USB. ASR-33 был прекрасен — он позволял
печатать аж 10 символов в секунду! Термин TTY все еще используется нами как
сокращение от телетайпа.
Системы с разделением времени были изобретены для того, чтобы занять эти
небольшие компьютеры. Да, такие системы действительно походили на аренду
жилья на время отпуска. Вы считаете место своим, и это действительно так, пока
вы занимаете его, но в другое время им пользуются другие люди.
В системе с разделением времени есть программа операционной системы (ОС),
которая запускается на компьютере. Программа ОС похожа на агента по бронированию для аренды жилья на курорте. Его задача — выделить каждому
пользователю различные ресурсы компьютера. Когда подходит ваша очередь
использовать машину, программы другого пользователя выгружаются на диск,
а ваша программа загружается в память и какое-то время работает. Все это происходит достаточно быстро — можно подумать, что у вас есть самый настоящий
компьютер, по крайней мере до тех пор, пока очередь немного не увеличится.
В какой-то момент память начнет переполняться, поскольку операционная
система станет тратить больше времени на процессы загрузки и выгрузки, чем
на запуск пользовательских программ.
Пробуксовка (thrashing) сильно замедлила работу систем с разделением времени, когда пользователей стало слишком много. Программисты начали работать
поздно ночью, потому что они могли забирать компьютеры себе, после того как
все уйдут домой.
Системы с разделением времени являются многозадачными, поскольку компьютер создает иллюзию, что он делает более одного дела одновременно.
Внезапно к одной машине стали подключать множество терминалов. Так появилось понятие «пользователь», чтобы машины могли определять, что кому
принадлежит.
Время шло, появлялись лучшие версии телетайпов, и каждое их поколение
становилось быстрее и тише. Но они все еще печатали что-то на бумаге (то есть
создавали бумажную копию) и годились только для текста. В модели телетайпа 37 были добавлены греческие символы, чтобы ученые могли печатать
математические уравнения. Терминалы IBM Selectric имели сменяемые печатные шары, которые позволяли пользователю изменять шрифты. Существовал
шрифт с точками в разных позициях, который позволял рисовать графики.

228   Глава 6. Разбор связей

Графические терминалы
Отказ от терминалов с бумажными копиями был обусловлен многими причинами — скоростью, надежностью, шумом и т. п. Экраны существовали пока
лишь для таких вещей, как радары и телевидение, — пришло время заставить их
работать с компьютерами. Это происходило медленно из-за эволюции электроники. Память была слишком медленной и дорогой. Графические терминалы
изначально создавались на основе разновидности вакуумной лампы (см. «Вакуумные лампы» на с. 93), называемой электронно-лучевой трубкой (ЭЛТ).
Внутренняя часть стекла покрывается химическим люминофором, который
светится при ударе электронов. Имея более одной решетки или отражательной
пластины, можно рисовать изображения на люминофоре. Это похоже на очень
талантливого игрока с битой, который может поразить мячом любую цель.
На самом деле, существуют два способа заставить этот дисплей работать. Версия с отражающей пластиной, называемая электростатическим отклонением,
использует тот же принцип, что и опасное статическое электричество. Другой
вариант — версия с электромагнитом, называемая электромагнитным отклонением. В любом случае биты необходимо преобразовать в напряжения, что
является еще одним применением строительного блока ЦАП.
Сегодня ЭЛТ — это в основном пережиток прошлого, который был заменен
жидкокристаллическим дисплеем (liquid crystal display, LCD). Жидкие кристаллы — это вещества, которые изменяют свои светопропускающие свойства при
подаче электрического тока. Типичный дисплей с плоским экраном очень похож на ЭЛТ в том, что в каждой точке растра есть три капли жидкого кристалла
с красным, зеленым и синим фильтрами и светом, который поступает сзади. Мы
по-прежнему говорим о жидкокристаллических дисплеях как об электроннолучевых трубках, но на самом деле ЭЛТ давно канули в Лету. ЖК-дисплеи
распространились повсеместно и заменили ЭЛТ в большинстве приложений;
благодаря ЖК-дисплеям стало возможным появление сотовых телефонов, ноут­
буков и телевизоров с плоским экраном.
Первые экранные терминалы назывались стеклянными телетайпами, потому
что они могли отображать только текст. Эти терминалы отображали 24 строки
по 80 символов в каждой, всего 1920 символов. Поскольку символ помещался
в байт, всего получалось два доступных в то время кибибайта памяти. Со временем были добавлены дополнительные функции, такие как редактирование
на экране и перемещение курсора, которые в конечном итоге были стандартизированы как часть ANSI X3.64.

Векторная графика
ЭЛТ по принципу работы очень похожи на миллиметровую бумагу. Электронный луч перемещается в некоторую точку в зависимости от напряжений по

Устройства взаимодействия с человеком   229
осям X и Y. Также есть ось Z, которая определяет яркость. Ранние модели не
учитывали цвета, поэтому это были черно-белые, или полутоновые, дисплеи.
Количество координат на дюйм называется разрешением.
Векторная графика — это рисование линий, или векторов. Можно создать картину, рисуя набор направленных линий. Тонкие стрелки на рис. 6.52 нарисованы
полностью с выключенной яркостью.

8

7
3
2 6

11
12
10

9

4

1
5

Рис. 6.52. Дом в векторной графике
Белая стрелка с черным контуром рисуется дважды: один раз с включенной
яркостью, а затем снова с выключенной. Если дважды провести одну и ту же
линию с включенной яркостью, она станет вдвое ярче, чего мы не хотим делать
только потому, что меняем положение.
Дом на рис. 6.52 нарисован по таблице отображения, которая представляет
собой список инструкций для рисования, как на рис. 6.53.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.

Переместитесь в (2, 0)
Нарисуйте линию до (2, 5)
Нарисуйте линию до (7, 5)
Нарисуйте линию до (7, 0)
Нарисуйте линию до (2, 0)
Переместитесь в (2, 5)
Нарисуйте линию до (8, 7)
Нарисуйте линию до (7, 5)
Переместитесь в (6, 0)
Нарисуйте линию до (5, 3)
Нарисуйте линию до (4, 3)
Нарисуйте линию до (4, 0)
Начните заново с шага 1

Рис. 6.53. Таблица отображения
Обратите внимание на последнюю инструкцию. Мы начинаем сначала, потому
что изображение на экране довольно быстро тускнеет. Это работает только из-за

230   Глава 6. Разбор связей
постоянства люминофора ЭЛТ — свойства, благодаря которому он остается
гореть после удаления луча, — и медленной реакции человеческого глаза. Нужно повторять одни и те же действия, чтобы изображение оставалось на экране.
Однако в этой инструкции есть еще кое-что. Вокруг нас много излучения в 60 Гц,
потому что в Америке это частота электроэнергии переменного тока (в некоторых других странах1 она составляет 50 Гц). Несмотря на все попытки экранировать излучение, оно все равно влияет на дисплеи и заставляет их колебаться.
Таким образом, графические терминалы, такие как GLANCE G, разработанные
в Bell Telephone Laboratories, имели команду «перезапуск на шаге 1, после того
как в следующий раз линия питания пересечет 0 в направлении от плюса к минусу». Это синхронизировало рисунок с интерференцией, так что он всегда
колебался в соответствии с ней и, следовательно, был незаметен.
На рисование изображения требовалось время, и неприятным побочным эффектом было то, что все шло хорошо до тех пор, пока список отображения не стал
настолько длинным, что его нельзя было нарисовать за одну шестидесятую долю
секунды. Изображение стало сильно мерцать, если его можно было нарисовать
только раз в тридцатую долю секунды.
Компания Tektronix нашла интересное решение проблемы мерцания, которое
получило название запоминающая трубка. Это был электронный аналог игрушки «Волшебный экран». С его помощью можно было рисовать очень сложные
изображения, но их нужно было встряхнуть электронным способом, чтобы стереть. Было очень сложно рисовать цельные изображения на GLANCE G, потому
что для этого требовалось огромное количество векторов и в итоге появлялось
мерцание. Запоминающие трубки могли обрабатывать сплошные изображения,
поскольку количество векторов не ограничивалось, но центры сплошных областей изображения начинали исчезать. На GLANCE G можно было стереть
одну строку, удалив ее из таблицы отображения. Запоминающая трубка этого
не позволяла. При стирании экран испускал ярко-зеленую вспышку, которую
наверняка вспомнят многие программисты-старожилы.

Растровая графика
Растровая графика — полная противоположность векторной. Именно так
изначально работало телевидение. Растр представляет собой непрерывно нарисованный узор, как показано на рис. 6.54.
Растр начинается в верхнем левом углу и проходит по всему экрану. Затем
горизонтальный обратный ход приводит к началу следующей линии. Наконец,
вертикальный обратный ход возвращает картинку к началу после рисования
последней линии.
1

В том числе в России. — Примеч. науч. ред.

Устройства взаимодействия с человеком   231
Это очень похоже на аналогию со стартовым пистолетом, которую я приводил
ранее при обсуждении последовательной связи. Когда растр работает, все, что
вам нужно сделать, — изменить яркость в нужное время, чтобы получить желаемое изображение, как показано на рис. 6.55.

Рис. 6.54. Растр

Рис. 6.55. Дом в растровой графике

Я также использовал аналогию с москитной сеткой в разделе «Цифровые изображения» на с. 222. Растровое изображение — это настоящая сетка, а это значит,
что рисовать между точками не получится. Это может привести к появлению
неприятных визуальных помех, например к тому, что крыша будет выглядеть
неправильно. Это связано с тем, что разрешение типичного растрового дисплея
довольно низкое — порядка 100 точек на дюйм. Низкое разрешение приводит
к заниженной выборке и искажению, аналогичному тому, что мы видели для
цифровых изображений. В настоящее время используется достаточная вычислительная мощность, чтобы постоянно применять сглаживание — например,
с помощью супердискретизации.
Растровое сканирование используется в таких устройствах, как факсы, лазерные принтеры и сканеры. Поднимите крышку сканера и посмотрите, как он
работает. Только не забудьте надеть солнцезащитные очки. Когда у принтеров
было больше движущихся частей и они были громче, люди придумали, как
воспроизводить на них растровую музыку, тщательно подобрав символы для
печати.
Растровые дисплеи не применяют таблицы отображения, хотя те по-прежнему
используются за растровыми дисплеями. Как мы увидим позже, веб-страницы
представляют собой таблицы отображения. В графическом языке OpenGL
есть таблицы отображения, и поддержка языка часто включена в графическое
оборудование. Монохромные дисплеи используют часть памяти с 1 битом для
каждой позиции растра. В свое время это был огромный объем памяти; теперь он
не имеет такого значения. Конечно, эта память может очень быстро разрастись.
Для растрового дисплея, который может отображать 256 различных уровней
серого, вам потребуется 8 бит памяти для каждой позиции растра.

232   Глава 6. Разбор связей
Цвет был открыт в Стране Оз и быстро перешел на экран. Монохромные или
полутоновые дисплеи были простыми: все, что вам нужно было сделать, — это
покрыть внутреннюю часть экрана слоем люминофора. Для цветных дисплеев
требовались три точки разного цвета в каждом месте растра — красный, зеленый и синий — и три электронных луча, которые могли пройти через эти точки
с большой точностью. Это означало, что для обычного дисплея нужно было
в три раза больше памяти.

Клавиатура и мышь
Терминалы позволяют вводить данные в дополнение к дисплею, который данные отображает. Хорошо известные вам терминалы — это клавиатура и мышь,
сенсорная панель на ноутбуке и сенсорный экран на телефоне и планшете.
Клавиатуры довольно просты. Это просто набор переключателей с некоторой
логикой. Распространенный способ создания клавиатуры — поместить ключевые
переключатели в сетку, мультиплексируя их, как показано на рис. 6.10. Мощность
последовательно подается на строки сетки, и значения столбцов считываются.
Мышь в том виде, в каком мы ее знаем, была изобретена американским инженером Дугласом Энгельбартом (Douglas Engelbart) (1925–2013) в Стэнфордском
исследовательском институте. Я упоминал в разделе «Квадратура» на с. 196, что
можно сделать мышь, используя пару квадратурных кодировщиков, по одному
для направлений X и Y.
Существует множество технологий для сенсорных панелей и экранов. Основное
их отличие состоит в том, что сенсорные экраны должны быть прозрачными,
чтобы дисплей был виден. Сенсорные устройства — это устройства для сканирования строк и столбцов, подобно клавиатурам, но в гораздо более мелком
масштабе.

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

7

Организация данных

Вы могли заметить, что я становлюсь немного одержим,
когда речь заходит о работе с памятью. Из главы 3 вы
узнали, что порядок доступа к устройствам памяти, таким
как DRAM, флеш-память и дисковые накопители, влияет
на их скорость. А из главы 5 — что производительность
также зависит от того, присутствуют ли нужные данные
в кэш-памяти. Учитывайте эти характеристики системы памяти
при организации данных — и вы повысите производительность. В этой главе
мы рассмотрим ряд структур данных, или стандартных способов организации
данных. Многие из них созданы для поддержки эффективного использования
различных типов памяти. Выбор структуры данных часто связан с компромиссом между пространством и временем, когда больше памяти используется
для ускорения определенных операций. (Обратите внимание, что структуры
данных более высокого уровня предоставляются языками программирования,
а не компьютерным оборудованием.)
Выражение «локальность ссылок» бытует как профессиональный жаргонизм,
но резюмирует большую часть того, о чем идет речь в этой главе. Оно означает:
«Храните нужные данные под рукой, а данные, которые вам вскоре понадобятся, — еще ближе».

Базовые типы данных
В языках программирования существует множество базовых типов данных.
Эти типы определяются двумя характеристиками: размером (количеством

234   Глава 7. Организация данных
битов) и интерпретацией типа (знаковый, беззнаковый, с плавающей точкой,
символьный, указатель, логический). На рис. 7.1 показаны типы данных, доступные на типичном современном компьютере в языке программирования C.
Различные реализации C на одном компьютере, а также разные языки, такие
как Pascal или Java, могут по-разному представлять эти типы данных. Некоторые языковые среды включают средства, позволяющие программисту
запрашивать порядок байтов (см. рис. 4.4 на с. 139), количество битов в байте
и многое другое.
char
7

short
0

15

long
0

31

0

long long
63

0
unsigned short

unsigned char

7

0

15

unsigned long
0

31

0

unsigned long long
63

0
float
31

0
double

63

0
int
31

0
unsigned int

31

0

указатель (предполагает наличие 64-разрядного компьютера)
63

0

Рис. 7.1. Типичные базовые типы данных языка C
Мы уже рассматривали все эти типы данных в главе 1, кроме указателя; единственная разница здесь в том, что мы используем для них названия языка C.
Американский инженер Гарольд Лоусон (Harold Lawson) изобрел указатель для
языка PL/I (Programming Language One) в 1964 году. Указатель — это просто
целое число без знака некоторого архитектурно-зависимого размера, но оно
интерпретируется как адрес памяти. Его можно сравнить с адресом дома —
число обозначает не сам дом, но адрес, по которому его можно найти. Мы уже

Массивы   235
видели, как это работает — подобно косвенной адресации из раздела «Режимы
адресации» на с. 149. Нулевой, или NULL-указатель, обычно не является допустимым адресом памяти.
Указатели стали популярными благодаря языку С. В некоторых языках вместо
указателей реализованы более абстрактные ссылки. Они были призваны решить
проблемы, возникающие из-за неправильного использования указателя, и я затрону этот вопрос позже в данной главе. Указатели, как правило, совпадают по
размеру с величиной типичного для данного устройства слова, поэтому к ним
можно получить доступ за один цикл.
Достижения в технологии производства микросхем стимулировали разработку
большого количества новых вычислительных машин в 1980-х годах, включая
переход от 16-битных компьютеров к 32-битным. В 1970-х и начале 1980-х
большинство программистов очень бесцеремонно относились к использованию
указателей в коде; например, предполагалось, что указатели и целые числа имеют
одинаковый размер, и потому их использовали взаимозаменяемо. Такой код при
переносе на новые устройства часто ломался, и его было трудно отладить. Это
привело к появлению двух независимых подходов к исправлению. Во-первых,
гораздо больше внимания стали уделять вопросам переносимости. Это принесло
плоды; проблемы с переносимостью и указателями сегодня встречаются не так
часто. Во-вторых, были разработаны языки без указателей, например Java. В некоторых случаях такой подход оказался оправданным, но не всегда.

Массивы
Типы данных, рассмотренные в предыдущем разделе, просты; их можно сравнить с частными домами. В языках программирования также поддерживаются
массивы, которые вместо этого можно сравнить с многоквартирными зданиями.
У многоквартирных зданий есть адрес, а у отдельных квартир — номера. Программисты называют номер устройства индексом (начиная с 0, в отличие от
большинства номеров квартир), а отдельные квартиры — элементами массива.
Типичные компьютерные строительные нормы и правила требуют, чтобы все
квартиры в доме были идентичными. На рис. 7.2 показано здание, которое содержит десять 16-битных квартир в языке C.
Каждый прямоугольник на рис. 7.2 представляет собой байт. Таким образом,
в этом массиве из 16-битных элементов каждый элемент занимает два 8-битных
байта. Нижний индекс элемента обозначает индекс массива.
Посмотреть на элементы массива по-другому можно через призму относительной адресации (см. «Относительная адресация» на с. 173). Каждый элемент
представляет собой смещение от адреса 0-го элемента, или базового адреса.
Таким образом, элемент1 находится на расстоянии двух байтов от элемента0.

236   Глава 7. Организация данных

Массив

0

1

элемент 0

2

3

элемент 1

4

5

элемент2

6

7

элемент3

8

9

элемент4

10

11

элемент5

12

13

элемент6

14

15

элемент7

16

17

элемент8

18

19

элемент9

Рис. 7.2. Десятиэлементный массив 16-битных чисел
Массив на рис. 7.2 представляет собой одномерный массив — уродливое одноэтажное здание с квартирами, соединенными общим коридором. Языки
программирования также поддерживают многомерные массивы — например,
четырехэтажное здание с трехбайтными квартирами. Для его представления
понадобится двумерный массив с двумя индексами: один — для номера этажа,
а другой — для номера квартиры на этом этаже. Мы даже можем создавать
трехмерные здания с указателями корпуса, этажа и квартиры; четырехмерные
постройки с четырьмя индексами и т. д.
Важно понимать, как многомерные массивы размещаются в памяти. Допустим, мы
подсовываем листовки под каждую дверь в многоквартирном доме 4 × 3. Можно
сделать это двумя способами. Первый — начать с этажа 0 и подложить листовку
в квартиру 0, затем перейти на этаж 1 и подложить листовку в квартиру 0 и т. д.
Илиможно начать с этажа 0 и подсунуть листовки под каждую дверь на этом
этаже, затем сделать то же самое на этаже 1 и т. д. Это как раз то, что называется
локальностью ссылок. Второй подход (перебор всех дверей на одном этаже) представляет лучшую локальность ссылок — такой путь гораздо проще пройти. Он
показан на рис. 7.3, где числа в скобках — это адреса относительно начала массива.
Индекс столбца перемещается между соседними столбцами, тогда как индекс
строки перемещается между строками, которые находятся дальше друг от друга
в адресном пространстве.
Массив
элемент0 элемент 0,0 (0)

элемент 0,1 (1)

элемент 0,2 (2)

элемент1 элемент 1,0 (3)

элемент 1,1 (4)

элемент 1,2 (5)

элемент2 элемент 2,0 (6)

элемент 2,1 (7)

элемент 2,2 (8)

элемент3 элемент 3,0 (9)

элемент 3,1 (10)

элемент 3,2 (11)

Рис. 7.3. Схема двумерного массива

Битовые матрицы   237
Этот подход применим и на более высоких измерениях. Если бы у нас был
комплекс из пяти четырехэтажных зданий, по три квартиры на этаже, рис. 7.3
повторился бы пять раз, по одному для каждого здания. В адресном пространстве
соседние здания дальше друг от друга, чем соседние строки, которые, в свою
очередь, дальше друг от друга, чем соседние столбцы.
Возвращаясь к рис. 7.2, подумайте, что произойдет, если попытаться получить
доступ к элементу10. Некоторые языки программирования, такие как Pascal,
проверяют, находится ли индекс массива в границах массива, но многие другие
(включая C) этого не делают. Без проверки элемент10 попадет в байты 20 и 21
относительно начала массива. Это может привести к сбою программы, если по
этому адресу нет доступной памяти. Отсутствие проверки — это также прореха в безопасности, которая может привести к непреднамеренному доступу
к данным, хранящимся вне массива. Ваша работа как программиста — всегда
оставаться в рамках массива, если язык программирования не делает этого
самостоятельно.

Битовые матрицы
Мы уже узнали, как создавать массивы из базовых типов данных, но иногда
подходящего базового типа данных не существует. Например, Санта должен
отличать непослушных детей от хороших, а детей огромное множество. Два
значения означают, что нам нужен только 1 бит на одного ребенка. Можно использовать байт для каждого значения, но это менее эффективно, потому что
приведет к глобальному потеплению на Северном полюсе и плохим новостям
для Снеговика Фрости1, поскольку таяние снегов считается уже существующим
условием и не учитывается примером. Что нам действительно нужно, так это
массив битов, или битовая матрица.
Битовые матрицы легко создавать. Предположим, что мы хотим отслеживать
35 бит. Мы знаем, что массива памяти из пяти 8-битных байт будет достаточно,
как показано на рис. 7.4.
bits0 7

6

5

4

3

2

bits1 15 14 13 12 11 10

1

0

9

8

bits2 23 22 21 20 19 18 17 16
bits3 31 30 29 28 27 26 25 24
bits4

34 33 32

Рис. 7.4. Массив как битовая матрица
1

«Снеговик Фрости» («Frosty the Snowman») — американский рождественский
короткометражный мультфильм, снятый в 1969 году. — Примеч. ред.

238   Глава 7. Организация данных
Для битовых матриц доступны четыре основные операции: задать бит, сбросить бит (задать его значение равным 0), проверить, задан ли бит, и проверить,
сброшен ли бит.
Можно использовать целочисленное деление, чтобы найти байт, содержащий
определенный бит, — для этого используется деление на 8. Сделать это быстро
можно на устройствах циклического сдвига (см. раздел «Сдвиг» на с. 143), сдвинув желаемый номер бита вправо на 3. Например, бит номер 17 будет сдвинут
в третий байт, потому что 17 ÷ 8 равно 2 в целочисленном делении, а байт 2 — это
третий байт, отсчитываемый от 0.
Следующий шаг — создание маски для битовой позиции. Подобно своему физическому аналогу, маска представляет собой битовый узор с отверстиями, через
которые «можно смотреть». Начинаем с операции И для желаемого номера
бита с маской 0x07, чтобы получить три младших бита; для 17 это 00010001
И 00000111, что дает 00000001, или позицию бита 1. Затем сдвигаем 1 влево на
эту величину, получая маску 00000010 — это позиция бита 17 в байте 2.
Используя индекс массива и битовую маску, легко выполнить следующие операции:
Задать бит

битыиндекс = битыиндекс ИЛИ маска

Сбросить бит

битыиндекс = битыиндекс И (НЕ маска)

Проверить, задан ли бит

(битыиндекс И маска) ≠ 0

Проверить, сброшен ли бит

(битыиндекс И маска) = 0

Есть еще одно полезное применение битовых матриц: проверка того, доступны
или заняты ресурсы. Если заданный бит представляет занятый ресурс, можно
просканировать массив в поисках байта, который состоит не только из единиц.
Это позволяет проверять восемь бит одновременно. Конечно, прежде чем обнаружить чистый бит, нужно найти байт, который его содержит, но это намного
эффективнее, чем проверять каждый бит по отдельности. Обратите внимание,
что в подобных случаях более эффективно использовать массив с самым большим базовым типом данных, например unsigned long в языке C, вместо массива
байтов.

Строки
Вы узнали о кодировании символов в разделе «Представление текста» на с. 63.
Последовательность символов, например в этом предложении, называется
строкой.
Как и в случае с массивами, часто нужно знать длину строки, чтобы иметь
возможность работать с ней. Обычно недостаточно просто создать массив для

Строки   239
каждой строки, потому что многие программы работают со строковыми данными
переменной длины; большие массивы часто используются, когда длина строки
заранее неизвестна. Поскольку размер массива не связан с длиной строки, нужен другой метод для отслеживания ее длины. Самый удобный способ сделать
это — связать длину строки с ее данными.
Один из подходов — хранить длину в самой строке, например в первом байте.
Это работает, но ограничивает длину строки 255 символами, что недостаточно
для многих областей применения. Для поддержки более длинных строк можно
использовать больше байтов, но в определенный момент количество оверхедов
(байтов длины строки) превысит длину самой строки. Кроме того, поскольку
строки представлены байтами, они могут иметь любое выравнивание, но для
многобайтового счетчика строки должны быть выровнены по его границам.
Язык C использует другой подход, заимствованный из псевдоинструкции .ASCIZ
языка ассемблера PDP-11, в которой нет специального типа данных для строк,
как в некоторых языках. Он просто использует одномерные массивы байтов;
байтовый тип данных в С — char именно потому, что строки представляют собой
массивы символов. Но есть одна хитрость: C не хранит длину строки. Вместо этого он добавляет дополнительный байт в конец массива символов для хранения
символа NUIL-терминатора. C использует символ NUL в ASCII (см. табл. 1.11),
имеющий значение 0, в качестве терминатора строки. Другими словами, NULтерминатор используется для обозначения конца строки. Это работает как для
ASCII, так и для UTF-8. Пример показан на рис. 7.5.
0

1

2

3

4

5

6

C

h

e

e

s

e

NUL

Рис. 7.5. Хранение строки с ограничением в С
Как видите, C использует 7 байт памяти для строки, хотя она состоит всего
из шести символов, потому что для ограничителя требуется дополнительный
байт.
Символ NUL хорош в качестве ограничителя, потому что большинство компьютеров имеют инструкцию, которая проверяет, равно ли значение строки 0. Любой
другой символ потребует дополнительных инструкций для загрузки значения,
которое мы будем проверять.
У использования терминатора строки вместо явной длины есть свои преимущества и недостатки. С одной стороны, хранилище компактно, что важно, и, по
сути, не требует оверхедов для таких операций, как «выводить каждый символ,
пока не будет достигнут конец строки». Однако для получения длины строки
нам понадобится просканировать всю строку до конца, считая символы. Кроме
того, при таком подходе в строке не может использоваться символ NUL.

240   Глава 7. Организация данных

Составные типы данных
Однокомнатные квартиры — это, конечно, неплохо, но чаще люди предпочитают что-нибудь получше, например апартаменты. В большинстве современных
языков есть способы спроектировать «апартаменты» на свой вкус, называемые структурами. В качестве комнат в «апартаментах» выступают элементы
структур.
Допустим, мы пишем программу «Календарь», которая содержит список (массив) событий с датами и временем их начала и окончания. Делая это на языке C,
каждый из объектов, соответствующий дню, месяцу, часам, минутам и секундам,
мы помещали бы в отдельный контейнер типа unsigned char, но для года следовало бы использовать контейнер типа unsigned short. На рис. 7.6 показана
структура для размещения даты и времени.
часы

минуты

секунды

год

месяц

день

Рис. 7.6. Структура даты и времени
Замечу, что размещать дату и время подобным способом отнюдь не обязательно. Можно задействовать неструктурированные массивы часов, минут и т. д.
Но использовать массивы со структурами «дата — время» гораздо удобнее.
Такие программы воспринимаются гораздо легче. Британский ученый в области компьютерных наук Питер Ландин (Peter Landin) в 1964 году ввел термин
«синтаксический сахар». Под сахаром подразумевались подобные структуры,
которые как бы делали программы «слаще». Конечно, тут есть место жарким
философским дебатам: то, что один считает подсластителем, для другого может
оказаться необходимой функциональностью. Многие утверждают, что синтаксический сахар сводится к таким вещам, как замена a = a + 1 на a += 1 или a++,
и лишь немногие — что массивы структур являются «сахаром» для наборов
массивов. Оглядываясь назад, мы понимаем, что первое утверждение спорно:
a += 1 и a++ были введены, поскольку компиляторам, которые в прошлом не
были столь совершенны, было проще преобразовывать их в машинный язык.
Вместе с тем, только появившись, структуры были слаще по сравнению с ранее
использовавшимися массивами. Сейчас значение структур увеличилось, так как
они широко используются в разработке программ.
Принципы работы с базовыми типами данных применимы и к составным типам данных наподобие структуры «дата — время». Рисунок 7.7 объединяет две
структуры «дата — время» с небольшим массивом, хранящим строки с именем
события. Это позволяет отразить в календаре всю информацию о событии.
Структуры часто занимают больше места, чем можно ожидать. Расположение
данных в памяти с выравниванием по естественным границам и без него обсуждалось в разделе «Память» на с. 136. Допустим, мы разместили структуру

Составные типы данных   241

начало

часы

минуты

секунды

год

месяц

день

конец

часы

минуты

секунды

год

месяц

день

имя события

массив символов (строка)

Рис. 7.7. Структура организации данных календаря
даты и времени в памяти 32-разрядного компьютера, как показано на рис. 4.2 на
с. 138. Язык сохраняет элементы структуры в порядке, указанном программистом. Порядок может иметь значение. Но язык также должен соблюдать выравнивание (рис. 4.3 на с. 138). Язык не может поместить год в четвертый и пятый
байты, как показано на рис. 7.7, потому что при этом произойдет пересечение
границ. Средства языков программирования решают эту проблему, автоматически выделяя дополнительное место для элемента структуры. Фактическое
использование памяти структурой будет выглядеть, как показано на рис. 7.8.
часы

минуты

секунды

дополнение

год

месяц

день

Рис. 7.8. Структура для даты и времени с дополнением
Можно перегруппировать элементы структуры, чтобы в итоге получить 7-байтовую структуру и избежать выделения дополнительного байта. Когда вы снова
объедините пару таких элементов, языковые инструменты, скорее всего, расширят структуру до 8 байт.
Отмечу, что это выдуманный пример. Не обязательно размещать даты и время
именно таким образом. Стандартным во многих системах подходом, пришедшим из UNIX и существующим с момента начала «эпохи UNIX» — 1 января
1970 года, является использование 32-битного числа для представления числа
секунд. Возможности 32-битного числа для представления числа секунд будут
исчерпаны в 2038 году, но во многих системах разработчики заранее подготовились, расширив это число до 64 бит.
На рис. 1.21 показано использование четырех 8-битных контейнеров для представления цвета с учетом прозрачности. Составная структура отлично справится
с этой задачей, но ее использование не всегда оправданно с точки зрения доступа
к данным. Например, если требуется скопировать значение цвета, то лучше
скопировать все 32 бита сразу, а не делать четыре копирования по 8 бит за раз.
Помочь в этом случае может другой тип структур.
Существуют не только «апартаменты», рассмотренные в предыдущем разделе, но
и «офисы с передвижными перегородками». Такие офисы в языке C называются
объединениями. Объединение позволяет упорядочивать представление данных
в определенном сегменте памяти. Разница между структурой и объединением

242   Глава 7. Организация данных
в том, что элементы, входящие в структуру, занимают память, а элементы, входящие в объединение, делят эту память между собой. На рис. 7.9 для создания
объединения структура RGBα совмещена с беззнаковым длинным числом.
пиксель
components.red

components.green

components.blue

components.alpha

цвет

Рис. 7.9. Объединение для пикселя
Используя объединение и синтаксис языка C, можно установить значение pixel.
color, равное 0x12345678, следовательно, pixel.components.red будет равно
0x12, pixel.components.green будет равно 0x34 и т. д.

Односвязные списки
Массивы — это наиболее эффективный способ хранения списков. Они содержат только сами данные (data), не требуя дополнительной информации для
их идентификации. С произвольными объемами данных дело обстоит иначе,
потому что при создании недостаточно большого массива, при его заполнении
придется создавать новый массив большего размера и копировать в него данные
из старого. А чрезмерно большой массив займет лишний объем памяти, который
не будет использоваться. К тому же без копирования не обойтись при вставке
элемента в середину массива или его удалении.
Если точное количество объектов неизвестно, лучшим решением будут связанные списки. Односвязные списки с использованием структур показаны на
рис. 7.10.
head

next

next

next

data

data

data

NULL

Рис. 7.10. Односвязный список
Обратите внимание, что next — это указатель, содержащий адрес следующего
элемента в списке. Первый элемент в списке известен как head; последний элемент — tail. Можно распознать окончание списка, потому что указатель next
перед концом списка содержит значение, которое не может соответствовать
элементу списка. Обычно это значение NULL.
В отличие от списка, показанного на рис. 7.10, все элементы массива расположены в памяти последовательно. Элементы списка могут находиться в любом
месте памяти, их организация показана на рис. 7.11.

Односвязные списки   243

Память

next

NULL

data

head

next
data
next
data

Рис. 7.11. Односвязный список в памяти
Добавить элемент в список легко; нужно просто поместить его перед первым
элементом списка, как показано на рис. 7.12.
вставить
next

head

data

next

next

data

data

Рис. 7.12. Вставка элемента в односвязный список
Удалить элемент немного сложнее. Для этого необходимо, чтобы указатель next
из элемента перед удаляемым направлял на следующий за удаляемым элемент,
как показано на рис. 7.13.
Удалить

next

next

next

data

data

data

Рис. 7.13. Удаление элемента из односвязного списка
Один из способов удаления связан с использованием пары указателей и показан
на рис. 7.14.
Указатель current перемещается по списку в поисках элемента для удаления.
Указатель previous позволяет поместить указатель next перед удаляемым
элементом списка. Для обозначения элемента используется точка (.), то есть
current.next означает следующий элемент текущего узла.

244   Глава 7. Организация данных
Начало
previous

current

NULL

head

Текущий
элемент current
равен NULL?
current

current.next

previous

current

Да

Нет
Нет

Текущий
элемент current
подлежит удалению?

Да
head

current.next

Да

Предыдущий
элемент previous
равен NULL?

Нет

previous.next

current.next

Выполнено

Рис. 7.14. Удаление элемента односвязного списка с помощью пары указателей
ПРИМЕЧАНИЕ
Рисунок 7.14 не идеальный пример; хотя, если честно, при работе над этим разделом мне встретились в Сети и намного худшие алгоритмы. Проблема с этим кодом
в том, что необходимость специальной проверки начального элемента списка его
усложняет.
Алгоритм на рис. 7.15 показывает, как использование косвенной адресации
второго порядка устраняет потребность в сложной логике и делает код проще.
Рассмотрим работу этого алгоритма подробнее. Взгляните на рис. 7.16. Индексы
показывают, как изменяется значение current по мере выполнения алгоритма.
Шаги, показанные на рис. 7.16, сложны для понимания, поэтому рассмотрим
их детально:
1. Мы начинаем с записи в current0 адреса head, вследствие чего current1
указывает на head. Это означает, что current указывает на head, который
указывает на элемент списка A.

Односвязные списки   245

Начало

адрес элемента head

current

Содержит ли
память, к которой
обращается current,
NULL?

current

Да

Нет

адрес current.next

Содержит ли
current адрес узла,
подлежащего удалению?

Нет

Да
Память, адресованная с помощью current

Следующий узел для удаления

Выполнено

Рис. 7.15. Удаление элемента односвязного списка с использованием
косвенной адресации
Удали меня
head

current0

A

B

C

head

A.next

B.next

C.next

current1

current2

current3

current4

D

E

Рис. 7.16. Удаление элемента односвязного списка в действии
2. Мы не ищем элемент А, поэтому двигаемся дальше.
3. Как показано пунктирной стрелкой, мы записываем в current адрес из
указателя next в элементе, на который current указывает в данный момент.
Поскольку current1 указывает на head, который указывает на элемент A,
current2 в конечном итоге указывает на A.next.
4. Это все еще не тот элемент, который мы хотим удалить, поэтому мы повторяем операцию, в результате чего current3 ссылается на B.next.
5. Это все еще не тот элемент, который мы хотим удалить, поэтому мы повторяем операцию, при этом current4 будет ссылаться на C.next.
6. C.next указывает на элемент D, который мы хотим удалить. По тонкой
пунктирной стрелке мы проходим путь current от C.next до D и заменя-

246   Глава 7. Организация данных
ем C.next значением D.next. Поскольку D.next указывает на элемент E,
C.next теперь указывает на E, как показано жирной пунктирной стрелкой,
удаляя D из списка.
Можно изменить подготовительный алгоритм, добавив переходы к середине
списка. Это полезно в случае необходимости упорядочивания по дате, имени
или другим критериям.
Я упоминал, что второй алгоритм позволяет получить более качественный код.
Сравним два варианта, записанные на языке программирования С. Чтобы увидеть разницу между листингом 7.1 и листингом 7.2, понимания кода не требуется.
Листинг 7.1. Код языка C для удаления элемента односвязного списка с помощью
пары указателей
struct node {
struct node *next;
// data
};
struct
struct
struct
struct

node
node
node
node

*head;
*node_to_delete;
*current;
*previous;

previous = (struct node *)0;
current = head;
while (current != (struct node *)0) {
if (current == node_to_delete) {
if (previous == (struct node *)0)
head = current->next;
else
previous->next = current->next;
break;
}
else {
previous = current;
current = current->next;
}
}

Листинг 7.2. Код языка C для удаления односвязного списка с использованием
косвенной адресации второго порядка
struct node {
struct node *next;
// data
};
struct node *head;
struct node *node_to_delete;
struct node **current;

Динамическое выделение памяти   247
for (current = &head; *current != (struct node *)0; current = &((*current)>next))
if (*current == node_to_delete) {
*current = node_to_delete->next;
break;
}
}

Можно заметить, что код с косвенной адресацией в листинге 7.2 намного проще,
чем код с парой указателей в листинге 7.1.

Динамическое выделение памяти
При рассмотрении вставки элемента в связанный список был опущен один важный момент. Я показал, как вставить новый узел, но не сказал, откуда взялась
память для этого узла.
На рис. 5.16 мы видели, что место в памяти для данных программы начинается с раздела для статически выделенных данных, за которым следует куча,
которую создает библиотека среды выполнения для этой программы. Это вся
память, доступная программе (за исключением стека и векторов прерываний)
на машинах без модулей управления памятью (MMU). Так как использование
всего объема памяти не имеет смысла, на машинах с MMU библиотека среды
выполнения запрашивает выделение объема памяти, который, по ее мнению, необходим программе. Разрыв (break) означает конец участка памяти, выделенной
программе. Существуют некоторые системные вызовы, которые увеличивают
или уменьшают объем доступной памяти.
Память для переменных, таких как массивы, является статической; то есть
ей назначается адрес, который не меняется. Узлы списка, наоборот, являются
динамическими; они поступают в ячейки и уходят из ячеек памяти по мере необходимости. Мы берем память для них из кучи.
Программе нужен способ управления кучей. Ей нужно знать, какая часть памяти
используется, а какая является доступной для записи. Для этого существуют
библиотечные функции, так что вам не придется писать собственные. В языке C
это функции malloc и free. Посмотрим, как они применяются.
Один из способов применения malloc заключается в использовании структуры
односвязного списка. Куча разделена на блоки, каждый из которых имеет размер
(size) и указатель на следующий блок (next), как показано на рис. 7.17.
Изначально вся куча представляет один блок. Когда программа запрашивает
выделение памяти, malloc ищет блок, в котором достаточно места, возвращает вызывающему объекту указатель на блок и корректирует размер блока,
учитывая выделенную для программы память. Когда программа освобождает

248   Глава 7. Организация данных
память с помощью функции free, она просто помещает блок обратно в список
свободных блоков.
next

next

next

size

size

size

data

data

data

Рис. 7.17. Использование malloc для управления кучей
Время от времени malloc сканирует список в поиске свободных блоков, расположенных по соседству, и объединяет их в блок большего размера. Так как
выделение памяти требует прохождения по элементам в списке в поисках достаточно большого блока, это можно осуществить при выделении памяти (вызове
malloc). Со временем память может стать фрагментированной, что означает
отсутствие достаточного по объему блока памяти при наличии множества небольших блоков. В системах с MMU место разрыва можно корректировать, что
дает возможность использовать больший объем памяти при необходимости.
Этот подход сопряжен со значительными оверхедами: next и size добавляют
16 байт к каждому блоку на 64-разрядной машине.
Освобождение нераспределенной памяти — частая ошибка неопытных программистов. Другая ошибка — учет памяти, которая уже освобождена. Как показано
на рис. 7.17, записывая данные за пределы выделенной памяти, можно исказить
значения size и next. Это чревато сбоями, когда в дальнейшем потребуется использовать информацию из этих полей.
Один из побочных эффектов технологического прогресса — наличие у небольших вычислительных машин гораздо большего объема оперативной памяти, чем
требуется программе. В таких случаях лучше распределить память статически.
Это сократит оверхеды и позволит избежать ошибок, связанных с выделением
памяти.

Более эффективное выделение памяти
Часто встречаются связанные списки, содержащие текстовые строки. Предположим, у нас есть связанный список, в котором узел содержит указатель (pointer)
на строку (string), как показано на рис. 7.18.
Мы должны выделить память не только для каждого узла, но и для строки,
прикрепленной к узлу. Оверхеды на malloc могут быть значительными, особенно на 64-разрядной машине, где будет использоваться 16 байт оверхедов

Сборка мусора   249
для 16-байтного узла, а затем еще 16 байт для указания строки. Взгляните на
4-байтную строку с cat, показанную на рис. 7.18.
узел
строка

next
string

c

a

t

NUL

Рис. 7.18. Узел списка со строкой
Можно сократить оверхеды, упаковав узел и строку в один контейнер. Вместо
выделения места для узла, а затем строки можно выделить пространство для
узла и строки, сложив их объемы и добавив дополнительное пространство, которое может потребоваться для корректного размещения в памяти. Узлы имеют
переменный размер, и это нормально. Этот способ вдвое сокращает оверхеды.
Результат со строкой cat показан на рис. 7.19.
Менее эффективный
pointer

pointer

size
next
string

Более эффективный
pointer

size
c

a

t

size
next

NUL
c

a

t

NUL

string

Рис. 7.19. Более эффективное выделение памяти
Этот подход эффективнее и при удалении узлов. В первом варианте для удаления данных потребуются два вызова функции free: один — для строки, а другой — для узла. Во втором случае требуется лишь один вызов функции free.

Сборка мусора
При применении динамического управления памятью могут возникнуть две
проблемы, которые являются результатом неаккуратного использования
указателей. Напомню, что указатель — это просто число, представляющее
адрес в памяти. Но не все числа являются допустимыми адресами. Указание
на несуществующую память или память, которая не соответствует заданным
процессором правилам выравнивания данных, может вызвать ошибку обработки и сбой программы.
Возможно, вы изучаете язык программирования, такой как Java или JavaScript,
который не имеет указателей, но поддерживает динамическое выделение памяти без использования аналогов malloc и free. Вместо этого названные языки

250   Глава 7. Организация данных
реализуют сборку мусора — метод, изобретенный в 1959 году американским
специалистом по сomputer science и когнитивистом Джоном Маккарти (John
McCarthy) (1927–2011) для языка программирования LISP. Сборка мусора пережила второе рождение, став средством для решения проблемы неправильного
использования указателей.
Такие языки, как Java, используют ссылки вместо указателей. Ссылки — это
абстракции для указателей, обеспечивающие большую часть той же функциональности без фактического предоставления адресов в памяти.
Языки с автоматической сборкой мусора часто имеют в своем арсенале оператор new, который создает элементы и выделяет для них память (этот оператор
также имеется в языках, не использующих автоматическую сборку мусора,
таких как C++). Для удаления элемента не существует определенного оператора. Вместо этого языковая среда выполнения отслеживает использование
переменных и автоматически удаляет те, которые, по ее мнению, больше не
используются. Способов для этого много, и один из них заключается в подсчете ссылок на переменные и удалении переменных в случае отсутствия
ссылок на них.
Сборка мусора — это компромисс; он имеет и свои недостатки. Одна из проблем
сборки мусора аналогична проблеме обновления LSI-11 (см. «Оперативная
память» на с. 125). Суть ее в том, что программист не имеет достаточного контроля над сборкой мусора, которая может запуститься, даже несмотря на то,
что программе нужно сделать что-то более важное. Кроме того, программы, как
правило, занимают много памяти, потому что легче оставлять ненужные ссылки,
а не удалять их. Такой подход препятствует восстановлению памяти и вызывает
замедление работы программы. Однако сбоя программы, который наступил бы
из-за некорректных указателей, в данном случае не происходит. Несмотря на
преимущество решения проблем с указателями, проблему отслеживания ненужных ссылок порой устранить труднее.

Двусвязные списки
Так как при удалении элемента для обработки указателя в односвязном списке
нужно найти элемент, предшествующий удаляемому, функция delete может
быть довольно медленной. Если список большой, поиск может выполняться
очень долго. К счастью, существует другой тип списка, который решает эту
проблему за счет использования дополнительной памяти.
Двусвязный список содержит ссылку не только на следующий элемент, но и на
предыдущий, как показано на рис. 7.20. Это удваивает оверхеды для каждого
узла, но устраняет необходимость перемещения по списку для функции delete.
Это позволяет достичь компромисса между количеством необходимой памяти
и временем выполнения.

Иерархические структуры данных   251

previous

previous

previous

next

next

next

data

data

data

Рис. 7.20. Двусвязные списки
Преимущество двусвязного списка в том, что можно вставлять и удалять элемент
в любом месте списка без просмотра ненужных элементов. На рис. 7.21 показано,
как происходит добавление нового узла после элемента A.
Новый
previous
next

Элемент А

data

previous

previous

next

next

data

data

Рис. 7.21. Вставка в двусвязном списке
На рис. 7.22 показано, что удалить элемент так же просто.
Старый
previous

previous

previous

next

next

next

data

data

data

Рис. 7.22. Удаление в двусвязном списке
Как видите, при выполнении этих операций не нужно просматривать все элементы списка.

Иерархические структуры данных
До сих пор мы рассматривали только линейные структуры данных. Они отлично
подходят для многих приложений, но в какой-то момент их линейность может
стать проблемой. Хранить данные — лишь половина дела; необходимо еще и эффективно их извлекать. Допустим, у нас есть перечень элементов, хранящихся
в связанном списке. Возможно, нам придется пройти весь список, чтобы найти

252   Глава 7. Организация данных
искомый элемент; для списка длиной n может потребоваться n просмотров.
Это не критично для небольшого списка, но для больших значений n подобный
подход неэффективен.
Ранее мы рассмотрели использование указателей для соединения узлов в списки.
Мы не ограничены количеством указателей — только воображением и объемом
памяти. Можно расположить узлы иерархически, как в примере на рис. 5.4.
Простейшей иерархической структурой данных является двоичное дерево —
«двоичное» не из-за двоичных чисел, а потому, что узел может соединяться
с двумя другими узлами. Создадим узел, содержащий число (number), как показано на рис. 7.23.
root

number

left
number

left

right

right
number

left

right

Рис. 7.23. Узлы двоичного дерева, содержащие числа (left — левый, right — правый)
Корень (root) для древовидной структуры — это аналог элемента head из связанного списка.
Поиграем в лото и запишем числа в форме двоичного дерева по мере их выпадения. Затем посмотрим номера и узнаем, какие числа выпали во время игры.
На рис. 7.24 показан алгоритм, который добавляет число в дерево. Он работает
аналогично удалению узла в односвязном списке, в том смысле что он основан
на косвенной адресации.
Посмотрим на алгоритм в действии, вставив числа 8, 6, 9, 4 и 5. Когда мы вставляем 8, к корню root ничего еще не прикреплено, поэтому прикрепляем это
число. Когда мы вставляем 6, корень уже занят, поэтому сравниваем число 6 со
значением в корневом узле; затем, поскольку 6 меньше 8, выбираем левую сторону. Она свободна, поэтому прикрепляем там новый узел. 9 находится справа
от 8, 4 — слева от 6 и т. д., как показано на рис. 7.25.
Хотя в этой структуре содержится пять узлов, но, проверив в худшем случае
всего три узла, мы найдем нужный. Это гораздо эффективнее связанного списка,
где может потребоваться проверить все пять узлов. Искать элемент в двоичном
дереве, как показано на рис. 7.26, достаточно легко. В указателях нет необходимости, так как дерево менять не нужно.

Иерархические структуры данных   253

Начало

current

адрес элемента root

Указывает ли
current на элемент
со значением
NULL?

Да

Создать новый узел

Сохранить число в новом узле

Нет

Память, адресованная
переменной current
Адрес нового узла

Выполнено
current
адрес
элемента current.left

Сравнить
число со значением
в узле current

<

>

адрес
current
элемента current.right

=
Повтор.
Кто-то жульничает!

Выполнено

Рис. 7.24. Алгоритм вставки в двоичное дерево
root

8

6

4

9

5

Рис. 7.25. Двоичное дерево

254   Глава 7. Организация данных
Начало

current

current

current.left

<

root

Является ли
элемент current
NULL?

Да

Сравнить
число со значением
в узле current

>

Выполнено, числа нет вдереве

current

current.right

=
Число найдено!

Выполнено

Рис. 7.26. Алгоритм просмотра двоичного дерева
Примечательно, что распределение чисел по ветвям зависит от порядка вставки.
На рис. 7.27 показано, что произойдет, если вставить числа по порядку: 4, 5, 6, 8 и 9.
Этот вырожденный случай очень похож на односвязный список. Мы не только
теряем преимущества двоичного дерева — из-за неиспользуемых левых указателей снижается эффективность использования памяти. Лучше, чтобы дерево
выглядело, как показано на рис. 7.28 справа.
Поиск в двоичном дереве зависит от глубины дерева; если элемент находится
на n уровней ниже корня, то для его поиска требуется n проверок. Для этого
нужно только log2 n запросов в случае сбалансированного двоичного дерева, а не
n запросов, как в связанном списке. Можно утверждать, что в худшем случае
придется просмотреть 1024 узла в связанном списке, содержащем 1024 узла, но
нужно будет проверить только 10 узлов в сбалансированном двоичном дереве.
Существует множество алгоритмов балансировки деревьев, которые я не буду
здесь подробно описывать. Чтобы сбалансировать дерево, требуется время, так
что необходим компромисс между скоростью алгоритма, временем вставки/
поиска и временем балансировки. Алгоритмы балансировки деревьев требуют
затрат вычислительной мощности, а некоторые — еще и выделения дополнительной памяти для хранения. Однако использование дополнительной памяти
довольно скоро, по мере увеличения размера дерева, оправдывает себя, поскольку log2 n становится намного меньше n.

Хранение данных на дисковых устройствах   255

root

4

5

6

8

9

Рис. 7.27. Плохо сбалансированное двоичное дерево

Рис. 7.28. Несбалансированное и сбалансированное двоичные деревья

Хранение данных на дисковых устройствах
О дисководах мы говорили еще в разделе «Блочные устройства» на с. 129. Рассмотрим их подробнее, чтобы понять особенности организации данных при
использовании этих устройств. Осторожно: впереди множество указателей!
Я упоминал, что основной единицей на диске является блок. Последовательно
расположенные блоки называются кластерами. Данные можно хранить в кластерах, занимающих смежные секторы на дорожке. Так поступают, когда требуется
очень высокая производительность. В остальных случаях это не очень хороший
способ. К тому же количество данных может превышать объем, размещаемый
на одной дорожке. Поэтому данные хранятся в любых доступных секторах. Иллюзию непрерывного хранения обеспечивает драйвер устройства операционной
системы. Теперь ситуация выглядит знакомой, но есть нюанс: вместо одного

256   Глава 7. Организация данных
блока для хранения объекта нам нужно найти достаточное количество блоков
фиксированного размера и разделить объект между ними.
Связанные списки — не самое лучшее решение для учета свободных и используемых блоков диска, потому что последовательный анализ элементов происходит
слишком медленно. На диске 8 TиБ содержится почти 2 миллиарда блоков,
и при наихудшем сценарии доступны 250 блоков в секунду, то есть просмотр
всего диска займет более 15 лет, что делает этот способ непрактичным. Звучит
действительно удручающе, но имейте в виду, что это 1 МиБ данных в секунду.
Когда мы управляем данными в основной памяти, достаточно ссылаться на них
с помощью указателя. Но указатели являются временными объектами, а так как
диски используются для долгосрочного хранения данных, нужно что-то более
постоянное. Вы уже видели ответ: имена файлов. Необходимо каким-то образом
сохранить их на диск и связать их с блоками, используемыми для хранения
данных самих файлов.
Один из способов управления этими операциями восходит к — совершенно
верно — UNIX. Ряд блоков выделяется в качестве aйнодов (inode) — название,
образованное соединением сокращения слова «индекс» (index) и слова «узел»
(node); таким образом, айноды являются индексными узлами. Айнод содержит
сведения о файле, такие как информация о владельце, размере и разрешениях
на доступ, а также индексы блоков, которые содержат данные файла, как показано на рис. 7.29.
Блоки
сдвойной
косвенной
адресацией
Айнод
Информация

Блоки
спрямой
адресацией

Блоки
стройной
косвенной
адресацией

Блоки
скосвенной
адресацией
...

...

...

...
...
...

Рис. 7.29. Структура данных в файловой системе
Выглядит довольно сложно, но только на первый взгляд. Айнод обычно имеет
12 прямых указателей на блоки (на самом деле это не указатели, а просто индексы блоков), которые поддерживают файлы длиной до 4096 × 12 = 49 152 байта.
Этого достаточно для большинства файлов. Если файл больше, то используются

Хранение данных на дисковых устройствах   257
блоки косвенной адресации. Предполагая использование 32-разрядных индексов
(хотя скоро они должны быть 64-разрядными), можно в одном айноде записать
индексы 1024 блоков косвенной адресации, каждый из которых занимает по
4 байта, помещенных в один блок, добавляя еще 4 МиБ к максимальному размеру файла. Если этого недостаточно, 4 ГиБ доступны при использовании блоков
с двойной косвенной адресацией и, наконец, еще 4 ТиБ — через использование
блоков с тройной косвенной адресацией.
Часть информации, хранящейся в айноде, показывает, что содержат блоки:
сведения о каталоге или другие данные. При помощи каталога устанавливается
связь между именами файлов и индексами, которые соответствуют файлам.
Одна из приятных особенностей работы UNIX состоит в том, что каталог на
самом деле — просто файл другого типа. Это означает, что он может ссылаться на другие каталоги, что и дает знакомые иерархические файловые системы
с древовидной структурой.
Все это кажется очень похожим на произвольное дерево. Так и есть, но лишь до
определенного момента. Одной из особенностей иерархического расположения
является возможность для нескольких айнодов ссылаться на одни и те же блоки
с помощью ссылок. Ссылки позволяют одному и тому же файлу находиться в нескольких каталогах. Оказывается, очень удобно также ссылаться на каталоги.
Для этого были изобретены символические ссылки. Но символические ссылки
могут привести к зацикливанию в графе файловой системы, поэтому нужен
специальный код программы для обнаружения и предотвращения бесконечного цикла. В любом случае эта сложная иерархическая структура отслеживает
используемые блоки, но еще не хватает эффективного способа отслеживать
свободное пространство.
Один из способов отследить свободное пространство — использовать битовую
карту (см. «Битовые матрицы» на с. 237) с 1 битом для каждого блока на диске.
Битовая карта может быть довольно большой: дисковому накопителю объемом
8 Тбайт потребуется почти 2 миллиарда бит, на что уйдет около 256 Мбайт.
Однако этот способ все-таки эффективен. Тратится менее чем 0.01 % от общего
объема дискового пространства, и не обязательно это должно быть в памяти
одновременно.
Работа с битовыми картами довольно проста и эффективна, особенно если они
хранятся в 64-битных словах. Предположив, что 1 указывает на используемый
блок, а 0 указывает на свободный блок, можно легко искать слова, которые состоят не только из единиц, чтобы находить свободные блоки.
Но есть проблема: граф файловой системы и битовая карта могут рассинхронизироваться. Например, может произойти сбой питания во время записи данных на
диск. В «темные времена», когда на компьютерах были переключатели и мигающие лампочки, пришлось бы восстанавливать поврежденную файловую систему,
вводя номера айнодов с помощью этих самых переключателей. Необходимость

258   Глава 7. Организация данных
в этом отпала с приходом программы fsck. Она следует по графу файловой
системы и сопоставляет его с данными о свободных блоках. Это лучше, чем
было, но по мере увеличения дисков обработка занимает все больше и больше
времени. Новые архитектуры файловой системы с ведением журнала повышают
эффективность восстановления поврежденных данных.

Базы данных
Двоичные деревья — отличный способ хранения данных в памяти, но они не
так хороши для хранения огромных объемов данных, которые не помещаются
в память. Отчасти это связано с тем, что узлы дерева, как правило, небольшие
и поэтому плохо справляются с хранением данных о секторах диска.
База данных — это просто набор данных, организованных определенным образом. Система управления базами данных (СУБД) — это программа, позволяющая хранить информацию в базе данных и извлекать ее оттуда. СУБД обычно
включает в себя ряд интерфейсов, расположенных поверх базового механизма
хранения.
Базы данных являются примером применения B-деревa — структуры данных,
изобретенной немецким ученым в области компьютерных наук Рудольфом
Байером (Rudolf Bayer) и его американским коллегой Эдом Маккрайтом (Ed
McCreight) в «Боинге» в 1971 году. B-дерево — это сбалансированное, но не
двоичное дерево. Оно требует немного больше места, чем сбалансированное
двоичное дерево, но работает лучше, особенно когда данные хранятся на диске.
Это еще один пример того, как понимание архитектуры памяти позволяет более
эффективно ее использовать.
Допустим, у нас есть сбалансированное двоичное дерево имен, упорядоченных
по алфавиту. Оно будет выглядеть примерно так, как показано на рис. 7.301.
Ken

Dennis

Brian

Rob

Doug

Mike

Steve

Рис. 7.30. Сбалансированное двоичное дерево

1

Здесь и далее имена и фамилии приведены без перевода, так как пример основан на
английском алфавите. — Примеч. ред.

Индексы   259
Узел B-дерева имеет намного больше ответвлений (дочерних элементов)
по сравнению с узлами двоичного дерева. Количество ответвлений выбирается так, чтобы узел точновписывался в дисковый блок, как показано на
рис. 7.31.
A–Z
...
A–M

N–Z
. ..

.. .
Brian

Dennis

Doug

Ken

Mike

Rob

Steve

Рис. 7.31. B-дерево
Как видите, внутренние узлы сбалансированы, что обеспечивает предсказуемое
время поиска. На рис. 7.31 есть неиспользуемые дочерние ссылки, которые
занимают много места. Когда дочерние ссылки заканчиваются, дерево легко
перебалансировать, просто изменив диапазон, охватываемый узлом. Например,
если у узла A-M закончились дочерние узлы, его можно разделить на узлы A-G
и H-M. Алфавит не лучший пример, так как оптимальным подходом в подобных случаях является разделение на части, кратные двум. Число же элементов
в этом примере нечетное.
Большее количество ссылок в узле означает меньше узлов. Крупные узлы
не вызывают проблем, так как они не превышают размера дискового блока,
который может быть обработан как единое целое. Немного пустого пространства из-за неиспользуемых дочерних ссылок остается, но это разумный
компромисс.

Индексы
Упорядочивание данных позволяет организовать эффективный доступ к ним.
Однако нам часто требуется доступ к данным, упорядоченным более чем по
одному критерию, например имена и фамилии или имена и любимые группы.
На рис. 7.31 показаны узлы, упорядоченные по именам. Эти узлы часто называют первичным индексом. Но индексов может быть более одного, как показано
на рис. 7.32. Несколько индексов позволяют эффективно искать объекты различными способами.
Издержки при работе с индексами заключаются в необходимости их обслуживания. Каждый индекс должен обновляться при изменении данных. Если поиск
выполняется чаще модификации, эти издержки оправданны.

260   Глава 7. Организация данных

Brian Kernighan
Dennis Ritchie
Индекс имени

Doug McIlroy

A–M

A–M

Ken Thompson
Mike Les k
...

Индекс фамилии

...

A–Z . . .

...

A–Z

Rob Pike
Steve Bourne
N–Z

N–Z
...

...

Рис. 7.32. Множественные индексы

Перемещение данных
Ранее я упоминал, что использование массивов вместо связанных списков вынуждает копировать данные при необходимости увеличения массива. Копирование нужно, чтобы перемещать таблицы страниц в MMU и из него, битовые
карты на диск и с диска и т. д. Программы тратят много времени на перемещение
данных из одного места в другое, поэтому важно делать это эффективно.
Начнем с полумеры: установим значение длины length всех блоков памяти
равным 0, как показано на рис. 7.33.
Этот алгоритм работает, но не очень эффективно. Предполагая, что длительность
выполнения каждого блока на рис. 7.33 одинакова, мы тратим больше времени
на вспомогательные записи, чем на обнуление ячеек памяти. Применение развертывания цикла может сделать процесс более эффективным, что и показано на
рис. 7.34. Предположив, что length — четное число, развернем цикл так, чтобы
теперь больше времени тратилось на обнуление и меньше — на другие операции.
Универсальное средство решения подобных проблем, к счастью, имеется. Во
время работы в Lucasfilm канадский программист Том Дафф (Tom Duff) изобрел метод Даффа для ускорения копирования данных; рис. 7.35 показывает

Перемещение данных   261
вариант обнуления памяти. Этот подход работает только в том случае, если
length больше нуля.
Начало
current

Установить адрес первого байта в ноль

length > 0 ?

Нет

Выполнено

Да
Память, указанная в current

current

current + 1

length

length – 1

0

Рис. 7.33. Обнуление блока памяти
Начало
current

Установить адрес первого байта в ноль

length > 0 ?

Нет

Выполнено

Да
Память, указанная в current

current

current + 1

memory pointed to by current

current

length

0

0

current + 1

length – 2

Рис. 7.34. Обнуление блока памяти с развертыванием цикла

262   Глава 7. Организация данных
Начало
current

Установить адрес первого байта в ноль
length ÷8

восьмерки

0x7

length И

Да

0?

Память, указанная в current

0

Нет
current

Да

7?

Нет

Нет
5?

Нет

Нет
Да

3?

0

current + 1

Память, указанная в current
current

0

current + 1

Память, указанная в current
current

Да

0

current + 1

Память, указанная в current
current

Да

4?

Память, указанная в current
current

Да

6?

current + 1

0

current + 1

Память, указанная в current

0

Нет
current

2?

Да

current + 1

Память, указанная в current

0

Нет (1)
current

current + 1

Память, указанная в current
current
восьмерки

0

current + 1
восьмерки – 1

восьмерки > 0 ?

Да

Нет
Выполнено

Рис. 7.35. Обнуление блока памяти с помощью модифицированного метода Даффа

Перемещение данных   263
Метод Даффа разворачивает цикл восемь раз и перемещается в середину, чтобы
обработать все оставшиеся байты. Может возникнуть соблазн развернуть цикл
еще раз, но необходимо соблюдать размер кода, потому что его размещение
в кэше команд требует большой скорости.
На рисунке в части цикла видно, что соотношение времени восстановления
памяти к времени вспомогательных операций значительно увеличилось. Хотя
начальная настройка и ветвление в нужном месте цикла выглядят сложными,
на самом деле это не так. Для этого не требуется множество условных ветвей,
а всего лишь несколько операций с адресами:
1. Задать маску для всего, кроме трех младших битов, используя значение 0x7.
2. Вычесть результат из 8.
3. Задать маску для всего, кроме трех младших битов, используя значение 0x7.
4. Умножить на количество байтов между инструкциями по обнулению.
5. Добавить адрес первой инструкции по обнулению.
6. Ветвление по этому адресу.
Другой способ повысить эффективность — принять, что на 64-разрядной машине
8 байт могут быть обнулены одновременно. Конечно, для обработки оставшихся
байтов в начале и в конце требуется дополнительный код. Необходимо использовать алгоритм рис. 7.36 без цикла на восьмерках для начала и конца. В середине
обнуляем столько 8-байтовых блоков, сколько возможно.
Процесс усложняется, если вместо установления значения для блока копировать
блок. Ведь, скорее всего, источник и место назначения не будут иметь одинаковое
разбиение на байты. Следует проверять, что и источник, и приемник выровнены
по машинным словам. Несоответствие между разбиением на слова — обычное
дело.
Копирование имеет еще одну сложность. Она заключается в том, что копирование обычно используется для перемещения данных в области памяти. Например,
у нас может быть буфер со словами, разделенными пробелами, в котором мы
хотим прочитать первое слово и сжать все остальное, чтобы оставить место для
дополнительных данных. Нужно быть осторожными при копировании данных
в перекрывающихся областях; иногда приходится копировать данные обратно,
чтобы избежать их перезаписи.
Интересен пример раннего терминала растровой графики (см. «Растровая графика» на с. 230) под названием blit, разработанный канадским программистом
Робом Пайком (Rob Pike) в Bell Telephone Laboratories в начале 1980-х годов.
В то время еще не было практики создания индивидуальных интегральных
микросхем для выполнения подобных задач. Исходная и целевая области

264   Глава 7. Организация данных
памяти могли перекрываться, например в случае перетаскивания окна, и данные могли иметь любое выравнивание. Производительность была очень важна,
потому что процессоры были не очень быстры по сравнению с сегодняшними;
blit использовал Motorola 68000. MMU отсутствовал, поэтому Пайк написал
код, который анализировал источник и место назначения и сходу генерировал
оптимальный код для максимально быстрого копирования. Я создал аналог
для системы, использующей Motorola 68020. Это позволило добиться еще
более высокой производительности, ведь у 68020 был кэш команд, в который
идеально помещался сгенерированный код, так что не нужно было постоянно
обращаться к памяти с инструкциями. Следует отметить, что это послужило
предвестником JIT-метода (метода «точно в срок», just-in-time), используемого
на многих виртуальных машинах, включая Java.

Векторный ввод/вывод
Эффективное копирование данных важно для производительности системы,
но полное исключение копирования — лучший вариант. Большое количество
данных проходит через операционную систему в пользовательские программы
и из них, и эти данные часто расположены в памяти отдельными фрагментами.
Предположим, что мы генерируем некие аудиоданные в формате mp3 и хотим
записать их на аудиоустройство. Как и многие форматы файлов, mp3-файлы
состоят из нескольких фреймов, каждый из которых содержит заголовок, за
которым следуют данные. Типичный аудиофайл содержит несколько фреймов, которые во многих случаях имеют одинаковые заголовки, как показано
на рис. 7.36.
Заголовок
Циклический избыточный код
Дополнительная информация
Основные данные
Вспомогательные данные

Рис. 7.36. Макет фрейма mp3
Можно построить каждый фрейм, скопировав все данные в буфер. Но когда потребуется записать эти данные на аудиоустройство, придется копировать их еще
раз. В качестве альтернативы можно записать каждую часть каждого фрейма отдельно, но это увеличит затраты времени и мощности на переключение контекста
и в случае записи только части фрейма чревато проблемами с аудиоустройством.
Гораздо эффективнее просто передать системе набор указателей на каждый фрагмент фрейма и позволить собирать фреймы вместе в соответствии с порядком

Подводные камни объектно-ориентированного программирования   265
их написания, как показано на рис. 7.37. При этом необходимо обеспечить поддержку системных вызовов (readv, writev).
Заголовок
Вектор
размер
размер
размер
размер
размер

Циклический избыточный код
Дополнительная информация
Основные данные
Вспомогательные данные

Рис. 7.37. Сбор данных
Идея состоит в том, чтобы передать вектор размеров и указателей на данные
операционной системе, которая затем собирает их по порядку. Существуют
версии как для чтения, так и для записи: запись известна как сбор (gathering),
потому что данные собираются из многих мест, в то время как чтение известно
как рассеивание (scattering), потому что данные распределены по многим местам.
Вся концепция называется рассеивание/сбор (scatter/gather).
Рассеивание/сбор стали мейнстримом благодаря сетевому коду Беркли, на котором основана работа интернета. Я упоминал в разделе «TCP/IP» на с. 206, что
IP-данные отправляются в пакетах, а TCP отвечает за передачу пакетов целиком
и в правильном порядке. Пакеты, поступающие от конечной точки связи (таковой она может быть для вас, а для меня это сокет), собираются в непрерывный
поток для предоставления их пользовательским программам.

Подводные камни объектно-ориентированного
программирования
Поскольку вы учитесь программировать, возможно, вы изучаете объектноориентированный язык, такой как Java, C++, Python или JavaScript. Объектноориентированное программирование — отличная методология. Однако при
неправильном подходе она может привести к проблемам с производительностью.
Объектно-ориентированное программирование получило серьезное развитие
с языка C++. C++ — интересный пример. Изначально он был построен поверх
языка C, что позволяет увидеть, как он работает.
Объекты имеют методы, эквивалентные функциям, и свойства, эквивалентные
данным. Все необходимое для объекта может быть собрано в единую структуру данных. Использовать средства языка C для создания типов и указателей,

266   Глава 7. Организация данных
особенно указателей на функции, несомненно, хорошо. Структура объекта
в языке C может выглядеть примерно так, как показано на рис. 7.38.
Object
self
parent

Parent object

constructor

struct object *new() {. . .}

destructor

void gozer() {. . .}

method 1

void method_1() {. . .}

method 2

void method_2() {. . .}

method ...
property 1
property 2
property ...

Рис. 7.38. Структура объекта в языке С (object — объект, self — cобственно объект,
parent — родитель, constructor — конструктор, destructor — деструктор, method —
метод, property — свойство)
Некоторые свойства, такие как свойства с целочисленными значениями
(Property 1), содержатся в самой структуре объекта, в то время как другие требуют дополнительной памяти (Property 2), на которую ссылается структура
объекта.
Очевидно, что эта структура может стать довольно большой, особенно если
существует множество методов. Можно решить эту проблему, выделив методы
в отдельную структуру, как показано на рис. 7.39. Это еще один компромисс
времени и памяти.
Object
self
parent

Parent object

Methods

methods

constructor

struct object *new() {. . .}

property 1

destructor

void gozer() {. . .}

property 2

method 1

void method1() {. . .}

property . . .

method 2
method . . .

Рис. 7.39. Отдельная структура методов
Программисты использовали такой подход к объектно-ориентированному программированию задолго до того, как датский программист Бьерн Страуструп

Сортировка   267
(Bjarne Stroustrup) изобрел C++. Изначально C++ был оболочкой вокруг C,
которая делала возможным подобные преобразования.
Почему это важно? Идеологи объектно-ориентированного программирования
считают, что объекты способны решить любые задачи. Но, как видно на предыдущих рисунках, с объектами связано определенное количество вспомогательных данных. Объекты вынуждены использовать собственные методы вместо
глобально доступных функций и в результате упаковываются не так плотно, как
чистые типы данных. Поэтому когда производительность имеет первостепенное
значение, лучше придерживаться классических массивов.

Сортировка
Сортировка данных нужна по множеству причин. Иногда отсортированные
результаты, например имена в алфавитном порядке, легче искать. Очень часто
хранение данных в отсортированном виде ускоряет поиск за счет сокращения
количества обращений к памяти.
Я не буду углубляться в алгоритмы сортировки, потому что это довольно серьезная тема, широко освещенная в литературе. Существует множество хороших
функций сортировки, так что вряд ли вам придется писать собственные — если
только в качестве домашнего задания. Но есть несколько важных моментов,
которые следует иметь в виду.
Один из них в том, что, если размер сортируемых объектов больше размера
указателей на эти объекты, сортировку следует проводить, перемещая не сами
данные, а указатели на них.
Кроме того, существует соглашение о сортировке. Дерево для игры в лото позволяло принимать решения на основе арифметического сравнения — было
ли одно число меньше, равно или больше другого. Этот метод уходит корнями
к языку программирования FORTRAN, созданному в 1956 году и включавшему
инструкцию, показанную в листинге 7.3.
Листинг 7.3. Арифметическая операция IF (ЕСЛИ) в языке FORTRAN
IF (expression) branch1, branch2, branch3

Оператор IF оценивает выражение и переходит к branch1, если результат меньше
нуля, branch2, если он равен нулю, и branch3, если он больше нуля. Ветви похожи
на описанные в разделе «Ветвление» на с. 150.
Сортировка чисел является простой операцией. Хорошо бы применить эту же
методологию к сортировке других объектов. На рис. 7.10 видно, что узел списка
может содержать произвольные данные. То же самое верно для древовидных
и других структур данных.

268   Глава 7. Организация данных
В версии UNIX III была представлена библиотечная функция qsort, которая
реализовала классический алгоритм быстрой сортировки. Интересная особенность qsort заключалась в том, что функция знала, как сортировать объекты, но
не умела их сравнивать. Из-за этого она использовала указатели на функции языка C; при вызове qsort со списком объектов для сортировки также вызывалась
функция сравнения, которая возвращала 0 для значений «меньше»,
«равно» или «больше», как в арифметическом операторе FORTRAN IF. Этот
подход позволил применять qsort произвольно. Например, если узел содержал
как имя, так и возраст, дополнительная функция могла сравнивать элементы сначала по возрасту, а затем по имени. Таким образом, qsort выдавала результаты,
упорядоченные по возрасту, а затем по имени. Данный подход работал хорошо
и был скопирован во многих других системах.
С учетом этого была разработана стандартная функция сравнения строк библиотеки C strcmp. Она возвращает значение меньше, равное или больше нуля. Этот
подход также был взят за основу в других системах.
Версия strcmp, изначально предназначенная для ASCII, просто проходила по
строкам, выявляя отличия одного символа от другого. Она продолжала работать,
если значение было равно нулю, и возвращала 0, если был достигнут конец строк.
В противном случае возвращался результат вычитания.
Все это эффективно, если данные сортируются для распределения в дереве,
и неэффективно, если данные сортируются для размещения в алфавитном
порядке. Этот подход работал во времена ASCII. Из табл. 1.10 видно, что числовой и алфавитный порядок одинаковы. Но метод перестает работать при
поддержке других локалей. Побочным эффектом поддержки других языков,
проявившимся на более поздней стадии, является то, что только символы
ASCII находятся в сопоставимом порядке сортировки или, иначе, в порядке,
отражающем специ­фику языка.
Например, какое значение присвоить немецкой букве β — «острой» букве S
(Eszett или scharfes S)? Ее значение в Юникоде равно 0x00DF. Из-за этого слово
Straße будет расположено после слова Strasse в случае сравнения строк. Но на
самом деле это разные представления одного и того же слова. β эквивалентно
ss. Сравнение строк, учитывающее языковой стандарт, воспринимало бы эти
слова как равные.

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

Создание хешей   269
Общая концепция заключается в применении некоторой хеш-функции к ключам
поиска, которые путем этой хеш-функции равномерно распределяются по памяти. Если хеш-функцию легко вычислить и она превращает ключ в последовательность символов в уникальном месте в памяти, то это обеспечивает быстрый
поиск ключа. Однако следует учитывать некоторые практические реалии.
Каждый «пласт» представляет хранилище для объекта, связанного с ключом.
Хеш-функция должна выдавать значения, которые помещаются в памяти.
Также она не должна распределять объекты по слишком большому объему
памяти. В противном случае производительность пострадает из-за использования чрезмерного объема памяти и отсутствия места ссылки. Из-за отсутствия предварительных данных о ключах невозможно создать идеальную
хеш-функцию.
Один из способов стабилизировать хранилище — использовать хеш-функцию,
которая сопоставляет ключи и определенные индексы массива. Массив называется хеш-таблицей, пример которой представлен на рис. 7.40. Элементы массива
называются сегментами.
Ключ поиска

Хеш-функция

Индекс

Хеш-таблица

...

Рис. 7.40. Хеширование
Что делает хеш-функцию хорошей? Она должна легко вычисляться и равномерно распределять ключи по сегментам. Простая хеш-функция, приемлемо
работающая для текста, всего лишь суммирует значения символов. Этого недостаточно, потому что в результате суммирования может получиться индекс,
выходящий за пределы хеш-таблицы. Но эту проблему легко решить, сделав
индекс суммой по модулю размера хеш-таблицы. Посмотрим, как это происходит. Возьмем размер таблицы, равный 11; лучше использовать простые числа
для размеров таблиц, потому что часть суммы, кратная 2, попадает в разные
сегменты, улучшая распределение результатов.
Допустим, у нас есть приложение для песен, сыгранных на концертах нашей
любимой группы. Возможно, в нем хранится дата последнего воспроизведения.
Мы просто будем использовать первое слово в названии каждой песни.
Как видно на рис. 7.41, мы начинаем с «Hell in a bucket» — в данном случае
используем сегмент 4. Далее идет «Touch» в сегменте 9, за которым следует
«Scar­let» в сегменте 3. Но когда мы добираемся до «Alligator», у нас возникает
проблема, потому что значение хеш-функции такое же, как и для «Scarlet». Это
называется коллизией.

270   Глава 7. Организация данных
1: Hell in a Bucket
0
1
2
3
4
5
6
7
8
9
10

2: Touch of Grey

Hell

3: Scarlet Begonias

4: Alligator

Hell

Scarlet
Hell

Alligator
Scarlet
Hell

Touch

Touch

Touch

Рис. 7.41. Коллизия хеш-функции
Решим эту проблему, заменив сегменты хеш-цепочками, которые в простейшей
форме представляют односвязные списки, как показано на рис. 7.42.
Хеш-таблица
0
1
2
3

Alligator

4

Hell

Scarlet

5
6
7
8
9

Touch

10

Рис. 7.42. Цепочки хешей
Вариантов управления хеш-цепочкой множество. Можно просто вставить коллизии в начало цепочки, как показано на рис. 7.42. Это быстро. Но поиск может
замедляться по мере удлинения цепочек, поэтому можно выполнить сортировку
вставками. Сортировка занимает больше времени, но исключает прохождение
цепочки до конца, если искомый элемент найден. Существует также множество
методов обработки коллизий — например, устранение цепочек хешей и использование некоторого алгоритма для поиска пустого места в таблице.
Не зная заранее ожидаемого количества символов, трудно подобрать оптимальный размер хеш-таблицы. Можно отслеживать длину цепочки и увеличивать
хеш-таблицу, если цепочки становятся слишком длинными. Эта операция может
быть дорогостоящей, но окупится по причине нечастого выполнения.

Эффективность и производительность   271
Существует множество вариантов хеш-функций. Святой грааль хеш-функций —
это идеальный хеш, который сопоставляет каждый ключ с уникальным сегментом. Практически невозможно создать идеальную хеш-функцию, если все ключи
не известны заранее, но математики придумали функции гораздо лучше тех, что
используются в этом примере.

Эффективность и производительность
Создание эффективных алгоритмов поиска стоило множества усилий. Большая
часть этой работы была проделана в эпоху, когда компьютеры были дорогими.
Производительность и эффективность были взаимосвязаны.
Стоимость электроники упала настолько, что практически к любой покупке вам
добавят горсть каких-нибудь светодиодов в виде бонуса. Производительность
и эффективность больше не имеют тесной взаимосвязи; иногда более высокой
производительности можно добиться за счет использования менее эффективных
алгоритмов на большем количестве процессоров, а не за счет использования
более эффективного алгоритма при меньшем количестве процессоров.
Такое разделение эффективности и производительности используется при сегментировании базы данных, также называемом горизонтальным разделением. Сегментирование включает в себя разделение базы данных на несколько сегментов,
каждый из которых находится на отдельном компьютере, как показано на рис. 7.43.
Сегмент 1
Хранилище

Хранилище

Процессор 1

Сегмент 2
Процессор 2

Контроллер

Интерфейс

...
Сегмент n
Хранилище

Процессор n

Рис. 7.43. Сегментирование базы данных
Операции с базой данных, запрошенные через интерфейс, отправляются всем
сегментам, а результаты собираются контроллером. Этот метод повышает
производительность, ведь операции распределяются между несколькими обработчиками.
Один из вариантов сегментирования называется MapReduce. По сути, он позволяет предоставлять код контроллеру для сборки промежуточных результатов.

272   Глава 7. Организация данных
Благодаря этому при выполнении таких операций, как «подсчитать количество
учащихся во всех математических классах», не нужно запрашивать список учащихся перед подсчетом.
Базы данных — не единственное применение подхода с несколькими обработчиками. Интересен пример DES (стандарта шифрования данных) Фонда
электронных инноваций, созданного в 1998 году; подробнее см. в книге «Cracking
DES» (O’Reilly, 1998). Была сконструирована машина, которая использовала
1856 специальных чипов, каждый из которых подбирал ключи к зашифрованным данным. Любые «интересные» результаты направлялись контролеру для
дальнейшего анализа. Эта машина могла тестировать 90 миллиардов ключей
в секунду.

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

8

Обработка языка

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

Язык ассемблера
Мы рассматривали реализацию программы на машинном языке для вычисления
последовательности Фибоначчи в табл. 4.4 на с. 153. Как вы понимаете, подбирать все битовые комбинации для инструкций довольно неудобно. Программисты привыкли все упрощать, потому они и изобрели новый способ написания
компьютерных программ — язык ассемблера.
С помощью этого языка можно было делать удивительные вещи. Он позволял
программистам использовать мнемоники для инструкций, вместо того чтобы
запоминать все битовые комбинации. В нем появились имена, или ярлыки, адресов. А еще он позволял программистам добавлять комментарии, чтобы другим
людям было проще читать и понимать программу.
Программа под названием ассемблер может читать исходный код на языке
ассемблера и извлекать из него машинный код, заполняя значения ярлыков

274   Глава 8. Обработка языка
(символы) по мере чтения. Это особенно удобно, поскольку таким образом
можно предотвратить глупые ошибки, которые неизбежно появляются при
перемещении кода.
В листинге 8.1 показано, как может выглядеть программа Фибоначчи
(см. табл. 4.4) на гипотетическом ассемблерном языке из главы 4.
Листинг 8.1. Программа на ассемблерном языке для вычисления
последовательности Фибоначчи

again:

first:
second:
next:

load
store
load
store
load
add
store

#0
first
#1
second
first
second
next

load
store
load
store
cmp
ble
bss
bss
bss

second
first
next
second
#200
again
1
1
1

; установить 0 первым числом в последовательности
; установить 1 вторым числом в последовательности
; сложить первое и второе числа, чтобы получить
; следующее число в последовательности
; какие-нибудь действия над числом
; установить второе число на место первого
; установить следующее число на место второго
;
;
;
;
;

результат получен?
нет, повторить
где хранится первое число
где хранится второе число
где хранится следующее число

Псевдоинструкция bss (что означает блок, начинающийся с символа, block started
by symbol) резервирует участок памяти — в нашем случае один адрес, — не
помещая ничего внутрь. Псевдоинструкции не соотносятся с инструкциями
машинного языка напрямую, а представляют собой инструкции для ассемблера.
Видим, что с языком ассемблера работать проще, чем с машинным, — но все еще
утомительно.
В далекие времена нужные для работы программы приходилось загружать в компьютер самостоятельно. К моменту появления первого компьютера ассемблер
еще не был изобретен, поэтому программисты были вынуждены создавать его
буквально на ходу, собирая информацию по крупицам. Первый ассемблер был
довольно примитивным, но он хотя бы работал, и его можно было улучшить —
было с чем работать.
Термин самозагрузка со временем прижился, и сейчас его часто сокращают
до просто загрузки. Загрузка компьютера чаще всего подразумевает запуск
маленькой программы, которая запускает программу побольше, а та, в свою
очередь, запускает новую, еще большую. В первые компьютеры программу загрузки приходилось вводить вручную, используя переключатели и датчики на
передней панели.

Языки высокого уровня   275

Языки высокого уровня
Ассемблер стал большим подспорьем для программистов. Однако выполнение
простых задач все еще требовало значительных усилий. Нужен был способ использовать меньше слов для описания более сложных задач. В книге «Мифический человеко-месяц, или Как создаются программные системы» Фредерика
Брукса (Fred Brooks) (Addison-Wesley), написанной в 1975 году, утверждается,
что в среднем программист может написать от 3 до 10 документированных и отлаженных строк кода в день. Если бы одной строкой можно было описать более
сложные задачи, мы делали бы свою работу гораздо быстрее.
Пришло время представить высокоуровневые языки, которые оперируют более
высокими уровнями абстракции, чем ассемблер. Исходный код в высокоуровневых языках пропускается через программу под названием компилятор,
которая переводит, или компилирует, его в машинный язык, известный как
объектный код.
Сегодня насчитываются тысячи высокоуровневых языков, предназначенных
для решения как общих, так и специфических задач. Один из первых высокоуровневых языков известен под названием FORTRAN, что расшифровывается
как «транслятор формул» (formula translator). Его можно использовать, чтобы
легко написать программу, которая вычисляет значения выражений вроде
y = m × x + b. В листинге 8.2 показано, как программа вычисления последовательности Фибоначчи будет выглядеть на языке FORTRAN.
Листинг 8.2. Программа для вычисления последовательности Фибоначчи
на языке FORTRAN
C
C
5
C
C
C
C

УСТАНОВИТЬ ДВА ПЕРВЫХ ЧИСЛА В ПОСЛЕДОВАТЕЛЬНОСТИ КАК I И J
I=0
J=1
ПОЛУЧИТЬ СЛЕДУЮЩЕЕ ЧИСЛО В ПОСЛЕДОВАТЕЛЬНОСТИ
K=I+J
ДЕЙСТВИЯ С ЧИСЛОМ
СДВИНУТЬ ЧИСЛА I И J В ПОСЛЕДОВАТЕЛЬНОСТИ
I=J
J=K
ПОВТОРИТЬ, ЕСЛИ ПОСЛЕДНЕЕ ЧИСЛО МЕНЬШЕ 200
IF (J .LT. 200) GOTO 5
ГОТОВО

Выглядит проще, чем ассемблер, не правда ли? Обратите внимание, что строки, начинающиеся с буквы C, обозначают комментарии. Здесь используются
и метки, но теперь они должны обозначать численные значения. Также обратите
внимание, что не требуется явно определять нужное количество памяти — она
магическим образом резервируется при объявлении переменных, таких как I
и J. Благодаря FORTRAN получилось кое-что интересное (или ужасное, тут уж
как посмотреть) — и примерно этим мы и занимаемся по сей день. Любое имя

276   Глава 8. Обработка языка
переменной, начинающееся с букв I, J, K, L, M или N, обозначало целое число —
таким же образом описывались доказательства в математике. Переменные,
начинающиеся с любой другой буквы, обозначали числа с плавающей точкой,
или вещественные (REAL), как они назывались в языке FORTRAN. Поколения
программистов, работавших с FORTRAN, все еще используют i, j, k, l, m и n или
их заглавные эквиваленты для обозначения целочисленных переменных.
Громоздкий язык FORTRAN в свое время использовался для таких же громоздких компьютеров. С появлением меньших и не таких дорогих устройств
(для них можно было выделить комнату поменьше) пришли и новые языки
программирования. Многие из этих языков, например BASIC (сокращение от
Beginner’s All-purpose Symbolic Instruction Code — универсальный код символических инструкций для начинающих), представляли собой вариации на тему
FORTRAN. У всех этих языков была одна и та же проблема — с увеличением
сложности программы становилось все труднее разобраться в запутанной
сети номеров строк и инструкций GOTO. Изначально стройная система нумерации меток распадалась при малейшей попытке внести изменения. Многие
программисты использовали метки с шагом 10 или 100, чтобы впоследствии
промежутки между ними можно было заполнить, но и это не всегда помогало.

Структурное программирование
Языки вроде FORTRAN или BASIC называются неструктурированными,
потому что они не имеют заданной структуры для организации меток и инструкций GOTO. В реальности нельзя построить дом, просто бросив на землю
кучу досок, чего не скажешь о языке FORTRAN. Я имею в виду оригинальную
версию FORTRAN — в результате эволюции язык приобрел некоторые черты
структурного программирования, и он до сих пор остается одним из самых востребованных языков программирования для научных вычислений.
Структурные языки программирования были разработаны, чтобы избавиться
от проблемы спагетти-кода — ужасного нагромождения инструкций GOTO.
Некоторые из них зашли слишком далеко — например, в языке Pascal вы не
встретите ничего похожего на GOTO, из-за чего его можно использовать разве
что для обучения элементарному структурному программированию. Если
честно, то он и был создан именно для этого. Язык С, преемник созданного
Кеном Томпсоном (Ken Thompson) языка B, был разработан Деннисом Ритчи
(Dennis Ritchie) в Bell Telephone Laboratories. Сегодня этот прагматичный язык
остается одним из самых популярных в мире, и многие из более новых языков
программирования — в том числе C++, Java, PHP, Python и JavaScript — используют приемы С.
В листинге 8.3 показан вариант программы Фибоначчи на языке JavaScript.
Обратите внимание, что в этом фрагменте кода отсутствует явное ветвление.

Лексический анализ   277
Листинг 8.3. Программа на языке JavaScript для вычисления последовательности
Фибоначчи
var first; // первое число
var second; // второе число
var next;
// следующее число в последовательности
first = 0;
second = 1;
while ((next = first + second) < 200) {
// действия с числом
first = second;
second = next;
}

Операторы внутри фигурных скобок {} выполняются до тех пор, пока условие
while в скобках истинно. Выполнение продолжается после }, когда это условие
становится ложным. Поток управления стал чище, что делает программу более
понятной.

Лексический анализ
Разберемся, что включает в себя понятие «обработка языка». Начать стоит
с лексического анализа — перевода символов (букв) в лексемы (слова).
В упрощенном виде лексический анализ можно представить как разделение
языка на два типа лексем: слова и разделители. Например, в соответствии
с описанными выше правилами выражение лекс лютор1 (автор всех злодейских
языков программирования) состоит из двух лексем-слов (лекс и лютор) и одной
лексемы-разделителя (пробела). На рис. 8.1 показан простой алгоритм разделения вводимых символов по типам лексем.
Просто получить лексемы недостаточно — нужно классифицировать их, поскольку в реальной жизни в языках используется множество типов лексем (имена, числа, операторы). В языках, как и в математике, есть операторы и операнды,
а операнды, в свою очередь, могут быть переменными и постоянными (числами).
Из-за допущений во многих языках задача еще больше усложняется — например,
разделители могут быть не указаны явно, как в примере с A+B (подразумевается
A + B — обратите внимание на пробелы). Эти две формы записи эквивалентны,
но в первом случае мы не видим явного указания разделителей.
На удивление трудно классифицировать численные константы, даже если не
задумываться о разнице между восьмеричными, шестнадцатеричными, целыми числами и числами с плавающей точкой. Изобразим схематично несколько
1

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

278   Глава 8. Обработка языка
правильных вариантов записи числа с плавающей точкой. Оно может быть
задано как 1., .1, 1.2, +1.2, –.1, 1e5, 1e+5, 1e–5 и 1.2E5, что показано на рис. 8.2.
Начало

Очистить
буфер
лексем

Получить
символ

Нет
Нет

Получена
лексема

Да

Лексема
пуста?

Это
разделитель?

Нет

Поместить
вбуфер
лексем

Рис. 8.1. Простой лексический анализ
экспонента (E или e)
цифра
Начало

1

+–

цифра

цифра

цифра
2

цифра

3

4

5
экспонента
(E или e)

6

цифра

7

цифра

Рис. 8.2. Схематическое изображение числа с плавающей точкой
Начнем с кружка, обозначенного цифрой 1. Знак + или – приводит нас к кружку
под номером 2, а с помощью символа «.» мы перейдем к кружку 4. После кружка 2 мы можем перейти к кружку 4 (если следующий символ — это «.») или
к кружку 3 (если следующий символ — цифра). В кружках под номерами 3 и 4
находятся цифры. Классификация завершится, если для следующего символа не
встретится подходящей стрелки перехода. Например, ввод пробела после любого
кружка означает конец записи. Однако если после кружка 2 не ввести цифру или

Лексический анализ   279
десятичную точку, после кружка 6 — цифру, а после кружка 5 — знак или цифру,
это будет ошибка, потому что мы не получим правильный вариант записи числа
с плавающей точкой. Для упрощения ошибочные пути не включены в схему.
Все это очень напоминает поиск сокровищ по пиратской карте. Переходя по
стрелкам, мы перемещаемся в новые локации. Если вместо допустимого символа ввести, к примеру, букву Я в кружке 1, мы потеряемся и не сможем больше
ориентироваться.
Схему на рис. 8.2 можно рассматривать как спецификацию для записи чисел
с плавающей точкой — можно даже написать программу, реализующую заданный
алгоритм. Есть и другие, более формальные способы написания спецификаций —
например, нормальная форма Бэкуса — Наура.
ФОРМА БЭКУСА — НАУРА
Форма Бэкуса — Наура (БНФ) восходит к работам индийского знатока санскрита
Панини (Panini) (прим. V в. до н. э.). БНФ получила свое название от фамилии
американского программиста Джона Бэкуса (John Backus) (1924–2007) — он
также изобрел язык FORTRAN — и датского программиста Петера Наура (Peter
Naur) (1928–2016). БНФ представляет собой формальный способ спецификации
языков. В этой книге мы не будем углубляться в детали, но о БНФ стоит знать, потому что она используется в документах RFC («запрос на комментарии», request
for comments), определяющих, помимо прочего, интернет-протоколы. Ниже представлена БНФ для числа с плавающей точкой:



::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
::= |







::=
::=
::=
::=
::=




::= | "." | "." | "."
::=

"e" | "E"
"+" | "-"
| ""

| ""

Выражения слева от знака «::=» можно заменить на выражения справа от него.
Знак «|» обозначает выбор между выражениями, а все, что указано в кавычках,
обозначает литералы — то есть то, что должно быть записано в буквальном виде.

Конечные автоматы
Учитывая сложность записи чисел, можно представить, что для извлечения языковых лексем из входных данных потребуется огромное множество специального
кода. Однако, судя по рис. 8.2, существует более простой подход — создание

280   Глава 8. Обработка языка
конечного автомата, содержащего набор состояний и причин для перехода из
одного состояния в другое, — именно это и изображено на рис. 8.2. Если собрать
все данные воедино, получим табл. 8.1.
Таблица 8.1. Таблица состояний для чисел с плавающей точкой
Входные
данные

Состояние
1

2

3

4

5

6

7

0

3

3

3

4

7

7

7

1

3

3

3

4

7

7

7

2

3

3

3

4

7

7

7

3

3

3

3

4

7

7

7

4

3

3

3

4

7

7

7

5

3

3

3

4

7

7

7

6

3

3

3

4

7

7

7

7

3

3

3

4

7

7

7

8

3

3

3

4

7

7

7

9

3

3

3

4

7

7

7

e

Ошибка

Ошибка

5

5

Ошибка

Ошибка

Конец
ввода

E

Ошибка

Ошибка

5

5

Ошибка

Ошибка

Конец
ввода

+

2

Ошибка

Конец
ввода

Конец
ввода

6

Ошибка

Конец
ввода



2

Ошибка

Конец
ввода

Конец
ввода

6

Ошибка

Конец
ввода

.

4

4

4

Ошибка

Ошибка

Конец
ввода

Другое

Ошибка

Ошибка

Конец
ввода

Ошибка

Ошибка

Конец
ввода

Конец
ввода

В соответствии с таблицей из состояния 1 при вводе цифры мы перейдем в состояние 3, ввод e или E приведет к состоянию 5, ввод знака «+» или «–» — к состоянию 2, ввод знака «.» — к состоянию 4, а ввод любого другого значения —
к состоянию ошибки.
Благодаря конечным автоматам можно классифицировать входные данные при
помощи простого фрагмента кода, как показано в листинге 8.4. Для этого заменим
состояния ошибка и конец ввода из табл. 8.1 на 0 и –1 соответственно. Для любых
значений, не представленных в таблице, достаточно вывести строку другое.

Лексический анализ   281
Листинг 8.4. Применение конечного автомата
state = 1;
while (state > 0)
state = state_table[state][next_character];

Этот же подход можно использовать и для любых других типов лексем. Для
этого вместо единственного значения конец ввода вводятся значения всех необходимых типов лексем.

Регулярные выражения
В случае со сложными языками построить без ошибок таблицу наподобие 8.1 из
схемы на рис. 8.2 очень непросто, поэтому были придуманы языки для специ­
фикации языков. В 1956 году американский математик Стивен Коул Клини
(Stephen Cole Kleene) (1909–1994) представил математическое основание этого
подхода. А в 1968 году Кен Томпсон впервые использовал его в компьютерном
текстовом редакторе. В 1974-м он же создал команду grep для UNIX (сокращение от globally search a regular expression and print — глобальный поиск и вывод
регулярного выражения). Так появился ныне повсеместно используемый термин
регулярное выражение. Регулярные выражения представляют собой самостоятельные языки — на сегодняшний день в мире насчитывается несколько языков регулярных выражений, несовместимых между собой. Регулярные выражения легли
в основу механизма поиска по шаблону. На рис. 8.3 показан пример регулярного
выражения, выполняющего поиск по шаблону для числа с плавающей точкой.
Группа

Или

Группа

[+-]?([0-9]*\.?[0-9]+)|([0-9]+\.?[0-9]*)[Ee][+-]?[0-9]+
Одно или несколько предшествующих чисел от 0 до 9
Числа от 0 до 9
Предшествующая десятичная точка или ее отсутствие
Десятичная точка
Экранирует следующий символ
Предшествующие числа от 0 до 9 или их отсутствие
Числа от 0 до 9
Предшествующий знак числа или его отсутствие
+ или –

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

282   Глава 8. Обработкаязыка
какая-то часть выражения завершается знаком «?», это означает, что данная часть
либо отсутствует вообще, либо присутствует только один раз. Знак «*» означает, что часть выражения может повторяться от нуля до нескольких раз, а знак
«+» означает один или несколько повторов части выражения. Набор символов
в квадратных скобках представляет собой шаблон, проверяющий, встречается ли
в выражении один из этих символов. К примеру, шаблону [abc] соответствуют
символы a, b или c. Знак «.» означает любой символ, а для обозначения самого
знака «.» в выражении используется экранирование с помощью обратной косой
черты (\). Знак «|» означает символ либо справа, либо слева от него. Скобки(),
как и в математике, используются для группировки частей выражения.
Читаем выражение слева направо — оно начинается с необязательного знака
«плюс» или «минус». За ним следуют цифры или десятичная точка, а за ними —
одна или несколько цифр (в случае с числами вроде 1.2 или .2). Или же можно
увидеть одну или несколько цифр, необязательную десятичную точку и, что
также необязательно, одну или несколько цифр после нее (как в случае с числами
1 и 1.). За всем этим может следовать экспонента, обозначаемая знаком E или e,
а за ней — необязательный знак «плюс» или «минус» и одна или несколько цифр.
Не так уж и страшно, правда?
Язык регулярных выражений мог быть еще более полезным, если бы автоматически генерировал таблицы состояний помимо обработки входных данных и разделения их на лексемы. И, между прочим, он на самом деле умеет
это делать благодаря исследованию Bell Telephone Laboratories. В 1975 году
американский физик Майк Леск (Mike Lesk) со стажером Эриком Шмидтом
(Eric Schmidt), ныне председателем правления Alphabet, родительской компании Google, написали программу под названием lex — сокращение от lexical
analyzer — лексический анализатор. Как поют Beatles в песне Penny Lane: «It’s
a Kleene machine»1. Позже в проекте GNU была разработана версия программы
с открытым исходным кодом — flex. Оба этих инструмента делают как раз то,
что нам нужно, — запускают программу на основе таблицы состояний, которая
выполняет предоставленные пользователем фрагменты, если входные данные
совпадают с шаблоном регулярного выражения. Например, фрагмент программы
lex в листинге 8.5 выводит символы ah, если во входных данных встречаются
строки ar или er, а символы er — если введенное слово заканчивается на a.
Листинг 8.5. Фрагмент бостонской программы lex
[ae]r
printf("ah");
a/[ .,;!?] printf("er");

Косая черта «/» во втором шаблоне означает «проверять совпадения по шаблону
слева от черты, если перед этим встретилась часть выражения справа от черты».
Если совпадений не найдено, выводится исходный текст. Данную программу
1

Отсылка к теореме Клини (Kleene’s theorem). — Примеч. науч. ред.

От слов к предложениям   283
можно использовать, чтобы преобразовать текст на стандартном американском
английском в текст с бостонским акцентом. К примеру, если ввести фразу Park
the car in Harvard yard and sit on the sofa, в результате получим Pahk the cah in
Hahvahd yahd and sit on the sofer.
lex в два счета справляется с классификацией лексем. В листинге 8.6 приведен
фрагмент программы lex, которая определяет все ранее рассмотренные форма-

ты чисел, имена переменных и некоторые операторы. Вместо простого вывода
результата программа возвращает заранее определенные значения для каждого
типа лексем. Обратите внимание: некоторые символы имеют специальное значение в lex, поэтому их необходимо экранировать с помощью обратной косой
черты. В этом случае программа распознает их как литералы.
Листинг 8.6. Классификация лексем при помощи lex
0[0-7]*
[+-]?[0-9]+
[+-]?(([0-9]*\.?[0-9]+)|([0-9]+\.?[0-9]*))([Ee][+-]?[0-9]+)?
0x[0-9a-fA-F]+
[A-Za-z][A-Za-z0-9]*
\+
\*
\/
=

return
return
return
return
return
return
return
return
return
return

(INTEGER);
(INTEGER);
(FLOAT);
(INTEGER);
(VARIABLE);
(PLUS);
(MINUS);
(TIMES);
(DIVIDE);
(EQUALS);

В листинге не показано, как именно lex получает действительные значения
лексем. Обнаружив число, мы должны узнать его значение. То же с именем
переменной — обнаружив имя переменной, мы должны знать ее значение.
Учтите, что lex подходит не для всех языков. Программист Стивен С. Джонсон
(Stephen C. Johnson) (мы еще встретим его в этой книге) объяснил, что lex может
использоваться для разработки довольно сложных лексических анализаторов,
но для некоторых языков (например, FORTRAN), не подходящих под известные
теоретические стандарты, лексический анализатор придется создавать вручную.

От слов к предложениям
Выше мы рассмотрели, как последовательности символов превращаются в отдельные слова. Однако язык этим не ограничивается — пришло время научиться
соединять слова в предложения в соответствии с правилами грамматики.
Создадим простой калькулятор с четырьмя функциями, взяв за основу лексемы
из листинга 8.6. Калькулятор должен считать выражения вроде 1 + 2 корректными, а 1 + + + 2 — нет. Это напоминает уже известный нам поиск по шаблону,
не правда ли? И тогда велика вероятность, что кто-то из программистов уже
задумывался над подобной задачей.

284   Глава 8. Обработка языка
Этот кто-то — Стивен С. Джонсон. Он, что неудивительно, также работал в Bell
Labs. Стивен создал программу yacc (сокращение от yet another compiler compiler,
«еще один компилятор компилятора») в начале 1970-х. Название недвусмысленно намекает на то, что тогда над похожими задачами уже работало немало
программистов. yacc до сих пор используется — версия с открытым исходным
кодом под названием bison доступна в рамках проекта GNU. yacc и bison, подобно lex, генерируют таблицы состояний и код для работы с ними.
Программа yacc генерирует восходящий синтаксический анализатор (или
анализатор типа «сдвиг-свертка») при помощи стека (см. раздел «Стеки»).
В данном контексте сдвиг означает перемещение лексемы по стеку, а свертка — замену найденного по шаблону набора лексем из стека соответствующей
лексемой. В листинге 8.7 представлен пример БНФ для калькулятора — он
использует значения лексем, полученные в результате работы программы lex
из листинга 8.6.
Листинг 8.7. Простая БНФ для калькулятора









::= PLUS | MINUS | TIMES | DIVIDE
::= INTEGER | FLOAT | VARIABLE
::= | PLUS
| MINUS
| TIMES
| DIVIDE
::= EQUALS
::= |
::= "" |
::=

Гораздо проще понять алгоритм «сдвиг-свертка», увидев его в действии. На
рис. 8.4 показано, что произойдет, если ввести в калькулятор выражение 4 + 5 – 3.
Из врезки «Различные представления уравнений» на с. 169 вы можете помнить,
что для обработки инфиксного представления требуется большая глубина стека,
чем для постфиксного. Дело в том, что в случае с инфиксным представлением
нужно сдвинуть больше лексем (например, скобок) перед применением свертки.
4

+

5

4

+

9



3

9



4

Начало

Сдвиг

Сдвиг

Сдвиг

6

9

Свертка

Сдвиг

Сдвиг

Свертка

Рис. 8.4. Анализатор типа «сдвиг-свертка» в действии
В листинге 8.8 показан код калькулятора, реализованного с помощью программы yacc . Обратите внимание на его сходство с БНФ. Это учебный

Клуб «Язык дня»   285
пример — полностью рабочий алгоритм включал бы слишком много уточняющих деталей.
Листинг 8.8. Часть yacc-кода для простого калькулятора
calculator : statements
;
statements : /* empty */
| statement statements
;
operand

: INTEGER
| FLOAT
| VARIABLE
;

expression :
|
|
|
|
;

expression
expression
expression
expression
operand

PLUS operand
MINUS operand
TIMES operand
DIVIDE operand

assignment : VARIABLE EQUALS expression
;
statement

: expression
| assignment
;

Клуб «Язык дня»
Раньше языки были сложными. В 1977 году два программиста, канадец Альфред
Ахо (Alfred Aho) и американец Джеффри Ульман (Jeffrey Ullman) из Bell Labs,
опубликовали книгу «Компиляторы. Принципы, технологии и инструментарий». Это была одна из первых книг, выпущенных с использованием системы
компьютерной верстки и типографского языка troff. Если вкратце, то основной
посыл книги можно выразить так: «Языки настолько сложны, что вам придется
зарыться в матчасть, выучить теорию и т. д.». Второе издание (1986), созданное
совместно с индийским программистом Рави Сети (Ravi Sethi), транслировало
совершенно другую идею. Ее можно описать как «мы разобрались в языках
и научим этому вас». И это была сущая правда.
Новое издание открыло миру программы lex и yacc. Оказалось, что уже существует множество языков для самых разных задач — и не только языков программирования. Мне особенно полюбился небольшой язык chem (автор — канадский
программист Брайан Керниган (Brian Kernighan) из Bell Labs), который умеет
выводить химические структурные формулы на основе входных данных вроде

286   Глава 8. Обработка языка
C double bond O. Схемы в данной книге, между прочим, были созданы с помощью
изобразительного языка pic, также придуманного Брайаном Керниганом.

Создавать новые языки весело. Как итог, люди начали выпускать новые языки
программирования, не имея четкого представления об их истории, что привело
к повторению прежних ошибок. Например, способ обработки пробелов (промежутков между словами) в языке Ruby вносит ошибку, давно исправленную
в ранних версиях языка С. (Помните, что один из классических подходов
к ошибкам — объявить, что так и было задумано.)
В результате сегодня доступно огромное множество языков программирования.
Некоторые из них не содержат ничего принципиально нового и служат лишь
воплощением вкуса их создателей. Стоит обратить внимание разве что на предметно-ориентированные языки программирования, в том числе небольшие языки
вроде pic и chem, предназначенные для решения локальных задач. Американский
программист Джон Бентли (Jon Bentley) выпустил прекрасную колонку под
названием Programming Pearls о небольших языках в журнале Communications
of the ACM еще в 1986 году. В 1999 году эти статьи были собраны и изданы под
обложкой одноименной книги (Addison-Wesley).

Деревья синтаксического анализа
Ранее я уже упоминал о компиляции языков высокого уровня. Теперь вы узнаете,
что высокоуровневые языки можно не только компилировать, но и интерпретировать. Выбор действия зависит, скорее, не от проектирования языка, а от
его реализации.
Языки компилируются в машинный код, как показано в табл. 4.4 на с. 153.
Компилятор принимает исходный код и переводит его на машинный язык для
определенного устройства. Большинство компиляторов могут компилировать
одну и ту же программу для разных устройств. После компиляции программа
готова к запуску.
В свою очередь, интерпретировав язык, вы не получите машинный код для
реального устройства (под «реальными устройствами» я имею в виду аппаратное обеспечение). Интерпретированные языки подходят для запуска на виртуальных машинах — устройствах, созданных на основе ПО. Они могут иметь
собственный машинный язык, но его нельзя назвать набором компьютерных
инструкций, реализованных в аппаратном виде. Обратите внимание, что термин виртуальная машина в последнее время сильно перегружен — в данном
контексте я говорю об абстрактном вычислительном устройстве. Некоторые
интерпретированные языки могут быть запущены интерпретаторами напрямую, а другие компилируются в так называемые промежуточные языки для
дальнейшей интерпретации.

Деревья синтаксического анализа   287
В целом скомпилированный код выполняется быстрее, потому что после компиляции он представляет собой машинный язык. Можно сравнить это с переводом книги — после перевода на определенный язык она становится доступной
любому, кто его понимает. Интерпретированный язык — это нечто эфемерное.
Представьте, что вы читаете книгу вслух, одновременно переводя ее на язык
слушателя. Чтобы прочитать эту книгу кому-то другому на другом языке, придется заново ее переводить. Несмотря на сложности, интерпретированные языки
позволяют выполнять задачи, которые трудно решить с помощью аппаратного
обеспечения. Компьютеры работают достаточно быстро, поэтому во многих
случаях можно смириться с неизбежными задержками при использовании
интерпретаторов.
На рис. 8.4 изображен калькулятор, работающий напрямую с входными данными. Для калькулятора этого может быть достаточно, но в данном примере мы
опускаем шаг, важный для работы компилятора или интерпретатора. Я говорю
о создании дерева синтаксического анализа с использованием грамматики калькулятора — НАГ-структуры данных (НАГ — направленный ациклический граф).
Подобное дерево строится из узлов, как показано на рис. 8.5.
узел

объединение

код

число с плавающей точкой .f

лист 0

целое число

.i

лист .

указатель на узел

.n

массив символов

.s

листn

. .

Рис. 8.5. Схема узла дерева синтаксического анализа
Каждый узел включает в себя код, по которому определяется тип данного узла.
Существует также массив листьев; интерпретация каждого листа определяется
его кодом. Листья представляют собой объединения — они могут включать в себя
более одного типа данных. Для именования членов мы используем синтаксис языка С — например, .i указывает на интерпретацию листа как целого числа (integer).
Предположим, что существует функция makenode, задача которой — создавать
новые узлы. Она принимает количество листьев (leaf) в качестве первого аргумента и код (code) — в качестве второго, а также последовательно переданные
значения всех листьев.
Добавим немного кода в листинг 8.8, все еще опуская некоторые незначительные
подробности. Также для простоты ограничимся обработкой целых чисел. Новые
строки кода отвечают за работу программы при проверке грамматических правил. В yacc значения всех элементов с правой стороны обозначаются как $1, $2
и т. д., а $$ представляет собой результат применения правила. В листинге 8.9
видим расширенную версию yacc-калькулятора.

288   Глава 8. Обработка языка
Листинг 8.9. Создание дерева синтаксического анализа для простого калькулятора
на основе yacc
calculator
statements
operand
expression

assignment
statement

:
;
:
|
;
:
|
;
:
|
|
|
|
;
:
;
:
|
;

statements

{ do_something_with($1); }

/* empty */
statement statements

{ $$.n = makenode(2, LIST, $1, $2); }

INTEGER
VARIABLE

{ $$ = makenode(1, INTEGER, $1); }
{ $$ = makenode(1, VARIABLE, $1); }

expression
expression
expression
expression
operand

PLUS operand
MINUS operand
TIMES operand
DIVIDE operand

{
{
{
{
{

$$.n
$$.n
$$.n
$$.n
$$ =

= makenode(2,
= makenode(2,
= makenode(2,
= makenode(2,
$1; }

PLUS, $1, $3); }
MINUS, $1, $3); }
TIMES, $1, $3); }
DIVIDE, $1, $3); }

VARIABLE EQUALS expression

{ $$.n = makenode(2, EQUALS, $1, $3); }

expression
assignment

{ $$ = $1; }
{ $$ = $1; }

В данном примере все простые правила возвращают соответствующие им значения. Более сложные правила вроде statements, expression и assignment создают
узел, добавляют к нему дочерние элементы и возвращают результат. На рис. 8.6
представлен пример обработки некоторых входных данных.
Входные данные:

1 + 2 * 3
foo = 5
6 / foo

СПИСОК

ПЛЮС

ЦЕЛОЕ ЧИСЛО

СПИСОК

УМНОЖИТЬ

СПИСОК

РАВНО

1

ЦЕЛОЕ ЧИСЛО

ЦЕЛОЕ ЧИСЛО

2

3

"foo"

ЦЕЛОЕ ЧИСЛО

РАЗДЕЛИТЬ

5

ЦЕЛОЕ ЧИСЛО

ПЕРЕМЕННАЯ

6

foo

Рис. 8.6. Дерево синтаксического анализа для простого калькулятора

Интерпретаторы   289
Как видите, код генерирует дерево. В самом его верху используется правило
calculator (калькулятор) для создания связанного списка выражений из узлов
дерева. Все оставшееся дерево состоит из узлов statement (выражение), которые
содержат operator и operands (оператор и операнды).

Интерпретаторы
В листинге 8.9 вы могли заметить загадочный вызов функции do_something_with,
куда передается корневой узел дерева синтаксического анализа. Данная функция заставляет интерпретатор «запустить» дерево, начиная с обхода связного
списка, как показано на рис. 8.7.
узел

корень

узел ≠ NULL?

Нет

Конец

Да
вычислить узел «лист 0 »

узел

узел «лист 1 »

Рис. 8.7. Обход связного списка дерева синтаксического анализа
Далее мы переходим к вычислению значений в дереве при помощи рекурсивного
обхода в глубину. Эта функция изображена на рис. 8.8.
Благодаря тому что каждый узел имеет свой код, легко понять, как действовать.
Однако нам потребуется дополнительная функция для хранения имени переменной (символа) и ее значения в таблице символов и еще одна функция для
поиска значения, связанного с именем переменной. Все это можно реализовать
с помощью хеш-таблиц.
Добавив код обхода списка и вычисления в yacc, мы сможем тут же запустить
дерево синтаксического анализа. Как вариант, дерево можно сохранить в файл,
чтобы прочитать и запустить его позже. Именно так работают некоторые языки
программирования, такие как Java и Python. Во всех отношениях данный код
представляет собой набор инструкций на машинном языке, только предназначенный для устройств на основе ПО, а не для аппаратного обеспечения. На каждой используемой машине должна быть предустановлена программа, которая
может запустить сохраненное дерево синтаксического анализа. Часто один и тот
же исходный код, полученный из интерпретатора, по-разному компилируется
и используется для разных задач.

290   Глава 8. Обработка языка
вычислить
код узла

вернуть

ПЛЮС?

a

вычислить лист 0

b

вычислить лист 1

вернуть a + b

МИНУС?

a

вычислить лист 0

b

вычислить лист 1

вернуть a - b

УМНОЖИТЬ?

a

вычислить лист 0

b

вычислить лист 1

вернуть a × b

РАЗДЕЛИТЬ?

a

вычислить лист0

b

вычислить лист 1

вернуть a ÷ b

РАВНО?

a

вычислить лист 1

записать значение а в результат вычисления листа 0

вернуть лист0

ЦЕЛОЕ ЧИСЛО?

ПЕРЕМЕННАЯ?

вернуть a

a

вернуть a

найти лист 0

Выполнено

Ошибка

Рис. 8.8. Вычисление значений в дереве синтаксического анализа
На рис. 8.9 представлена структура интерпретатора.
Серверная
часть для
устройства 1

Интерфейсная часть
Входные
данные
программы

Лексический
анализ

Синтаксический анализ

Дерево
синтаксического анализа
(на промежуточном языке)

Рис. 8.9. Структура интерпретатора

Серверная
часть для
устройства 2
Серверная
часть для
устройства n

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

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

Интерфейсная часть

Входные
данные
программы

Лексический
анализ

Синтаксический
анализ

Дерево
синтаксического
анализа
(на промежуточном языке)

Генератор
кода для
устройства 1

Код
на языке
ассемблера
для
устройства 1

Ассемблер
устройства 1

Код
на машинном языке
для
устройства 1

Генератор
кода для
устройства 2

Код
на языке
ассемблера
для
устройства 2

Ассемблер
устройства 2

Код
на машинном языке
для
устройства 2

Ассемблер
устройства n

Код
на машинном языке
для
устройства n

Генератор
кода для
устройства n

Код
на языке
ассемблера
для
устройства n

Рис. 8.10. Структура компилятора
Генератор кода создает код на машинном языке для определенного устройства.
Инструменты, представленные в некоторых языках (например, в С), могут создавать код на ассемблере (см. «Язык ассемблера») для заданного устройства.
Затем код на ассемблере пропускается через ассемблер устройства для получения кода на машинном языке.
Генератор кода работает в точности как рассмотренная ранее комбинация обхода дерева синтаксического анализа и вычисления его значений (см. рис. 8.7
и 8.8). Разница в том, что прямоугольники для вычисления значений дерева на
рис. 8.8 заменяются прямоугольниками с генерацией кода на языке ассемблера.
На рис. 8.11 изображен упрощенный вариант генератора кода. Выделенные
полужирным выражения (например, add tmp) — это инструкции на машинном
языке для учебного компьютера из главы 4. Обратите внимание, что в этом компьютере нет инструкций для умножения и деления, но для примера достаточно
представить, что они есть.

292   Глава 8. Обработка языка
вычислить
код узла

ПЛЮС?

вычислить лист 1, store tmp

вычислить лист 0

add tmp

МИНУС?

вычислить лист1, store tmp

вычислить лист 0

sub tmp

УМНОЖИТЬ?

вычислить лист1, store tmp

вычислить лист 0

mul tmp

РАЗДЕЛИТЬ?

вычислить лист 1, store tmp

вычислить лист 0

div tmp

РАВНО?

вычислить лист 1

leaf0: bss 1

store leaf0

Внимание: добавляется в конце кода
ЦЕЛОЕ ЧИСЛО?

load #leaf0

ПЕРЕМЕННАЯ?

load leaf0
store tmp

Ошибка

Выполнено

Рис. 8.11. Создание ассемблера из дерева синтаксического анализа
Применив схему с рис. 8.11 к дереву синтаксического анализа на рис. 8.6, получим программу на языке ассемблера, представленную в листинге 8.10.
Листинг 8.10. Вывод на машинном языке, представленный генератором кода
load
store
load
mul
store
load
add

#3
tmp
#2
tmp
tmp
#1
tmp

;
;
;
;
;
;
;
;

первый элемент списка
взять целое число 3
записать в хранилище
взять целое число 2
перемножить значения узлов поддерева
записать в хранилище
взять целое число 1
сложить его с результатом умножения 2 и 3

Оптимизация   293

tmp:
foo:

store

tmp

load
store
store

#5
foo
tmp

load
store
load
div
store
bss
bss

foo
tmp
#6
tmp
tmp
1
1

;
;
;
;
;
;
;
;
;
;
;
;
;

записать в хранилище
второй элемент списка
взять целое число 5
сохранить в хранилище для переменной foo
записать в хранилище
третий элемент списка
вызвать содержимое переменной foo
записать в хранилище
взять целое число 6
разделить
записать в хранилище
ячейка хранилища для временных переменных
ячейка хранилища для переменной foo

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

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

ПЛЮС

Оптимизировать

ЧИСЛО
7

ЧИСЛО

УМНОЖИТЬ

1

ЧИСЛО

ЧИСЛО

2

3

Рис. 8.12. Оптимизация дерева синтаксического анализа

294   Глава 8. Обработка языка
Предыдущий пример не очень информативен, потому что в учебном калькуляторе не предусмотрено условное ветвление. На самом деле, оптимизаторы умеют
очень многое. Рассмотрим код из листинга 8.11 (пример на языке С).
Листинг 8.11. Присваивание внутри цикла на языке С
for (i = 0; i < 10; i++) {
x = a + b;
result[i] = 4 * i + x * x;
}

В листинге 8.12 показано, как улучшить данный пример при помощи оптимизатора.
Листинг 8.12. Инвариантная оптимизация цикла
x = a + b;
optimizer_created_temporary_variable = x * x;
for (i = 0; i < 10; i++) {
result[i] = 4 * i + optimizer_created_temporary_variable;
}

Код из листинга 8.12 делает то же самое, что и в листинге 8.11, но эффективнее.
Оптимизатор определил, что выражение a + b представляет собой инвариант
цикла (то есть его значение внутри цикла не меняется). Поэтому оптимизатор
вынес его из тела цикла, тем самым избавившись от десятикратного повтора
вычисления. Кроме этого, выяснилось, что результат вычисления выражения
x * x не меняется, и оптимизатор также вынес его из тела цикла.
В листинге 8.13 представлена еще одна возможность под названием снижение
стоимости — процесс замены дорогих операций более дешевыми, в нашем случае
замена умножения сложением.
Листинг 8.13. Пример цикла на языке C со снижением стоимости и инвариантной
оптимизацией
x = a + b;
optimizer_created_temporary_variable = x * x;
optimizer_created_4_times_i = 0;
for (i = 0; i < 10; i++) {
result[i] = optimizer_created_4_times_i + optimizer_created_temporary_
variable;
optimizer_created_4_times_i = optimizer_created_4_times_i + 4;
}

Снижая стоимость операции, можно воспользоваться преимуществами относительной адресации, чтобы более эффективно вычислять result[i]. result[i],
согласно рис. 7.2, — это адрес переменной result плюс i, умноженное на размер
элемента массива. Как и в случае с optimizer_created_4_times_i, можно начать
с адреса result и прибавлять к нему размер элемента массива на каждой итерации цикла, вместо того чтобы применять более медленное умножение.

Выводы   295

Осторожнее с аппаратной частью!
Оптимизаторы прекрасны, но они могут вызвать неожиданные проблемы с кодом, управляющим аппаратным обеспечением. На рис. 8.14 показана переменная,
обозначающая аппаратный регистр, который включает лампочку при задании
бита равным 0, как мы видели на рис. 6.1.
Листинг 8.14. Пример кода, который не стоит оптимизировать
void
lights_on()
{
PORTB = 0x01;
return;
}

Выглядит отлично, но что с этим сделает оптимизатор? Он скажет: «Хм, переменная была записана, но ни разу не прочитана, поэтому избавимся от нее».
Похожий пример представлен в коде из листинга 8.15, который включает
лампочку и проверяет, включена она или нет. Оптимизатор может переписать
функцию так, чтобы она просто возвращала 0x01 без записи значения в переменную PORTB.
Листинг 8.15. Еще один пример кода, который не стоит оптимизировать
unsigned int
lights_on()
{
PORTB = 0x01;
return (PORTB);
}

Эти примеры доказывают, что иногда полезно иметь возможность отключить
оптимизацию. Обычно с этой целью ПО делят на программы общего назначения
и программы для аппаратного обеспечения и применяют оптимизацию только
в ПО общего назначения. Кроме того, в некоторых языках программирования
существуют механизмы, блокирующие оптимизацию для заданных фрагментов
кода. Например, в языке С ключевое слово volatile означает, что обозначенная
им переменная недоступна для оптимизации.

Выводы
Дочитав до этого места, вы получили представление о том, как работают компьютеры и как они запускают программы. В этой главе вы увидели, как преобразовать программный код для запуска на устройствах, и узнали, как компилировать
и интерпретировать программы.
В следующей главе вы встретитесь с боссом интерпретаторов — веб-браузером —
и языками, которые он интерпретирует.

9

Веб-браузер

Вы, скорее всего, и не задумывались, что веб-браузер,
которым вы пользуетесь каждый день, представляет
собой виртуальную машину — абстрактный компьютер
с весьма сложным набором инструкций, созданный исключительно на основе программного обеспечения. Другими словами, он также является примером рассмотренных
в предыдущей главе интерпретаторов.
В этой главе вы узнаете, что может делать виртуальная машина. Мы изучим
новый язык ввода и поймем, как он интерпретируется браузером. Важно понимать, что браузер — существо чрезвычайно сложное, поэтому мне не удастся
описать здесь абсолютно все его возможности.
Браузеры интересно изучать по разным причинам. Они представляют собой,
с одной стороны, большие и сложные приложения, а с другой — компьютеры,
созданные на основе программного обеспечения, которые можно запрограммировать самостоятельно. В браузерах есть консоль разработчика, которую можно
использовать для запуска примеров из данной главы. Это поможет в режиме
реального времени разобраться, как работает браузер.
Понимание работы веб-браузеров — это первый шаг к изучению проектирования
систем, понятия не менее важного, чем само программирование (мы рассмотрим
проектирование систем более подробно в главе 15). Повышение интереса к вебразработке превратило браузер в магнит для новых возможностей. Некоторые
новые функции добавились к уже существующим наборам инструкций, другие
же стали дублировать имеющуюся функциональность, что отрицательно сказалось на совместимости. В результате мы получили поддержку сразу нескольких
наборов инструкций в браузерах. В этой главе я постарался показать, что не вижу

Языки разметки   297
особой ценности в новых возможностях браузеров, которые мало отличаются
от уже имеющихся.
Прежде всего наличие множества способов выполнять одни и те же операции
означает, что программистам приходится тратить много времени на то, чтобы
все это выучить. Кроме того, приходится расходовать силы на то, чтобы выбрать
один из существующих подходов к разработке, что также влияет на сложность
программ. Статистика в компьютерной индустрии говорит о прямой зависимости между количеством строк кода и числом ошибок. Браузеры часто ломаются.
Как мы рассмотрим подробнее в главе 13, сложность исходного кода увеличивает
вероятность появления проблем в области безопасности.
Несовместимые способы разработки приводят к ошибкам в программировании.
Представьте, что американец сел за руль в Новой Зеландии: элементы управления автомобилем расположены непривычно для него, потому что в Новой
Зеландии левостороннее движение. Иностранцев, привыкших к правостороннему движению, легко узнать по тому, что только они включают «дворники» при
повороте. Хотелось бы избежать подобного в программировании.
По моему мнению, сам факт, что многие веб-стандарты превратились в живые документы, сигнализирует о наличии проблем. Если этот термин вам незнаком, то
знайте, что я говорю об онлайн-документации, которая постоянно обновляется.
Стандарты существуют именно для того, чтобы поддерживать стабильность и согласованность; живые документы же актуальны только в определенный момент
времени. Писать код в условиях постоянно меняющихся спецификаций сложно.
В этом контексте живые документы удобны только для некоторых своих создателей (потому что документация и связанное с ней ПО могут вообще никогда
не дойти до завершающей стадии) и неудобны для большинства потребителей.

Языки разметки
Если бы книга, которую вы держите в руках, была написана на 15 лет раньше,
эта глава началась бы с введения в HTML (HyperText Markup Language — язык
гипертекстовой разметки). Но я уже намекнул, что браузер очень похож на Большое тихоокеанское мусорное пятно — он постепенно разрастается, потому что
к нему прилипает все больше всякой всячины. В этом контексте невозможно не
сказать о языках разметки — прежде всего о том, что это и для чего они нужны.
Разметка представляет собой систему аннотирования или добавления меток
к тексту — таким образом, чтобы их можно было отличить от самого текста.
Метки похожи на язвительные комментарии красными чернилами, оставленные
учителем в тетради ученика.
Языки разметки появились задолго до первых компьютеров. Они применялись
еще во времена печатных станков — с их помощью авторы и редакторы оставляли

298   Глава 9. Веб-браузер
пометки для наборщиков. Эта же полезная задумка пригодилась, когда появилась возможность автоматизировать набор текста с помощью компьютеров.
Таким образом, сегодняшние языки разметки — лишь реинкарнация старой идеи.
Языков разметки огромное множество. Например, первый вариант этой книги
был написан при помощи языка для набора текстов под названием troff. В листинге 9.1 представлен исходный код этого абзаца.
Листинг 9.1. Предыдущий абзац на языке troff
.PP
Языков разметки огромное множество.
Например, первый вариант этой книги был написан при помощи языка
для набора текстов под названием \fCtroff\fP.
В листинге 9-1 представлен исходный код этого абзаца.

Как видите, практически весь пример кода выше представляет собой обычный
текст, за исключением трех элементов разметки. Элемент \fC указывает языку
troff, что текущий шрифт нужно поместить в стек и заменить его шрифтом C
(Courier). Элемент \fP означает, что нужно извлечь шрифт из стека (см. раздел
«Стеки» на с. 166) и тем самым вернуться к использованию предыдущего шрифта.
Веб-страницы представляют собой обычные текстовые файлы, подобные примеру с языком troff. Для их создания не нужны особенные программы — достаточно текстового редактора. В действительности модные программы для
создания веб-страниц только усложняют процессы, поэтому, чтобы упростить
себе жизнь, лучше работать с текстовыми редакторами.
Как разметить текст, имея под рукой лишь обычные текстовые символы?
Необходимо наделить некоторые символы сверхспособностями — подобно
сверхспособностям Супермена, который в обычной жизни работает простым
журналистом. Например, в языке troff все символы, перед которыми стоят
знаки «.», «'» или «\», имеют сверхспособности.
Компания IBM выпустила собственный язык разметки под названием GML
(сокращение от Generalized Markup Language — обобщенный язык разметки,
хотя на самом деле он назван по первым буквам фамилий создателей — Goldfarb
(Голдфарб), Mosher (Мошер) и Lorie (Лори)). Этот язык используется для
корпоративного издательского инструмента ISIL. Позже эта разработка была
расширена до языка Standard Generalized Markup Language (SGML) (стандартный
обобщенный язык разметки), который был принят Международной организа­
цией по стандартизации в 1980-х. SGML был «обобщен» настолько, что неизвестно, смог ли вообще кто-то создать полную рабочую версию стандарта.
Язык eXtensible Markup Language (XML) (расширяемый язык разметки) представляет собой практическую разновидность SGML. Позже именно он стал
поддерживаться веб-браузерами.

Унифицированные указатели ресурсов   299
И HTML, и XML были созданы на основе SGML. Они используют часть синтаксиса SGML, но не соответствуют стандарту в полной мере.
XHTML — это подправленная версия HTML, работающая в соответствии
с правилами XML.

Унифицированные указатели ресурсов
Первый веб-браузер под названием «Всемирная паутина» (WWW,
WorldWideWeb), изобретенный британским инженером и программистом сэром
Тимом Бернерсом-Ли в 1990 году, работал достаточно просто, что можно понять
по рис. 9.1. Для получения документа с сервера при помощи протокола HTTP
в браузере использовались унифицированные указатели ресурсов (Uniform
Resource Locator (URL)), что мы уже обсуждали в разделе «Всемирная паутина» на с. 207. Сервер посылал документ браузеру, а тот отображал его на экране.
Ранее для создания веб-документов использовался только язык HTML, теперь
же для этой цели создано множество самых разных языков.
URL
Браузер

Сервер

Документ

Рис. 9.1. Взаимодействие веб-браузера с веб-сервером
URL — это текстовые строки, имеющие определенную структуру. Сейчас нам
достаточно рассмотреть три основных элемента URL, изображенных на рис. 9.2.
https://www.nostarch.com/catalog/general-computing
Протокол

Хост

Путь

Рис. 9.2. Анатомия URL
Протокол определяет механизм коммуникации — например, протокол https означает «протокол защищенной передачи гипертекста» (сокращение от HyperText
Transfer Protocol (Secure)). Хост — это сервер, от которого мы получаем документ. Хост можно обозначить с помощью цифрового интернет-адреса (см. раздел «IP-адреса» на с. 206), но чаще всего для этого используются текстовые
доменные имена (см. раздел «Система доменных имен» на с. 206). Путь обозначает местоположение нужного нам документа, подобно пути к документу
в файловой системе.

300   Глава 9. Веб-браузер
В качестве протокола можно указать file — в этом случае вместо имени хоста
и пути указывается локальное имя файла в системе, где запущен браузер.
Другими словами, протокол file указывает на файл, расположенный на компьютере.
Число доступных протоколов постоянно растет — например, сегодня можно
встретить протоколы bitcoin для криптовалют и tv для телевизионных трансляций. В целом они подобны, а иногда и идентичны протоколам, рассмотренным
ранее в разделе «Всемирная паутина».

HTML-документы
Как я уже говорил, первые веб-страницы представляли собой документы, написанные на языке разметки HTML. В HTML использовался гипертекст —
текст, в котором содержатся дополнительные ссылки, например на другие
веб-страницы. Любители фантастики могут сравнить гипертекст с гиперпространством: вы щелкаете ссылку и — вжух! — переноситесь в другое место. Гипертекст появился гораздо раньше, чем веб-страницы, но именно в веб-окружении
его наконец-то оценили по достоинству.
Рассмотрим простой HTML-документ в листинге 9.2.
Листинг 9.2. Моя первая веб-страница



My First Web Page



This is my first web page.


Cool!





Сохраните HTML-код из листинга 9.2 в файл на компьютере и откройте его
в браузере. В результате должно получиться как на рис. 9.3.
Видим, что результат на рис. 9.3 не совсем совпадает с текстом из листинга 9.2.
Все из-за знака «меньше» (