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

13.4. Обходимся без мусора

229

13.4. Обходимся без мусора

Лисп позво­ля­ет­ отло­жить­ реше­ния­ каса­тель­но­ не только­ типов­ пере­­ менных,­ но и выде­ле­ния­ памя­ти­. На ранних­ стади­ях­ созда­ния­ програм­­ мы возмож­ность­ не думать­ о таких­ вещах­ дает­ больше­ свобо­ды­ вооб­ра­­ жению­. Но по мере­ того­ как програм­ма­ стано­вит­ся­ зрелой,­ она может­ стать быст­рее,­ меньше­ пола­га­ясь­ на дина­ми­че­ское­ выде­ле­ние­ памя­ти­.

Одна­ко­ уменьше­ние­ консин­га­ (consing) не обяза­тель­но­ приве­дет­ к уско­­ рению­ програм­мы­. В реали­за­ци­ях­ Лиспа­ с плохи­ми­ сборщи­ка­ми­ мусо­­ ра такие­ програм­мы­ обычно­ рабо­та­ют­ медлен­­но. До недав­не­го­ време­ни­ большин­ст­во­ реали­за­ций­ имели­ не самые­ удачные­ сборщи­ки­ мусо­ра,­ поэто­му­ быто­ва­ло­ мнение,­ что эффек­тив­ные­ програм­мы­ долж­ны по возмож­но­сти­ избе­гать­ выде­ле­ния­ памя­ти­. Но послед­ние­ усовер­шен­ст­­ вова­ния­ пере­вер­ну­ли­ ситуа­цию­ с ног на голо­ву­. Неко­то­рые­ реали­за­ции­ теперь­ имеют­ настоль­ко­ изощрен­ные­ сборщи­ки­ мусо­ра,­ что выде­лять­ память­ под объек­ты­ и выбра­сы­вать­ их за нена­доб­но­стью­ оказы­­вает­ся­ быст­рее,­ чем исполь­зо­вать­ эту память­ повтор­но­.

В этом разде­ле­ пока­за­ны­ основ­ные­ мето­ды­ эконо­мии­ памя­ти­. Скажет­ся­ ли это на произ­во­ди­тель­но­сти,­ зави­сит­ от исполь­зуе­мой­ реали­за­ции­. Как всегда,­ лучший­ совет:­ попро­буй­те­ сами ­– и все увиди­те­.

Суще­ст­ву­ет­ множе­ст­во­ приемов,­ позво­ляю­щих­ избе­жать­ выде­ле­ния­ па­ мяти­. Неко­то­рые­ из них практи­че­ски­ не изме­ня­ют­ вид вашей­ програм­­ мы. Напри­мер,­ одним­ из наибо­лее­ простых­ мето­дов­ явля­ет­ся­ исполь­зо­­ вание­ дест­рук­тив­ных­ функций­. В следую­щей­ табли­це­ приво­дят­ся­ не­ кото­рые­ часто­ употреб­ляе­мые­ функции­ и их дест­рук­тив­ные­ анало­ги:­

Безопас­ные­

Дест­рук­тив­ные­

append

nconc

reverse

nreverse

remove

delete

remove-if

delete-if

remove-duplicates

delete-duplicates

subst

nsubst

subst-if

nsubst-if

union

nunion

intersection

nintersection

set-difference

nset-difference

 

 

Если­ вам извест­но,­ что моди­фи­ка­ция­ списка­ безопас­на,­ исполь­зуй­те­ delete вместо­ remove, nreverse вместо­ reverse и т. д.

Если­ вы желае­те­ полно­стью­ исклю­чить­ выде­ле­ние­ памя­ти,­ это еще не значит,­ что придет­ся­ забыть­ о возмож­но­сти­ созда­ния­ объек­тов­ на лету­. Чего­ следу­ет­ избе­гать,­ так это выде­ле­ния­ памя­ти­ под объек­ты­ на лету­

230

Глава 13. Скорость

и ее после­дую­ще­го­ сбора­. Общим­ реше­ни­ем­ явля­ет­ся­ забла­го­вре­мен­ное­ выде­ле­ние­ блоков­ памя­ти­ и их дальней­шее­ повтор­ное­ исполь­зо­ва­ние­ «вручную»­. Забла­го­вре­мен­но­ озна­ча­ет­ в момент­ компи­ля­ции­ или неко­­ торой­ проце­ду­ры­ инициа­ли­за­ции­. Когда­ скорость­ начи­на­ет­ играть­ роль, зави­сит­ от прило­же­ния­.

