Автор книги: Брайан Керниган
Жанр: Компьютеры: прочее, Компьютеры
сообщить о неприемлемом содержимом
Во всех современных компьютерах базовой единицей обработки и организации памяти являются восемь бит, которые рассматриваются как единое целое. Группа из восьми бит называется байтом. Это слово ввел в 1956 году Вернер Бухгольц, разработчик архитектуры ЭВМ, работавший в IBM. Одним байтом можно закодировать 256 различных значений (28, то есть все уникальные комбинации из восьми нулей и единиц) – например, целые числа от 0 до 255, или 7-битный символ из ASCII (с одним битом в запасе), или что-то еще. Часто конкретный байт входит в более крупную группу, которая отображает что-то более объемное или сложное. Два байта вместе дают 16 бит, чего достаточно для представления числа от 0 до (216 – 1), или 65 535. Они также могут отображать символ из набора Unicode, скажем, из записи названия «Токио», первый или второй в слове. На каждый иероглиф отведено по два байта. Четыре байта – это 32 бита, которые могут определять четыре символа ASCII, или два символа Unicode, или числа до (232 – 1), что составляет около 4,3 миллиарда. Байтами допустимо отображать что угодно, однако сам процессор имеет довольно скромный набор групп – в частности, целые значения различного размера, – а также располагает инструкциями для их обработки.
Если мы хотим записать числовое значение, представленное одним или несколькими байтами, его допустимо выразить в десятичной форме, что удобно для чтения человеком (только если оно точно числовое). Мы можем записать его в двоичной системе, чтобы увидеть отдельные биты: это важно, поскольку разные биты кодируют разные типы информации. Однако двоичная система громоздка, и такие представления более чем втрое длиннее десятичных, поэтому обычно используется альтернативная система счисления, называемая шестнадцатеричной25. Для записи чисел с таким основанием применяются 16 цифр (по аналогии с 10 цифрами для десятичной и 2 для двоичной) – 0,1… 9, A, B, C, D, E и F. Каждая шестнадцатеричная цифра представляет собой 4 бита, числовые значения которых приведены на рис. 2.13.

