- •Оглавление
- •Предисловие
- •Предисловие к русскому изданию
- •Глава 1. Введение
- •1.1. Новые инструменты
- •1.2. Новые приемы
- •1.3. Новый подход
- •Глава 2. Добро пожаловать в Лисп
- •2.1. Форма
- •2.2. Вычисление
- •2.3. Данные
- •2.4. Операции со списками
- •2.5. Истинность
- •2.6. Функции
- •2.7. Рекурсия
- •2.8. Чтение Лиспа
- •2.9. Ввод и вывод
- •2.10. Переменные
- •2.11. Присваивание
- •2.12. Функциональное программирование
- •2.13. Итерация
- •2.14. Функции как объекты
- •2.15. Типы
- •2.16. Заглядывая вперед
- •Итоги главы
- •Упражнения
- •Глава 3. Списки
- •3.1. Ячейки
- •3.2. Равенство
- •3.3. Почему в Лиспе нет указателей
- •3.4. Построение списков
- •3.5. Пример: сжатие
- •3.6. Доступ
- •3.7. Отображающие функции
- •3.8. Деревья
- •3.9. Чтобы понять рекурсию, нужно понять рекурсию
- •3.10. Множества
- •3.11. Последовательности
- •3.12. Стопка
- •3.13. Точечные пары
- •3.14. Ассоциативные списки
- •3.15. Пример: поиск кратчайшего пути
- •3.16. Мусор
- •Итоги главы
- •Упражнения
- •Глава 4. Специализированные структуры данных
- •4.1. Массивы
- •4.2. Пример: бинарный поиск
- •4.3. Строки и знаки
- •4.4. Последовательности
- •4.5. Пример: разбор дат
- •4.6. Структуры
- •4.7. Пример: двоичные деревья поиска
- •4.8. Хеш-таблицы
- •Итоги главы
- •Упражнения
- •Глава 5. Управление
- •5.1. Блоки
- •5.2. Контекст
- •5.3. Условные выражения
- •5.4. Итерации
- •5.5. Множественные значения
- •5.6. Прерывание выполнения
- •5.7. Пример: арифметика над датами
- •Итоги главы
- •Упражнения
- •Глава 6. Функции
- •6.1. Глобальные функции
- •6.2. Локальные функции
- •6.3. Списки параметров
- •6.4. Пример: утилиты
- •6.5. Замыкания
- •6.6. Пример: строители функций
- •6.7. Динамический диапазон
- •6.8. Компиляция
- •6.9. Использование рекурсии
- •Итоги главы
- •Упражнения
- •Глава 7. Ввод и вывод
- •7.1. Потоки
- •7.2. Ввод
- •7.3. Вывод
- •7.4. Пример: замена строк
- •7.5. Макрознаки
- •Итоги главы
- •Упражнения
- •Глава 8. Символы
- •8.1. Имена символов
- •8.2. Списки свойств
- •8.3. А символы-то не маленькие
- •8.4. Создание символов
- •8.5. Использование нескольких пакетов
- •8.6. Ключевые слова
- •8.7. Символы и переменные
- •8.8. Пример: генерация случайного текста
- •Итоги главы
- •Упражнения
- •Глава 9. Числа
- •9.1. Типы
- •9.2. Преобразование и извлечение
- •9.3. Сравнение
- •9.4. Арифметика
- •9.5. Возведение в степень
- •9.6. Тригонометрические функции
- •9.7. Представление
- •9.8. Пример: трассировка лучей
- •Итоги главы
- •Упражнения
- •Глава 10. Макросы
- •10.1. Eval
- •10.2. Макросы
- •10.3. Обратная кавычка
- •10.4. Пример: быстрая сортировка
- •10.5. Проектирование макросов
- •10.6. Обобщенные ссылки
- •10.7. Пример: макросы-утилиты
- •10.8. На Лиспе
- •Итоги главы
- •Упражнения
- •Глава 11. CLOS
- •11.1. Объектно-ориентированное программирование
- •11.2. Классы и экземпляры
- •11.3. Свойства слотов
- •11.4. Суперклассы
- •11.5. Предшествование
- •11.6. Обобщенные функции
- •11.7. Вспомогательные методы
- •11.8. Комбинация методов
- •11.9. Инкапсуляция
- •11.10. Две модели
- •Итоги главы
- •Упражнения
- •Глава 12. Структура
- •12.1. Разделяемая структура
- •12.2. Модификация
- •12.3. Пример: очереди
- •12.4. Деструктивные функции
- •12.5. Пример: двоичные деревья поиска
- •12.6. Пример: двусвязные списки
- •12.7. Циклическая структура
- •12.8. Неизменяемая структура
- •Итоги главы
- •Упражнения
- •Глава 13. Скорость
- •13.1. Правило бутылочного горлышка
- •13.2. Компиляция
- •13.3. Декларации типов
- •13.4. Обходимся без мусора
- •13.5. Пример: заранее выделенные наборы
- •13.6. Быстрые операторы
- •13.7. Две фазы разработки
- •Итоги главы
- •Упражнения
- •Глава 14. Более сложные вопросы
- •14.1. Спецификаторы типов
- •14.2. Бинарные потоки
- •14.3. Макросы чтения
- •14.4. Пакеты
- •14.5. Loop
- •14.6. Особые условия
- •Глава 15. Пример: логический вывод
- •15.1. Цель
- •15.2. Сопоставление
- •15.3. Отвечая на запросы
- •15.4. Анализ
- •Глава 16. Пример: генерация HTML
- •16.1. HTML
- •16.2. Утилиты HTML
- •16.3. Утилита для итерации
- •16.4. Генерация страниц
- •Глава 17. Пример: объекты
- •17.1. Наследование
- •17.2. Множественное наследование
- •17.3. Определение объектов
- •17.4. Функциональный синтаксис
- •17.5. Определение методов
- •17.6. Экземпляры
- •17.7. Новая реализация
- •17.8. Анализ
- •Комментарии
- •Алфавитный указатель
224 |
Глава 13. Скорость |
Так как до определения single? была сделана глобальная inline-декла рация1, использование single? не будет приводить к реальному вызову функции. Если мы определим вызывающую ее функцию так:
(defun foo (x) (single? (bar x)))
то при компиляции foo код single? будет встроен в код foo так, как если бы мы написали:
(defun foo (x)
(let ((lst (bar x)))
(and (consp lst) (null (cdr lst)))))
Существует два ограничения на inline-встраиваемость функции. Ре курсивные функции не могут быть встроены. И если inline-функция переопределяется, должны быть перекомпилированы все другие функ ции, вызывающие ее, иначе в них останется ее старое определение.
Для того чтобы избежать вызовов функций, в некоторых более ранних диалектах Лиспа использовались макросы (см. раздел 10.2). В Common Lisp делать это необязательно.
Различные компиляторы Лиспа отличаются друг от друга возможно стями оптимизации. Чтобы узнать, какая работа реально проделана компилятором, полезно изучить скомпилированный код, который вы можете получить с помощью disassemble. Эта функция принимает функ цию или имя функции и отображает результат ее компиляции, то есть набор машинных инструкций, которые реализуют эту функцию. Даже если дизассемблерный листинг является для вас китайской грамотой, вы можете хотя бы визуально оценить количество сделанных оптими заций: скомпилируйте две версии – с оптимизирующими декларация ми и без них – и просто оцените разницу. С помощью аналогичной мето дики можно выяснить, были ли функции встроены построчно. В любом случае, перед подобными экспериментами убедитесь, что установлены необходимые параметры компиляции для получения максимально бы строго кода.°
13.3. Декларации типов
Если Лисп – не первый язык программирования, с которым вы сталки ваетесь, то вас может удивить, что до сих пор мы ни разу не использова ли то, что совершенно необходимо во многих других языках: деклара ции типов.
В большинстве языков для каждой переменной необходимо определить свой тип, и в дальнейшем переменная может содержать лишь значения этого типа. Такие языки называют языками с сильной типизацией.
1Чтобы inline-декларации были учтены, возможно, понадобится также уста новить параметры компиляции для генерации быстрого кода.
13.3. Декларации типов |
225 |
Такой подход заставляет программиста выполнять лишнюю работу и ограничивает его возможности: вы не можете писать функции, приме нимые к различным типам аргументов, а также хранить в структурах данных разные типы.° Однако преимуществом данного подхода являет ся упрощение задачи компилятора: если он видит функцию, то знает заранее, какие действия нужно совершить.
В разделе 2.15 упоминалось, что Common Lisp использует более гибкий подход, называемый декларативной типизацией (manifest typing).1 Ти пы имеют значения, а не переменные. Последние могут содержать объ екты любых типов.
Если бы мы на этом остановились, то вынуждены были бы платить ско ростью за гибкость. Поскольку функция + может принимать аргументы разных типов, при каждом выполнении она должна затрачивать допол нительное время на выяснение того, с данными каких типов она вызы вается.
Если, например, от функции требуется всего лишь сложение целых чи сел, отказ от ее оптимизации приведет к низкой производительности. Подход к решению данной проблемы в Common Lisp таков: сообщите все, что вам известно. Если вам заранее известно, что вы складываете два числа типа fixnum, то можно объявить их таковыми, и компилятор сгенерирует код целочисленного сложения такой же, как в С.
Таким образом, различие в подходе к оптимизации не приводит к раз нице в плане скорости. Просто первый подход требует всех деклараций типов, а второй – нет. В Common Lisp объявления типов совершенно не обязательны. Они могут ускорить работу программы, но (если, конеч но, они сами корректны) не способны изменить ее поведение.
Глобальные декларации выполняются с помощью declaim, за которой должна следовать хотя бы одна декларационная форма. Декларация типа – это список, содержащий тип символа, сопровождаемый именем типа и именами одной или более переменных. Таким образом, для объ явления типа глобальной переменной достаточно сказать:
(declaim (type fixnum *count*))
ANSI Common Lisp допускает декларации без использования слова type:
(declaim (fixnum *count*))
Локальные декларации выполняются с помощью declare, которая при нимает те же аргументы, что и declaim. Декларации могут начинать лю бое тело кода, в котором появляются новые переменные: defun, lambda,
1Применяемый в Лиспе подход к типизации можно описать двумя способа ми: по месту хранения информации о типах и по месту ее применения. Дек ларативная типизация подразумевает связывание информации о типе с объ ектом данных, а типизация времени выполнения (run-time typing) подразу мевает, что информация о типах используется лишь в процессе выполнения программы. По сути, это одно и то же.
226 |
Глава 13. Скорость |
let, do и другие. К примеру, чтобы сообщить компилятору, что парамет ры функции принадлежат типу fixnum, нужно сказать:
(defun poly (a b x) (declare (fixnum a b x))
(+ (* a (expt x 2)) (* b x)))
Имя переменной в декларации ссылается на переменную, действитель ную в том же контексте, где встречается сама декларация.
Вы можете также задать тип любого выражения в коде с помощью the. Например, если нам известно, что значения a, b и x не только принадле жат типу fixnum, но и достаточно малы, чтобы промежуточные выраже ния также принадлежали типу fixnum, вы можете указать это явно:
(defun poly (a b x) (declare (fixnum a b x))
(the fixnum (+ (the fixnum (* a (the fixnum (expt x 2)))) (the fixnum (* b x)))))
Выглядит довольно неуклюже, не так ли? К счастью, есть две причины, по которым вам редко понадобится шпиговать численный код объявле ниями the. Во-первых, это лучше поручить макросам.° Во-вторых, мно гие реализации используют особые трюки, чтобы ускорить целочислен ную арифметику независимо от деклараций.
В Common Lisp существует невероятное многообразие типов; их набор практически не ограничен, ведь вы можете самостоятельно объявлять собственные типы. Однако явно объявлять типы имеет смысл только в некоторых случаях. Вот два основных правила, когда это стоит делать:
1.Имеет смысл декларировать типы в тех функциях, которые могут работать с аргументами некоторых разных типов (но не всех). Если вам известно, что аргументы вызова функции + всегда будут fixnum или первый аргумент aref всегда будет массивом одного типа, декла рация будет полезной.
2.Обычно имеет смысл декларировать лишь те типы, которые нахо дятся внизу иерархии типов: объявления с fixnum или simple-array будут полезными, а вот декларации integer или sequence не принесут ощутимого результата.
Декларации типов особенно важны при работе со сложными объекта ми, включая массивы, структуры и экземпляры. Такие декларации не только приводят к более быстрому коду, но и позволяют более эффек тивно организовать объекты в памяти.
Если о типе элементов массива ничего неизвестно, то он представляется в памяти как набор указателей. Однако если тип известен и все элемен ты принадлежат к одному типу, скажем double-float, тогда массив мо жет быть представлен как набор чисел в формате double-float. Во-пер вых, такой массив будет более экономно использовать память. Во-вто рых, отсутствие необходимости переходить по указателям приведет к более быстрому чтению и записи элементов.
13.3. Декларации типов |
|
227 |
x = |
|
|
1.234d0 |
2.345d0 |
3.456d0 |
y = |
|
|
1.234d0 |
2.345d0 |
3.456d0 |
Рис. 13.1. Результат задания типа элементов массива |
|
Тип массива можно задать с помощью аргумента :element-type в makearray. Такой массив называется специализированным. На рис. 13.1 по казано, что будет происходить в большинстве реализаций при выполне нии следующего кода:
(setf x (vector 1.234d0 2.345d0 3.456d0)
y (make-array 3 :element-type ’double-float) (aref y 0) 1.234d0
(aref y 1) 2.345d0 (aref y 2) 3.456d0)
Каждый прямоугольник на рисунке соответствует машинному слову в памяти. Каждый из двух массивов содержит заголовок неопределен ной длины, за которым следует представление трех элементов. В масси ве x каждый элемент – указатель. В нашем случае все три указателя одновременно ссылаются на элементы double-float, но могут ссылаться на произвольные объекты. В массиве y элементы действительно явля ются числами double-float. Второй вариант работает быстрее и занима ет меньше места, но мы вынуждены платить за это ограничением на од нородность массива.
Заметьте, что для доступа к элементам y мы пользовались aref. Специа лизированный вектор больше не принадлежит типу simple-vector, по этому мы не можем ссылаться на его элементы с помощью svref.
При создании массива в коде, который его использует, необходимо по мимо специализации объявить размерность и тип элемента. Такая дек ларация будет выглядеть следующим образом:
(declare (type (vector fixnum 20) v))
Эта запись говорит о том, что вектор v имеет размерность 20 и специа лизирован для целых чисел типа fixnum.
Наиболее общая декларация включает тип массива, тип элементов и спи сок размерностей:
(declare (type (simple-array fixnum (4 4)) ar))
228 |
Глава 13. Скорость |
Массив ar теперь считается простым массивом 4×4, специализирован ным для fixnum.
На рис. 13.2 показано, как создать массив 1 000×1 000 элементов типа single-float и как написать функцию, суммирующую все его элементы. Массивы располагаются в памяти в построчном порядке (row-major order). Рекомендуется по возможности проходить по элементам масси вов в таком же порядке.
(setf a (make-array ’(1000 1000)
:element-type ’single-float :initial-element 1.0s0))
(defun sum-elts (a)
(declare (type (simple-array single-float (1000 1000)) a))
(let ((sum 0.0s0))
(declare (type single-float sum)) (dotimes (r 1000)
(dotimes (c 1000)
(incf sum (aref a r c))))
sum))
Рис. 13.2. Суммирование по массиву
Чтобы сравнить производительность sum-elts с декларациями и без них, воспользуемся макросом time. Он измеряет время, необходимое для вы числения выражения, причем в разных реализациях результаты его работы отличаются. Его применение имеет смысл только для скомпи лированных функций. Если мы скомпилируем sum-elts с параметрами, обеспечивающими максимально быстрый код, time вернет менее полсе кунды:
> (time (sum-elts a))
User Run Time = 0.43 seconds 1000000.0
Если же мы теперь уберем все декларации и перекомпилируем sum-elts, то те же вычисления займут больше пяти секунд:
> (time (sun-elts a))
User Run Time = 5.17 seconds 1000000.0
Важность деклараций типов, особенно при работе с массивами и отдель ными числами, сложно переоценить. В данном случае две строчки кода обеспечили нам двенадцатикратный прирост производительности.