Напри­мер,­ если­ обстоя­тель­ст­ва­ позво­ля­ют­ нам огра­ни­чить­ размер­ стоп­ ки, то вместо­ ее созда­ния­ из отдель­ных­ ячеек­ можно­ сделать­ так, что стопка­ будет­ расти­ и уменьшать­ся­ в рамках­ зара­нее­ выде­лен­но­го­ векто­­ ра. Common Lisp имеет­ встроен­ную­ поддерж­­ку исполь­зо­ва­ния­ векто­ра­

вкаче­ст­ве­ стоп­ки. Для этого­ есть необя­за­тель­ный­ пара­метр­ fill-pointer

вmake-array. Первый­ аргу­мент­ make-array зада­ет­ коли­че­ст­во­ памя­ти,­ вы­ деляе­мой­ под вектор,­ а fill-pointer, если­ задан,­ опре­де­ля­ет­ его исход­­ ную факти­че­скую­ дли­ну:

>(setf *print-array* t)

T

>(setf vec (make-array 10 :fill-pointer 2

:initial-element nil))

#(NIL NIL)

Функции­ рабо­ты­ с после­до­ва­тель­но­стя­ми­ будут­ рассмат­ри­вать­ его как вектор­ из двух элемен­тов:­

> (length vec) 2

но его размер­ может­ вырас­ти­ до 10. Посколь­ку­ этот вектор­ имеет­ указа­­ тель запол­не­ния,­ к нему­ мож­но при­менять­ функции­ vector-push и vectorpop, анало­гич­ные­ push и pop для списков:­

>(vector-push ’a vec)

2

>vec

#(NIL NIL A)

>(vector-pop vec)

A

>vec

#(NIL NIL)

Вызов­ vector-push увели­чил­ указа­тель­ запол­не­ния­ и вернул­ его старое­ значе­ние­. Мы можем­ запи­сы­вать­ в такой­ вектор­ новые­ значе­ния­ до тех пор, пока­ указа­тель­ запол­не­ния­ меньше­ началь­но­го­ разме­ра­ векто­ра,­ задан­но­го­ первым­ аргу­мен­том­ make-array. Когда­ свобод­ное­ место­ закон­­ чится,­ vector-push вернет­ nil. В наш вектор­ vec мы можем­ помес­тить­ до 8 новых­ элемен­тов­.

Векто­ры­ с указа­те­лем­ запол­не­ния­ имеют­ один недос­та­ток ­– они более­ не принад­ле­жат­ типу­ simple-vector, и вместо­ svref нам прихо­дит­ся­ ис­ пользо­вать­ aref. Эти допол­ни­тель­ные­ затра­ты­ стоит­ учесть при выбо­ре­ подхо­дя­ще­го­ реше­ния­.

13.4. Обходимся без мусора

231

Неко­то­рые­ прило­же­ния­ могут­ произ­во­дить­ очень длинные­ после­до­ва­­ тельно­сти­. В таком­ случае­ вам помо­жет­ исполь­зо­ва­ние­ map-into вместо­ map. В каче­ст­ве­ перво­го­ аргу­мен­та­ она полу­ча­ет­ не тип новой­ после­до­ва­­ тельно­сти,­ а саму­ после­до­ва­тель­ность,­ в кото­рую­ будут­ запи­са­ны­ ре­ зульта­ты­. Из этой же после­до­ва­тель­но­сти­ могут­ браться­ и аргу­мен­ты­ для функции,­ кото­рая­ произ­во­дит­ ото­браже­ние­. Для приме­ра­ увели­чим­ на едини­цу­ все элемен­ты­ векто­ра:­