Рис. 2.13. Таблица шестнадцатеричных цифр и их значений в двоичной системе исчисления
Если вы не программист, то шестнадцатеричный код встречается вам лишь в нескольких местах. Одно из них – краски на веб-странице. Как я упоминал выше, в наиболее распространенном представлении цветов на компьютере используется три байта на каждый пиксель: по одному для красного, зеленого и синего. Эта система кодирования называется RGB (Red, Green, Blue). Каждый из таких компонентов хранится в виде одного байта, поэтому существует 256 различных значений (оттенков) красного, на любой из которых приходится 256 возможных значений зеленого, а на всякую их комбинацию – 256 оттенков синего. В общей сложности получаем 256 × 256 × 256 возможных цветов, что звучит внушительно. Давайте прибегнем к степеням двух и десяти, чтобы быстро прикинуть количество. Это 28 × 28 × 28, что равно 224, или 24 × 220, или примерно 16 × 106, или 16 миллионов. Вероятно, это число попадалось вам в описаниях компьютерных дисплеев («Более 16 миллионов цветов!»)26. Кстати, оценка занижена примерно на 5 %, ведь 224 в десятичной записи – 16 777 216.
Насыщенный красный представлен числом FF0000, то есть максимальным значением из 255 возможных, если брать десятичную систему. В нем нет ни зеленого, ни синего. А вот яркий, но негустой синий цвет, которым выделяются ссылки на многие веб-страницы, кодируется как 000 °CC. Желтый цвет – это смесь красного и зеленого, и его самый яркий оттенок выражается комбинацией FFFF00. Оттенки серого содержат в себе равное количество красного, зеленого и синего. Например, средний серый пиксель будет закодирован числом 808080, в котором содержание всех трех цветов будет одинаковым. Наконец, черный и белый – это 000000 и FFFFFF соответственно.
Шестнадцатеричные значения также используются в кодовых таблицах Unicode для определения символов. Например, –
это 6771 4EAC. Кроме того, вы можете встретить шестнадцатеричные числа в адресах Ethernet, о которых мы поговорим в главе 8, и в специальных символах в URL-адресах – это глава 10.
В рекламе компьютеров вы иногда встречаете понятие «64-битный» («Microsoft Windows 10 Ноше 64-битная»). Что это значит? Внутри себя компьютеры обрабатывают данные в виде блоков разного размера, в которые входят как числа – а для них удобнее применять 32 и 64 бита, – так и адреса, то есть указания, где расположена информация в оперативной памяти. Речь в рекламе идет о втором параметре. Тридцать лет назад произошел переход от 16-битных адресов к 32-битным, которые достаточно велики для доступа к 4 Гб памяти. В настоящее время мы почти перешли от 32 к 64 битам в компьютерах общего назначения27. Не стану предсказывать, когда мы двинемся от 64 к 128 битам: пока все на какое-то время затихнет.
Из этого обсуждения битов и байтов нам важно запомнить, что значение группы битов зависит от контекста: вам не удастся определить, что они отображают, просто посмотрев на них. Например, в байт может входить один бит, представляющий истину или ложь, а семь других не будут использоваться. Там может храниться небольшое целое число или символ ASCII, такой как #. Возможно, что байт составляет либо часть символа в другом алфавите, либо компонент большого числа из 2, 4 или 8 байт, либо фрагмент изображения или музыки на DVD, либо часть инструкции для процессора и так далее. (С десятичными цифрами все то же самое. В зависимости от контекста, трехзначное число может обозначать телефонный код района США, номер шоссе, показатель отбивания в бейсболе и многое другое.)
Инструкции для одной программы иногда служат данными для другой. Когда вы загружаете программу или приложение, то они представляют собой просто данные – биты, копируемые вслепую. Но когда вы запускаете программу, ее биты при обработке в ЦПУ воспринимаются как инструкции.
2.4. Краткие выводыПочему мы используем вместо десятичной системы двоичную? Намного легче создать физические устройства, в которых есть только два состояния (например, включено/выключено), а не десять. Эта сравнительная простота в явном виде присутствует во многих технологиях: ток (протекает или нет), напряжение (высокое или низкое), электрический заряд (есть или нет), магнетизм (направление на север или юг), свет (яркий или тусклый), отражение (блестящее или матовое). Это ясно осознавал фон Нейман, который в 1946 году сказал: «Наша фундаментальная единица памяти естественным образом адаптирована к двоичной системе, поскольку мы не пытаемся измерять градации заряда».
Зачем нам знать про двоичные числа или интересоваться ими? Одна из причин заключается в том, что, оперируя с числами в незнакомой системе счисления, мы можем, например, развить количественное мышление и лучше понять, как устроены числа со старым добрым основанием 10. Также это важно, потому что количество битов обычно тем или иным образом указывает на то, как много места, времени или сложных операций потребуется. Компьютеры вообще заслуживают того, чтобы в них разбирались, а двоичные числа – ключевой элемент их работы.
Двоичные значения также используются в повседневных занятиях, не связанных с компьютерами. Вероятно, дело в том, что такие расчеты, как удвоение или уменьшение веса, длины и тому подобного, вполне естественны для людей. Например, во втором томе книги Дональда Кнута «Искусство программирования»28 описаны английские единицы тары для вина 1300-х годов, в которые входят 13 двоичных порядков величины: 2 джилла – это один шопен, 2 шопена – это одна пинта, 2 пинты – это одна кварта, и так далее, пока 2 барреля не станут одним хогсхедом, 2 хогсхеда не сольются в одну пипу (или пайп), а 2 пипы не образуют один тан. Около половины этих единиц все еще используются в английской системе мер для жидкостей, хотя такие очаровательные слова, как «феркин» и «килдеркин» (два феркина, или половина барреля), ныне встречаются уже редко.
3. Процессор изнутри
Если, однако, приказы для машины сведены к числовому коду и если машины могут каким-то образом отличать число от команды, то орган памяти может хранить как числа, так и приказы.
Артур У. Бёркс, Герман Х. Голдстайн, Джон фон Нейман.Предварительное рассмотрение логического устройства электронного вычислительного прибора, 1946
В главе 1 я упомянул, что процессор, или ЦПУ, – «мозг» компьютера, хотя и с оговоркой, что это определение употребляется не в прямом значении. Сейчас пришло время более подробно рассмотреть процессор, поскольку это самый важный элемент вычислительной машины и его свойства имеют наибольшее значение для оставшейся части книги.
Как работает процессор? Что за процессы в нем идут и как? При первом близком рассмотрении ЦПУ мы обнаруживаем, что оно способно выполнять набор базовых операций. Процессор может, подобно калькулятору, производить арифметические операции: сложение, вычитание, умножение и деление чисел. Он извлекает данные из памяти, обрабатывает их и сохраняет результаты туда же, как действуют многие счетные устройства. Процессор управляет остальными частями компьютера – отправляет по шине сигналы для контроля и координации ввода и вывода от всего, что электрически соединено с ним, включая мышь, клавиатуру, экран и многое другое.
Самое главное – ЦПУ принимает решения, пусть и простые. Процессор способен сравнивать как числа (больше или меньше), так и данные других типов (совпадает ли эта часть информации с другой), и на основании полученных результатов решать, что делать дальше. Это самое важное его свойство, поскольку, хотя процессору доступно немногим больше, чем калькулятору, он умеет работать без вмешательства человека. Как сказали Беркс, Голдстайн и фон Нейман, «предполагается, что машина будет полностью автоматической по своей природе, то есть независимой от человека-оператора после начала вычислений».
Поскольку процессор может определять дальнейшие действия на основании обрабатываемых данных, он способен самостоятельно управлять всей системой. Хотя его репертуар действий невелик и незамысловат, он умеет выполнять миллиарды операций в секунду, поэтому справляется с чрезвычайно сложными вычислениями.
3.1. Компьютер-игрушкаПозвольте мне объяснить, как работает процессор, описав машину, которой нет. Этот выдуманный или условный компьютер действует по тем же принципам, что и реальный, но он намного проще. Поскольку он существует только на бумаге, я представлю его в той форме, которая лучше всего подойдет, чтобы разъяснить вам принципы действия настоящих ЭВМ. Я также могу создать программу для реального компьютера, которая будет имитировать мое воображаемое творение, а затем написать программы уже для этого воплощения вымышленной машины и посмотреть, как они будут выполняться.
Я привык называть такую выдуманную машину «компьютером-игрушкой», или просто Игрушкой, поскольку она не настоящая, но обладает свойствами реальной. По возможностям она примерно соответствует уровню мини-ЭВМ конца 1960-х годов и напоминает устройство, описанное в первой статье Беркса, Голдстайна и фон Неймана. В компьютере-игрушке есть память, отведенная под инструкции и данные, а также одна дополнительная область хранения – накопитель, емкости которого достаточно для удержания одного числа. Накопитель – это аналог экрана на калькуляторе: он показывает число, только что введенное пользователем, или последние результаты вычислений. Компьютер-игрушка содержит около десяти инструкций для выполнения основных операций, описанных выше. На рис. 3.1 приведены первые шесть из данного набора.

