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

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

Важность­ декла­ра­ций­ типов,­ особен­но­ при рабо­те­ с масси­ва­ми­ и отдель­­ ными­ числа­ми,­ сложно­ пере­оце­нить­. В данном­ случае­ две строчки­ кода­ обеспе­чи­ли­ нам двена­дца­ти­крат­ный­ прирост­ произ­во­ди­тель­но­сти­.

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