- •Оглавление
- •Предисловие
- •Предисловие к русскому изданию
- •Глава 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. Анализ
- •Комментарии
- •Алфавитный указатель
234 Глава 13. Скорость
(defconstant pool (make-array 1000 :fill-pointer t))
(dotimes (i 1000)
(setf (aref pool i) (make-ship)))
(defconstant harbor (make-hash-table :size 1100 :test #’eq))
(defun enter (n f d)
(let ((s (if (plusp (length pool)) (vector-pop pool)
(make-ship)))) |
|
(setf (ship-name s) |
n |
(ship-flag s) |
f |
(ship-tons s) |
d |
(gethash n harbor) s)))
(defun find-ship (n) (gethash n harbor))
(defun leave (n)
(let ((s (gethash n harbor))) (remhash n harbor) (vector-push s pool)))
Рис. 13.5. Порт, версия 2
Используя пулы, мы собственноручно выполняем часть работы по управ лению памятью. Сделает ли это нашу программу быстрее, зависит от то го, как конкретная реализация управляет памятью. Грубо говоря, ис пользование пулов оправдано лишь в реализациях с примитивными сборщиками мусора, а также в приложениях реального времени, где не предсказуемый запуск сборщика мусора может вызвать проблемы.
13.6.Быстрые операторы
Вначале главы было сказано, что Лисп – это, по сути, два разных языка. Если вы приглядитесь к дизайну Common Lisp, то увидите, что часть его операторов предназначена для ускорения выполнения, а другая часть – для удобства разработки.
Например, для доступа к элементу вектора существуют три оператора: elt, aref, svref. Такое разнообразие позволяет выжать из программы максимум производительности. На тех участках, где важна скорость, используйте svref вместо elt, которая работает и с массивами, и со спи сками.
Для работы со списками эффективнее использовать специализирован ную функцию nth, нежели elt. Лишь одна функция, length, не имеет аналогов для разных типов. Почему в Common Lisp нет отдельной вер сии этой функции для списков? Потому что программа, выполняющая
13.6. Быстрые операторы |
235 |
подсчет длины списка, уже безнадежна в плане производительности. В этом случае, как и во многих других, сам дизайн языка объясняет, что является эффективным, а что – нет.
Другая пара похожих функций – eql и eq. Первый предикат проверяет на идентичность, второй – на одинаковое размещение в памяти. Второй предикат обеспечивает большую эффективность, однако его стоит ис пользовать, если только вам заранее известно, что аргументы не явля ются числами или знаками. Два объекта равны с точки зрения eq, если они имеют одинаковое размещение в памяти. Числа и знаки не обязаны располагаться в каком-либо определенном месте в памяти, поэтому eq к ним неприменима (хотя во многих реализациях eq работает с типом fixnum). Для любых других аргументов eq будет работать аналогично eql.
Разумеется, быстрее всего выполнять сравнение объектов с помощью eq, так как при этом Лиспу достаточно сравнить лишь два указателя. Это значит, что хеш-таблицы с тестовой функцией eq (см. рис. 13.5) обес печивают самый быстрый доступ. В таких таблицах gethash хеширует лишь указатели и даже не смотрит, на что они указывают. Помимо ско рости доступа с хеш-таблицами связан еще один момент. Использова ние eq- и eql-таблиц приводит к дополнительным издержкам в случае применения копирующей сборки мусора, поскольку после каждой сбор ки хеши таких таблиц должны быть пересчитаны. Если это вызывает проблемы, лучше использовать eql-таблицы и числа типа fixnum в каче стве ключей.
Вызов reduce может быть эффективнее вызова apply,1 когда функция принимает остаточный параметр. Например, вместо выражения
(apply #’+ ’(1 2 3))
эффективнее использовать следующее выражение:
(reduce #’+ ’(1 2 3))
Кроме того, важно не только вызывать правильные функции, но и вы зывать их правильно. Необязательные аргументы, аргументы по ключу и остаточные аргументы дорогостоящи. В случае обычных аргументов вызывающая сторона кладет их в заранее известное вызванной функ ции место, в то время как для других аргументов требуется дополни тельная обработка на этапе выполнения. Хуже всего в этом отношении дело обстоит с аргументами по ключу. Для встроенных функций, ис пользующих эти аргументы, хорошие компиляторы используют особые подходы, чтобы получить быстрый код2. Но в ваших собственных функ циях рекомендуется избегать использования аргументов по ключу на
1Это утверждение автора является довольно сомнительным. По крайней ме ре, для ряда современных реализаций на приведенном примере наблюдает ся выраженный противоположный эффект. – Прим. перев.
2Речь идет о таких техниках, как, например, прямое кодирование (open co ding). – Прим. перев.
236 |
Глава 13. Скорость |
критических в плане скорости участках программы. Также разумно не передавать много значений через остаточный аргумент там, где этого можно избежать.
Индивидуальные компиляторы иногда применяют особые оптимиза ции. Например, некоторые могут оптимизировать вызов case с целочис ленными ключами в небольшом диапазоне их значений. Как правило, узнать подробнее о специфических оптимизациях в вашей реализации вы можете из документации к ней.
13.7. Две фазы разработки
Если в вашем приложении производительность имеет первостепенное значение, в качестве одного из решений попробуйте переписать крити ческие участки на низкоуровневом языке, например на С или ассембле ре. Такой подход применим не только к Лиспу, но и к любому высоко уровневому языку – критические участки программы на С часто пере писываются на ассемблере, – но чем абстрактнее язык, тем больше пре имуществ дает разбиение разработки на две фазы.
Стандарт Common Lisp не описывает механизм интеграции кода, напи санного на других языках. Эта задача полностью ложится на плечи раз работчиков конкретной реализации, и почти все из них так или иначе решают ее.1
Вам может показаться напрасной тратой времени написание части кода на одном языке с последующим переписыванием ее на другом. Тем не менее практика показала, что это отличный способ написания прило жений. Оказалось, что проще сначала разработать функциональность программы, а затем ее оптимизировать, чем делать это одновременно.
Если бы программирование было всего лишь механическим процессом, трансляцией спецификаций в код, было бы разумно выполнять всю ра боту за один шаг. Но настоящее программирование – это совсем иное. Независимо от проработанности спецификаций исследовательский ком понент в написании программ очень важен, и часто намного важнее, чем этого ожидают.
Казалось бы, если спецификации были хорошими, то программирова ние заключалось бы лишь в кодировании. Это порочная идея, хотя и распространенная. Исследовательская компонента важна уже хотя бы потому, что любая спецификация по определению содержит недос татки. Иначе она не была бы спецификацией.
В других областях точность спецификации может иметь первостепен ное значение. Если вы просите выточить из куска металла определен
1Библиотеки CFFI и UFFI предоставляют обобщенный механизм использо вания интерфейса с внешним кодом (foreign function interface), доступный в разных реализациях. – Прим. перев.
Итоги главы |
237 |
ную форму, необходимо как можно точнее описать, что вы хотите полу чить. Но это правило не распространяется на программы, поскольку и спецификации, и программный код делаются из одного и того же ма териала: текста. Вы не можете разработать спецификацию, которая в точности описывает все требования задачи. В противном случае это была бы уже готовая программа.°
В приложениях, подразумевающих значительное количество исследо ваний (объем которых опять же превзойдет ожидания), имеет смысл разделять реализацию программы на две фазы. Промежуточный ре зультат после первой фазы не будет окончательным. Например, при вая нии бронзовой скульптуры принято делать первый набросок из глины, который затем используется для создания формы, в которую будет отли та скульптура из бронзы.° И хотя в завершенной скульптуре глины не остается, эффект от ее использования все равно заметен. Теперь вообра зите, насколько сложней была бы та же задача при наличии лишь слит ка бронзы и зубила. По тем же причинам удобнее написать программу на Лиспе, а затем переписать на С, чем писать ее на С с самого начала.
Итоги главы
1.К оптимизации не следует приступать раньше времени. Основное внимание нужно уделять узким местам и начинать следует с выбора оптимального алгоритма.
2.Доступно пять параметров управления компиляцией. Они могут ус танавливаться как глобальными, так и локальными декларациями.
3.Хорошие компиляторы могут оптимизировать хвостовую рекурсию, превращая ее в циклы. Встраивание кода позволяет избежать вызо ва функций.
4.Декларации типов не обязательны, но могут сделать программу бо лее производительной. Особенно важны декларации в расчетном ко де и операциях с массивами.
5.Выделение меньшего количества памяти позволяет ускорить про грамму, особенно в реализациях с примитивными сборщиками му сора. Для этого используйте деструктивные функции, предвари тельное выделение памяти и размещение объектов на стеке.
6.В ряде ситуаций полезно брать объекты из предварительно создан ного пула.
7.Некоторые части Лиспа предназначены для быстрой работы, некото рые – для гибкой.
8.Процесс программирования обязательно включает исследователь ский момент, который следует отделять от оптимизации, иногда да же вплоть до использования двух разных языков.