(setf v (map-into v #’1+ v))

На рис. 13.3 пока­зан­ при­мер прило­же­ния,­ рабо­таю­ще­го­ с длинны­ми­ векто­ра­ми­. Это програм­ма,­ гене­ри­рую­щая­ неслож­ный­ словарь­ рифм (точнее,­ словарь­ ранее­ увиден­ных­ рифм).

(defconstant dict (make-array 25000 :fill-pointer 0))

(defun read-words (from)

(setf (fill-pointer dict) 0) (with-open-file (in from :direction :input)

(do ((w (read-line in nil :eof) (read-line in nil :eof)))

((eql w :eof)) (vector-push w dict))))

(defun xform (fn seq) (map-into seq fn seq))

(defun write-words (to)

(with-open-file (out to :direction :output :if-exists :supersede)

(map nil #’(lambda (x) (fresh-line out) (princ x out))

(xform #’nreverse

(sort (xform #’nreverse dict) #’string<)))))

Рис. 13.3. Гене­ра­ция­ слова­ря­ рифм

Функция­ read-words чита­ет­ слова­ из файла,­ содер­жа­ще­го­ по слову­ на строке,­° а write-words печа­та­ет­ их в обрат­ном­ алфа­вит­ном­ поряд­ке­. Ее вывод­ может­ начи­нать­ся­ так:

a amoeba alba samba marimba ...

и завер­шать­ся­ так:

... megahertz gigahertz jazz buzz fuzz

Исполь­зуя­ преиму­ще­ст­ва­ векто­ров­ с указа­те­лем­ запол­не­ния­ и map-into, мы сможем­ легко­ и эффек­тив­но­ напи­сать­ эту програм­му­.

232

Глава 13. Скорость

В расчет­ных­ прило­же­ни­ях­ будьте­ акку­рат­ны­ с типа­ми­ bignum. Опера­­ ции с bignum требу­ют­ выде­ле­ния­ памя­ти­ и рабо­та­ют­ суще­ст­вен­­но мед­ лен­нее. Но даже­ если­ ваша­ програм­ма­ долж­на в конце­ возвра­щать­ зна­ чения­ bignum, ее мож­но сделать­ эффек­тив­нее,­ орга­ни­зо­вав­ проме­жу­точ­­ ные резуль­та­ты­ как значе­ния­ fixnum.

Другой­ способ­ избе­жать­ выде­ле­ния­ памя­ти ­– заста­вить­ компи­ля­тор­ разме­щать­ объек­ты­ не в куче,­ а на стеке­. Если­ вам извест­но,­ что какой­- либо­ объект­ будет­ исполь­зо­вать­ся­ времен­но,­ вы може­те­ избе­жать­ выде­­ ления­ для него­ памя­ти­ в куче­ с помо­щью­ декла­ра­ции­ dynamic extent.

Декла­ри­руя­ dynamic-extent для пере­мен­ной,­ вы утвер­ждае­те,­ что ее зна­ чение­ не будет­ жить дольше,­ чем сама­ пере­мен­ная­. Как может­ значе­ние­ суще­ст­во­вать­ дольше­ пере­мен­ной?­ Вот при­мер:

(defun our-reverse (lst) (let ((rev nil))

(dolist (x lst) (push x rev))

rev))

Функция­ our-reverse акку­му­ли­ру­ет­ пере­дан­ный­ ей список­ в обрат­ном­ поряд­ке­ в rev и по завер­ше­нии­ возвра­ща­ет­ ее значе­ние­. Сама­ пере­мен­­ ная исче­за­ет,­ а список,­ кото­рый­ в ней нахо­дил­ся,­ продол­жа­ет­ суще­ст­­ вовать­ и возвра­ща­ет­ся­ назад­ вызвав­шей­ функции­. Дальней­шая­ его судь­ба непред­ска­зуе­ма­.

Для контра­ста­ рассмот­рим­ следую­щую­ реали­за­цию­ adjoin:

(defun our-adjoin (obj lst &rest args) (if (apply #’member obj lst args)

lst

(cons obj lst)))

Из этого­ опре­де­ле­ния­ функции­ видно,­ что список­ args нику­да­ не пере­­ дает­ся­. Он нужен­ не дольше,­ чем сама­ пере­мен­ная­. Значит,­ в этой си­ туации­ имеет­ смысл сделать­ декла­ра­цию­ dynamic-extent:

(defun our-adjoin (obj lst &rest args) (declare (dynamic-extent args))

(if (apply #’member obj lst args) lst

(cons obj lst)))

Теперь­ компи­ля­тор­ может­ (но не обязан)­ разме­щать­ args на стеке,­ то есть это значе­ние­ будет­ авто­ма­ти­че­ски­ отбро­ше­но­ по выхо­ду­ из our-adjoin.

13.5. Пример: заранее выделенные наборы

Что­бы избе­жать­ дина­ми­че­ско­го­ выде­ле­ния­ памя­ти­ в прило­же­нии,­ кото­­ рое исполь­зу­ет­ структу­ры­ данных,­ вы може­те­ зара­нее­ размес­тить­ кон­ кретное­ их коли­че­ст­во­ в пуле­ (pool). Когда­ вам необ­хо­ди­ма­ структу­ра,­

13.5. Пример: заранее выделенные наборы

233

вы бере­те­ ее из пула,­ а по завер­ше­нии­ рабо­ты­ с ней возвра­щае­те­ струк­ туру­ назад­ в пул.° Чтобы­ проил­лю­ст­ри­ро­вать­ эту идею, напи­шем­ прото­­ тип програм­мы,­ контро­ли­рую­щей­ пере­ме­ще­ние­ кораб­лей­ в гава­ни,­ а затем­ пере­пи­шем­ этот прото­тип­ с исполь­зо­ва­ни­ем­ пула­.

На рис. 13.4 пока­за­на­ первая­ версия­. Глобаль­ная­ пере­мен­ная­ *harbour* содер­жит­ список­ кораб­лей,­ каждый­ из кото­рых­ представ­лен­ в виде­ структу­ры­ ship. Функция­ enter вызы­ва­ет­ся­ при захо­де­ кораб­ля­ в порт; find-ship нахо­дит­ корабль­ с задан­ным­ именем­ (если­ он суще­ст­ву­ет);­ leave­ вызы­­вает­ся,­ когда­ корабль­ поки­да­ет­ гавань­.

(defparameter *harbor* nil)

(defstruct ship name flag tons)

(defun enter (n f d)

(push (make-ship :name n :flag f :tons d) *harbor*))

(defun find-ship (n)

(find n *harbor* :key #’ship-name))

(defun leave (n) (setf *harbor*

(delete (find-ship n) *harbor*)))

Рис. 13.4. Порт

Отлич­ный­ подход­ для напи­са­ния­ первой­ версии­ програм­мы,­ но такая­ реали­за­ция­ произ­во­дит­ доста­точ­но­ много­ мусо­ра­. При выпол­не­нии­ про­ граммы­ память­ выде­ля­ет­ся­ двумя­ путя­ми:­ при захо­де­ кораб­лей­ в га­ вань будут­ произ­во­дить­ся­ новые­ структу­ры,­ а с ростом­ списка­ *harbour* будут­ выде­лять­ся­ новые­ ячей­ки.

Мы можем­ изба­вить­ся­ от обоих­ источ­ни­ков­ мусо­ра,­ выде­ляя­ память­ в момент­ ком­пиля­ции­. На рис. 13.5 при­веде­на­ вторая­ версия­ програм­­ мы, кото­рая­ не произ­во­дит­ ника­ко­го­ мусо­ра­.

Строго­ гово­ря,­ выде­ле­ние­ памя­ти­ все же проис­хо­дит,­ но не в момент­ вы­ полне­ния­. Во второй­ версии­ *harbour* явля­ет­ся­ хеш-табли­цей,­ а не спи­ ском, и простран­ст­во­ под нее выде­ля­ет­ся­ при компи­ля­ции­. Тыся­ча­ структур­ ship также­ созда­ет­ся­ во время­ компи­ля­ции­ и сохра­ня­ет­ся­ в векто­ре­ pool. (Если­ пара­метр­ :fill-pointer имеет­ значе­ние­ t, указа­тель­ запол­не­ния­ разме­ща­ет­ся­ в конце­ векто­ра­.) Теперь­ при вызо­ве­ enter нам не нужно­ созда­вать­ новую­ структу­ру,­ доста­точ­но­ полу­чить­ одну­ из уже суще­ст­вую­щих­ в пуле­ с помо­щью­ make-ship. А когда­ leave удаля­ет­ ко­ рабль из гава­ни,­ соот­вет­ст­вую­щая­ структу­ра­ не стано­вит­ся­ мусо­ром,­ а возвра­ща­ет­ся­ назад­ в пул.

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