Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Pol_Grem_-_ANSI_Common_Lisp_High_tech_-_2012.pdf
Скачиваний:
28
Добавлен:
12.03.2016
Размер:
4.85 Mб
Скачать

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 интер­пре­­ тато­ром­ не только­ старо­мод­но,­ но и, строго­ гово­ря,­ ошибоч­но­.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]