Рис. 3.1. Типовые инструкции в машине-игрушке
Каждая ячейка памяти хранит одно число или одну инструкцию, поэтому программа состоит из последовательности инструкций и элементов данных, сохраненных в памяти. Выполняя операции, процессор начинает с первой ячейки памяти и затем повторяет простой цикл:
Fetch: извлечь следующую инструкцию из памяти
Decode: определить, что выполнять по этой инструкции (декодировать)
Execute: выполнить инструкцию вернуться к Fetch
Чтобы создать программу для Игрушки, мы должны написать последовательность инструкций для решения желаемой задачи, поместить их в память и приказать ЦПУ выполнить их. В качестве примера предположим, что память содержит следующие инструкции, которые хранятся как двоичные числа:
GET
STOP
Когда эта последовательность выполняется, первая инструкция запрашивает у пользователя число, вторая отображает введенное число, а третья велит процессору остановиться. Это смертельно скучно, но наглядно показывает, что такое программа. Будь у нас реальная машина-игрушка, то программа могла бы даже запуститься.
И, к счастью, рабочие игрушечные компьютеры существуют. На рис. 3.2 показан один из них в действии.
Это симулятор, написанный на JavaScript, поэтому его можно запустить на любом браузере, как мы увидим в главе 7.

Рис. 3.2. Симулятор компьютера-игрушки с готовой к запуску программой
Когда вы нажимаете ПУСК, выполняется инструкция GET и появляется диалоговое окошко (см. рис. 3.3). Число 123 введено пользователем.

Рис. 3.3. Диалоговое окно ввода симулятора Игрушки
После того как пользователь напечатал число и нажал ОК, симулятор запускается и отображает результаты, как показано на рис. 3.4. Как и было обещано, программа запрашивает введенное число, отображает его и останавливается.

