- •Оглавление
- •Предисловие
- •Предисловие к русскому изданию
- •Глава 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. Анализ
- •Комментарии
- •Алфавитный указатель
13
Скорость
На самом деле, Лисп сочетает в себе два языка: язык для быстрого на писания программ и язык для написания быстрых программ. На пер вых этапах создания программы вы можете пожертвовать ее скоростью ради удобства разработки, а когда ее структура примет окончательную форму, можно воспользоваться инструментами языка, позволяющими увеличить скорость работы программы.
Довольно сложно давать общие рекомендации по поводу оптимизации из-за внушительного разнообразия реализаций Common Lisp. Дейст вие, ускоряющее выполнение вашей программы в одной из них, может замедлить работу в другой. Чем мощнее язык, тем дальше он отстоит от аппаратной части, а чем дальше вы от аппаратной части, тем выше шансы, что разные реализации по-разному будут возвращаться к ней при генерации программного кода. Поэтому, несмотря на то что некото рые методики, описанные ниже, почти наверняка ускорят выполнение кода в любой реализации, все же не стоит забывать о рекомендательном характере содержимого этой главы.
13.1. Правило бутылочного горлышка
Независимо от реализации есть три момента, касающиеся оптимиза ции: она должна быть сосредоточена на бутылочных горлышках, она не должна начинаться слишком рано и она должна начинаться с алгорит мов. Пожалуй, по поводу оптимизации важнее всего понять, что в лю бой программе, как правило, есть несколько узких мест, выполнение которых занимает большую часть времени. По мнению Кнута, «подав ляющая часть времени выполнения программы, не связанной с вводом- выводом, сосредоточена в примерно 3% исходного кода».° Оптимизация таких участков даст существенно большее увеличение скорости, чем оптимизация остальных частей, которая будет пустой тратой времени.
13.2. Компиляция |
221 |
Поэтому исключительно важный первый шаг оптимизации любой про граммы – поиск подобных узких мест, или бутылочных горлышек. Мно гие реализации Лиспа включают собственные профилировщики, кото рые наблюдают за выполнением программы и предоставляют отчет о времени, затраченном на каждую из ее частей, выявляя тем самым уз кие места. Профилировщик – ценный инструмент, совершенно необхо димый для создания действительно эффективного кода. Если ваша реа лизация Лиспа включает профилировщик, то наверняка предоставля ет и документацию на него, которую следует изучить. Если же такого инструмента у вас нет, то узкие места придется угадывать самостоя тельно, и вы будете удивлены, когда узнаете, как часто подобные догад ки бывают ошибочными.
Следствие правила бутылочных горлышек: не стоит вкладывать слиш ком много усилий в оптимизацию на ранних стадиях написания про граммы. Кнут предлагает еще более жесткую формулировку: «Прежде временная оптимизация является корнем всех зол (или, по крайней ме ре, большей части) в программировании».° Пока программа не написа на, сложно сказать, где возникнет узкое место, поэтому не тратьте свое время зря. Кроме того, оптимизация затрудняет модификацию, и по пытка оптимизировать программу в момент ее написания сродни по пытке писать картину быстросохнущими красками.
Вероятность того, что программа получится хорошей, возрастет, если на каждой подзадаче можно будет сфокусироваться в подходящий момент времени. Одним из достоинств Лиспа является возможность работать в разном ритме: быстро писать медленный код или медленно писать бы стрый код. На первых этапах вы используете первый метод, а на стадии оптимизации переключаетесь на второй. Эта модель соответствует пра вилу бутылочного горлышка. В низкоуровневых языках, таких как ас семблер, вы фактически оптимизируете каждую строку кода. Большая часть этих усилий тратится напрасно, так как бутылочные горлышки составляют лишь небольшую часть всего кода. Более абстрактный язык позволяет уделить узким местам значительную долю времени, поэтому при меньших усилиях вы получаете существенно большую выгоду.
Приступая к оптимизации, начинайте с самого верху. Для начала убе дитесь, что используете наиболее оптимальный алгоритм, и лишь по том переходите к низкоуровневым трюкам при написании кода. Потен циальная выгода велика – возможно даже настолько, что вам, может быть, вообще не придется прибегать к каким-либо трюкам. Это утвер ждение нужно сочетать с предыдущим правилом: часто решение об ал горитме нужно принимать на ранней стадии написания программы.
13.2. Компиляция
Для управления процессом компиляции доступно пять параметров: speed (скорость производимого кода), compilation-speed (скорость самого
222 |
Глава 13. Скорость |
процесса компиляции), safety (количество проверок на ошибки в объ ектном коде), space (размер памяти, выделяемой для объектного кода) и debug (объем информации, оставляемой в коде для отладки) .
Параметры компиляции не являются переменными в привычном пони мании. Это своего рода веса, определяющие важность каждого пара метра: от 0 (не важно) до 3 (очень важно). Представим, что узкое место находится во внутреннем цикле некоторой функции. Мы можем доба вить декларацию следующего вида:
(defun bottleneck (...) (do (...)
(...) (do (...)
(...)
(declare (optimize (speed 3) (safety 0)))
...)))
Как правило, подобные декларации добавляются лишь после заверше ния основной работы по созданию программы.
Чтобы глобально запросить наиболее быстрый компилируемый код, можно сказать:
(declaim (optimize (speed 3) (compilation-speed 0) (safety 0)
(debug 0)))
Это весьма радикальный и часто ненужный шаг. Не забывайте про бу тылочное горлышко.1
Важным классом оптимизаций, производимых компиляторами Лиспа, является оптимизация хвостовых вызовов. Задавая максимальный вес параметра speed, вы включаете эту опцию, если она поддерживается компилятором.
Вызов считается хвостовым, если после него не нужно ничего вычис лять. Следующая функция возвращает длину списка:
(defun length/r (lst) (if (null lst)
0
(1+ (length/r (cdr lst)))))
Данный рекурсивный вызов не является хвостовым, так как его значе ние передается функции 1+. А вот в следующей версии есть хвостовая рекурсия:
(defun length/tr (lst) (labels ((len (lst acc)
(if (null lst)
1Старые реализации могут не предоставлять declaim. В этом случае исполь зуйте proclaim с цитируемым аргументом.
13.2. Компиляция |
223 |
acc
(len (cdr lst) (1+ acc))))) (len lst 0)))
Вообще говоря, хвостовая рекурсия имеет здесь место в локальной функции len, поскольку рекурсивный вызов является последним дей ствием в функции. Вместо того чтобы вычислять результирующее зна чение на обратном пути рекурсии, как это делает length/r, она аккуму лирует его на пути вперед. Для этого и нужен аргумент acc, значение которого возвращается в конце рекурсивного вызова.
Хороший компилятор может преобразовывать хвостовой вызов в goto, и таким образом хвостовая рекурсия скомпилируется в обычный цикл.° В машинном коде, когда управление впервые передается той его части, которая реализует функцию len, на стек кладется информация, указы вающая, что нужно сделать для завершения вызова. Но, поскольку по сле самого рекурсивного вызова делать ничего не надо, эти инструкции остаются действительными также и для второго вызова: для возврата из второго вызова нужно сделать то же самое, что и для возврата из пер вого. Таким образом, мы можем просто заменить старые аргументы функции новыми значениями, а после этого прыгнуть назад к началу функции и действовать так, как если бы это был второй вызов. При этом нет необходимости делать реальный вызов функции.
Другой способ уйти от затрат на полноценный вызов функции – заста вить компилятор встраивать функции построчно (inline). Это особенно ценно для небольших функций, затраты на выполнение которых сопо ставимы с затратами на сам вызов. Например, следующая функция вы ясняет, является ли ее аргумент списком или одиночным элементом:
(declaim (inline single?))
(defun single? (lst)
(and (consp lst) (null (cdr lst))))
Интерактивный или интерпретируемый
Лисп – это интерактивный язык, но это не обязывает его быть ин терпретируемым. Ранние версии Лиспа реализовывались как ин терпретаторы, в результате чего сложилось мнение, что все пре имущества Лиспа вытекают из его интерпретируемости. Эта идея ошибочна: Common Lisp является интерпретируемым и компи лируемым языком одновременно.
Некоторые реализации Common Lisp и вовсе не используют ин терпретацию. В них все, что вводится в toplevel, компилируется перед вычислением. Таким образом, называть toplevel интерпре татором не только старомодно, но и, строго говоря, ошибочно.