Рис. 3.4. Симулятор Игрушки после выполнения короткой программы
Следующая программа (см. рис. 3.5) немного сложнее, поскольку в ней добавлена еще одна функция – хранить значение в памяти и затем извлекать его. Программа получает число в накопитель, сохраняет в памяти, получает второе число в накопитель (оно переписывает первое), добавляет к нему первое число (извлекая его из памяти, где оно бережно хранилось), печатает сумму и затем останавливается.
Процессор запускается в начале программы и извлекает инструкции по одной за раз. Он по очереди выполняет каждую команду, затем переходит к следующей. После любой инструкции напечатан комментарий, то есть объяснения в помощь программистам, не влияющие на саму программу.
Единственная сложность заключается в том, что нам нужно выделить место в памяти для хранения данных – первого числа, которое будет считываться. Мы не можем оставить первое число в накопителе, так как вторая инструкция (GET) перезапишет его. Поскольку число – это данные, а не инструкция, мы должны хранить его в определенном месте в памяти, где оно не будет истолковано как инструкция. Но, если мы поместим его в конец программы, после всех инструкций, процессор точно не попытается интерпретировать значение данных как инструкцию, потому что остановится (STOP) и не доберется до него.

Рис. 3.5. Программа компьютера-игрушки для сложения двух чисел и вывода суммы
Нам также нужен способ ссылаться на место хранения, когда этого потребуют инструкции программы. Мы можем заметить одну возможность для хранения данных – седьмую ячейку памяти (после шестой инструкции) – и написать «STORE 7». В конечном итоге наша программа будет сохранена в такой форме. Но местоположение (адрес) может меняться при изменении программы. Решение в том, чтобы присвоить ему имя: как мы увидим в главе 5, любая программа способна выполнять такую «конторскую» задачу, то есть отслеживать, где в памяти находятся данные, заменяя имя правильным положением числа. FirstNum указывает, что это «первое число». Имя задается произвольно, хотя рекомендуется использовать то, которое отражает предназначение или смысл связанных с ним данных или инструкций. После имени мы поставили двоеточие, чтобы определить FirstNum как метку. По соглашениям о именовании, инструкции в программе набирают с отступом, а имена, привязанные к инструкциям или ячейкам памяти, – без. В симуляторе Игрушки учитываются все эти детали.
Как нам расширить программу, показанную на рис. 3.5, чтобы она суммировала три числа? Можно просто добавить еще одну последовательность инструкций – STORE, GET и ADD (для них есть два свободных места), но такое «расширение», конечно, не позволит суммировать тысячу чисел. Программа вообще не даст результата, если мы не будем знать заранее, сколько чисел нужно сложить.
Верное решение – дополнить репертуар процессора новым типом инструкций, который позволит ему повторно использовать последовательности команд. Инструкция GOTO, которую часто называют «ветвлением» или «переходом», указывает процессору, что следующую команду нужно брать не по порядку, а из местоположения, указанного в самой GOTO.
С помощью инструкции GOTO мы можем заставить процессор вернуться к предшествующей части программы и повторить выполнение команд. Вот несложный пример: программа, которая выводит каждое число по мере его ввода. По сути, так работает любая программа, которая копирует или отображает ввод данных для нее, и здесь показывается, что делает инструкция GOTO. Первая инструкция программы, изображенная на рис. 3.6, называется Тор («Верх»). Имя задано произвольно и указывает на ее роль. Наконец, последняя инструкция заставляет процессор вернуться к первой.

Рис. 3.6. Программа копирования данных, которая выполняется вечно
Это отчасти нам помогает: мы можем неоднократно использовать инструкции. Но перед нами все еще стоит критическая проблема: невозможно остановить бесконечное повторение последовательности инструкций, или цикла. Чтобы прервать цикл, нам нужна инструкция другого вида, которая позволит не бездумно продолжать, а проверить некое условие и решить, что делать дальше. Инструкция такого рода называется условным ветвлением или условным переходом. Одна из возможностей, которая есть в любом компьютере, – инструкция, которая проверяет, равно ли какое-либо значение нулю, и, если это так, переходит к определенной команде. К счастью, в Игрушке есть инструкция под названием IFZERO, которая переходит к заданной команде, если значение в накопителе равно нулю. В противном случае выполняется следующая инструкция в последовательности.
Давайте задействуем инструкцию IFZERO, чтобы написать программу (см. рис. 3.7), которая считывает и печатает входные значения до тех пор, пока в них не появится значение, равное 0. Она будет извлекать и отображать данные, пока пользователь не устанет и не введет ноль, после чего программа перейдет к инструкции STOP, помеченной как Bot, то есть bottom («низ»), и остановится. (Заманчиво написать IFZERO STOP, но так не сработает: за IFZERO должна следовать ячейка, а не инструкция.)

Рис. 3.7. Программа копирования данных, которая останавливается при вводе О
Обратите внимание, что у нас не отображается ноль, который служит сигналом о прекращении ввода. Как бы вы изменили программу, чтобы она выводила ноль перед тем, как остановиться? Вопрос без подвоха – ответ здесь ясен. Но это отлично показывает, как в программах возникают небольшие различия в ходе выполнения задачи, или как они вообще начинают делать не то, что задумывалось, – и все из-за того, что пару инструкций просто поменяли местами.
Сочетание GOTO и IFZERO позволяет нам писать программы, которые повторяют инструкции до тех пор, пока некое заданное условие не станет истинным. Значит, процессор может изменять порядок вычислений в зависимости от результатов предыдущих вычислений. (Подумайте, нельзя ли обойтись без GOTO, если у вас есть IFZERO. Найдется ли способ имитировать GOTO с помощью IFZERO и некоторых других инструкций?) Как ни удивительно, больше нам ничего не требуется для совершения операций, которые способен произвести любой цифровой компьютер. Какое угодно вычисление можно разбить на небольшие шаги, применяя элементарные инструкции.
Процессор Игрушки, имеющий в своем арсенале IFZERO, в принципе можно запрограммировать на выполнение буквально любых вычислений.
Я говорю «в принципе», потому что на практике мы обязаны учитывать скорость процессора, объем памяти, ограниченность величины чисел в компьютере и тому подобное. Так или иначе, иногда мы будем возвращаться к идее эквивалентности всех ЭВМ, поскольку это фундаментальная концепция.
В качестве еще одного примера работы IFZERO и GOTO на рис. 3.8 приведена программа, которая суммирует несколько чисел и останавливается, когда введен 0. Какое-либо специальное значение сплошь и рядом применяют для того, чтобы завершить последовательность ввода. В данном случае ноль вполне подходит в качестве метки окончания, потому что мы складываем числа, а добавлять данные с нулевым значением не обязательно.
Симуляторы компьютера-игрушки интерпретируют «инструкции» вроде последней строчки в этой программе так: «определить имя для ячейки памяти и поместить определенное цифровое значение в эту ячейку перед тем, как программа будет запущена». Это не настоящая инструкция, а скорее «псевдоинструкция», которая интерпретируется симулятором при обработке текста программы перед ее запуском.
Нам нужно место в памяти, чтобы сохранять текущую сумму в процессе сложения. В начале работы эта ячейка памяти должна содержать нулевое значение, по аналогии с тем, как мы очищаем память калькулятора. Нам также необходимо дать ячейке памяти имя, которое будет использоваться остальной программой для обращения к ней. Оно задается произвольно, но Sum – удачный вариант, так как указывает на роль ячейки памяти.

Рис. 3.8. Программа Игрушки для сложения последовательности чисел
Как бы вы проверили эту программу, чтобы убедиться, что она работает? На первый взгляд все выглядит нормально, и она дает правильные ответы на простые тестовые задачи. Упустить из виду проблему легко, поэтому нужна систематическая проверка. Ключевое слово – «систематическая»: просто вводить в программу случайные данные будет неэффективно.
Какой самый простой тестовый пример? Если мы не вводим никаких чисел, кроме нуля, который завершает ввод, то сумма будет равняться нулю, так что в качестве первой проверки годится. Потом нужно попробовать ввести одно число: сумма должна совпасть с ним. Затем надо ввести два числа, сумма которых уже вам известна (например, 1 и 2, где результат должен равняться 3). После нескольких таких тестов вы можете с достаточной степенью уверенности утверждать, что программа работает. Если вы скрупулезны, то проверите код еще до того, как он попадет на компьютер, – тщательно пройдетесь по всей последовательности инструкций. Хороший программист поступает так с каждой написанной им программой.