Как Lisp может быть динамическим и скомпилированным?
хорошо, поэтому сначала, чтобы избавиться от этого: я прочитал следующий ответ:
как Лисп динамический и компилируется?
но я действительно не понимаю его ответ.
в языке, таком как Python, выражение:
x = a + b
невозможно скомпилировать, так как для" компилятора " было бы невозможно узнать типы a и b (поскольку типы известны только во время выполнения), и, следовательно, как их добавить.
это то, что делает язык, такой как Python, невозможным для компиляции без объявлений типов, правильно? С объявлениями компилятор знает, что, например, a и b являются целыми числами, и поэтому знает, как их добавить и перевести в собственный код.
Так как:
(setq x 60)
(setq y 40)
(+ x y)
работы?
скомпилировано определяется как родной досрочной компиляции.
редактировать
на самом деле, это вопрос больше о том, можно ли компилировать динамические языки без объявлений типов, и если да, то как?
EDIT 2
после многих исследований (т. е. заядлый просмотр Википедии) я думаю, что понимаю следующее:
- динамические типизированные языки-это языки, в которых типы проверяются во время выполнения
- статические типизированные языки-это языки, где типы проверяются во время компиляции
- тип объявления позволяют компилятору сделать код более эффективным, потому что вместо того, чтобы делать вызовы API все время, он может использовать более собственные "функции" (поэтому вы можете добавить объявления типов в код Cython, чтобы ускорить его, но не должны, потому что он все еще может просто вызвать библиотеки Python в коде C)
- в Lisp нет типов данных; поэтому нет типов для проверки (тип-это сами данные)
- Obj-C имеет как статические, так и динамические объявления; первый проверяются ли типы во время компиляции, последние во время выполнения
исправьте меня, если я ошибаюсь в любом из вышеперечисленных пунктов.
2 ответов
Пример Кода:
(setq x 60)
(setq y 40)
(+ x y)
выполнение с интерпретатором Lisp
в интерпретаторе на основе Lisp выше будут данные Lisp, и интерпретатор смотрит на каждую форму и запускает оценщик. Поскольку он работает со структурами данных Lisp, он будет делать это каждый раз, когда он видит выше code
- получить первую форму
- у нас есть выражение
- это специальный SETQ форма
- оценить 60, результат 60
- найдите место для переменной x
- установите переменную x в 60
- получить следующую форма... ...
- у нас есть вызов функции +
- оценить x - > 60
- оценить y - > 40
- вызовите функцию + с 60 и 40 - > 100 ...
теперь +
код, который на самом деле знает, что делать. Сюсюкать, как правило, имеет различные типы чисел и (почти) ни один процессор не поддерживает все эти: fixnums, bignums, ratios, complex, float, ... Так что +
функция должна выяснить, какие типы имеют Аргументы и что она может сделать, чтобы добавить их.
выполнение с компилятором Lisp
компилятор просто выдаст машинный код, который будет делать операции. машинный код будет делать все, что делает интерпретатор: проверка переменных, проверка типы, проверка количества аргументов, вызов функций,...
если вы запустите машинный код, это намного быстрее, так как выражения Lisp не нужно рассматривать и интерпретировать. Интерпретатору нужно будет расшифровать каждое выражение. Компилятор уже сделал это.
он все еще медленнее, чем некоторый код C, так как компилятор не обязательно знает типы и просто выдает полностью безопасный и гибкий код.
таким образом, этот скомпилированный код Lisp намного быстрее, чем интерпретатор, выполняющий исходный код Lisp.
использование оптимизирующего компилятора Lisp
иногда это недостаточно быстро. Затем вам нужен лучший компилятор и скажите компилятору Lisp, что он должен больше работать в компиляции и создавать оптимизированный код.
компилятор Lisp может знать типы аргументов и переменных. Затем вы можете сказать компилятору опустить проверки выполнения. Компилятор также может предположить, что +
всегда одна и та же операция. Таким образом, он может встроить необходимый код. Поскольку он знает типы, он может генерировать код только для этих типов: integer addition.
но все же семантика Lisp отличается от операций C или машины. А +
не только имеет дело с различными типами чисел, он также автоматически переключается с малых целых чисел (fixnums) на большие целые числа (bignums) или ошибки сигнала при переполнении для некоторых типов. Вы также можете сказать компилятору, чтобы он опустил это и просто используйте собственное целочисленное сложение. Тогда ваш код будет быстрее , но не таким безопасным и гибким, как обычный код.
это пример полностью оптимизированного кода с использованием 64-битной реализации LispWorks. Он использует заявления типа, встроенные объявления и директивы оптимизации. Вы видите, что мы должны сказать компилятору немного:
(defun foo-opt (x y)
(declare (optimize (speed 3) (safety 0) (debug 0) (fixnum-safety 0))
(inline +))
(declare (fixnum x y))
(the fixnum (+ x y)))
код (64-битный машинный код Intel) тогда очень мал и оптимизирован для того, что мы сказали компилятор:
0: 4157 push r15
2: 55 push rbp
3: 4889E5 moveq rbp, rsp
6: 4989DF moveq r15, rbx
9: 4803FE addq rdi, rsi
12: B901000000 move ecx, 1
17: 4889EC moveq rsp, rbp
20: 5D pop rbp
21: 415F pop r15
23: C3 ret
24: 90 nop
25: 90 nop
26: 90 nop
27: 90 nop
но имейте в виду, что выше код делает что-то отличное от того, что сделал бы интерпретатор или безопасный код:
- он вычисляет только fixnums
- он бесшумно переполняется
- результат также fixnum
- он не проверяет ошибки
- это не работает для других числовых типов данных
теперь неоптимизированный код:
0: 49396275 cmpq [r10+75], rsp
4: 7741 ja L2
6: 4883F902 cmpq rcx, 2
10: 753B jne L2
12: 4157 push r15
14: 55 push rbp
15: 4889E5 moveq rbp, rsp
18: 4989DF moveq r15, rbx
21: 4989F9 moveq r9, rdi
24: 4C0BCE orq r9, rsi
27: 41F6C107 testb r9b, 7
31: 7517 jne L1
33: 4989F9 moveq r9, rdi
36: 4C03CE addq r9, rsi
39: 700F jo L1
41: B901000000 move ecx, 1
46: 4C89CF moveq rdi, r9
49: 4889EC moveq rsp, rbp
52: 5D pop rbp
53: 415F pop r15
55: C3 ret
L1: 56: 4889EC moveq rsp, rbp
59: 5D pop rbp
60: 415F pop r15
62: 498B9E070E0000 moveq rbx, [r14+E07] ; SYSTEM::*%+$ANY-CODE
69: FFE3 jmp rbx
L2: 71: 41FFA6E7020000 jmp [r14+2E7] ; SYSTEM::*%WRONG-NUMBER-OF-ARGUMENTS-STUB
...
вы можете видеть, что это вызывает процедуру библиотеки для добавления. Этот код делает все, что сделал бы интерпретатор. Но ему не нужно интерпретировать исходный код Lisp. Он уже скомпилирован в соответствующие машинные инструкции.
почему компилируется Lisp-код быстро (er)?
Итак, почему скомпилирован Lisp-код быстро? Две ситуации:
неоптимизированный код Lisp: система выполнения Lisp оптимизирована для динамических структур данных, и коду не нужно толковаться
оптимизированный код Lisp: компилятор Lisp нуждается в информации или выводит ее и делает много работы, чтобы испустить оптимизированный машинный код.
как программист Lisp вы хотели бы работать с неоптимизированным, но скомпилированным кодом Lisp большую часть времени. Это достаточно быстро и предлагает много комфорта.
различные режимы выполнения предлагают выбор
как программист Lisp мы имеем выбор:
- интерпретируемый код: медленный, но простой в отладке
- скомпилированный код: быстрый во время выполнения, быстрая компиляция, много проверок компилятора, немного сложнее отлаживать, полностью динамический
- оптимизированный код: очень быстро во время выполнения, возможно, небезопасно во время выполнения, много шума компиляции о различных оптимизациях, медленная компиляция
обычно мы оптимизируем только те части кода, которым нужна скорость.
имейте в виду, что есть много ситуаций, когда даже хороший компилятор Lisp не может творить чудеса. Полностью универсальная объектно-ориентированная программа (с использованием общей объектной системы Lisp) почти всегда будет иметь некоторые накладные расходы (диспетчеризация на основе классов времени выполнения, ...).
динамически типизированные и динамические не то же самое
обратите внимание также, что динамической типизацией и динамический различные свойства языка программирования:
шепелявость динамической типизацией поскольку проверки типов выполняются во время выполнения, а переменные по умолчанию могут быть установлены для всех типов объектов. Для этого Lisp также нужны типы, прикрепленные к самим объектам данных.
шепелявость динамический потому что и язык программирования Lisp и сама программа могут быть изменены на среда выполнения: мы можем добавлять, изменять и удалять функции, мы можем добавлять, изменять или удалять синтаксические конструкции, мы можем добавлять, изменять или удалять типы данных (записи, классы, ...), мы можем изменить синтаксис поверхности Lisp различными способами и т. д. Это помогает, что Lisp также динамически типизируется для предоставления некоторых из этих функций.
пользовательский интерфейс: сборка и разборка
ANSI Common Lisp обеспечивает
- два стандартных функции для компиляции кода: compile и скомпилировать файл
- одна стандартная функция для загрузки исходного или скомпилированного кода:загрузить
- одна стандартная функция для того чтобы демонтировать код:разборки
компиляция-это простой перевод с одного языка на другой.
Если вы можете выразить то же самое на языке A
и язык B
, вы можете скомпилировать эту вещь, выраженную в языке A
в то же самое на языке B
.
как только вы выразили свое намерение на каком-либо языке, оно выполняется путем понял. Даже при выполнении C или некоторых других составлен язык, ваше заявление есть:
- в переводе с языка C - > Assembly
- в переводе с ассемблера - > машинный код
- интерпретируется машиной.
компьютер на самом деле является интерпретатором очень основной язык. Поскольку это так просто и так сложно работать, люди придумали другие языки, с которыми легче работать, и их можно легко перевести в эквивалентные операторы в машинном коде (например, C). Тогда, вы можете захватите фазу компиляции, выполнив перевод "на лету", как это делает JIT-компилятор, или написав свой собственный интерпретатор, который выполняет непосредственно оператор на вашем языке высокого уровня (например, LISP или Python).
но обратите внимание, что интерпретатор - это просто ярлык для прямого выполнения вашего кода ! Если вместо выполнения кода интерпретатор напечатал бы любой вызов, который он будет делать, будет ли он выполнять код, вы бы это сделали... компилятор. Конечно, это было бы очень глупый компилятор, и он не будет использовать большую часть информации, которую он имеет.
фактические компиляторы будут пытаться собрать как можно больше информации из весь программа перед генерацией кода. Например, следующий код:
const bool dowork = false;
int main() {
if (dowork) {
//... lots of code go there ...
}
return 0;
}
теоретически будет генерировать весь код внутри if
филиала. Но умный компилятор, вероятно, сочтет его недостижимым и просто проигнорирует его, используя тот факт, что он знает все в программа и знает, что dowork
всегда будет false
.
в дополнение к этому, некоторые язык типы, который может помочь отправить вызов функции, обеспечить некоторые вещи во время компиляции и помочь для перевода в машинный код. Некоторые языки, такие как C требуются программист для объявления типа своих переменных. Другие, такие как LISP и Python, просто определяют тип переменной, когда она установлена, и паникуют во время выполнения, если вы пытаетесь использовать значение определенный тип, если требуется другой тип (например, если вы пишете (car 2)
в большинстве интерпретаторов lisp это вызовет некоторую ошибку, сообщающую вам, что ожидается пара). Типы могут использоваться для выделения памяти во время компиляции (например, компилятор C выделит точно 10 * sizeof(int)
байты памяти, если требуется выделить int[10]
), но это не точно требуются. Фактически, большинство программ C используют указатели для хранения массивов, которые в основном являются динамическими. При работе с указателем, а компилятор создаст / ссылку на код, который во время выполнения выполнит необходимые проверки, перераспределения и т. д. Но суть в том, что динамические и обобщены не будете против. Интерпретаторы Python или Lisp являются скомпилированными программами, но все же могут действовать на динамические значения. На самом деле, сам язык ассемблера на самом деле не типизирован, так как компьютер может выполнять любую операцию над любым объектом, поскольку все, что он "видит", - это потоки битов и операции над битами. Языки более высокого уровня вводят произвольные типы и ограничения, чтобы сделать вещи более читаемыми и предотвратить вас от совершенно сумасшедших вещей. Но это просто помочь, не абсолютное требование.
теперь, когда философская тирада закончена, давайте посмотрим на ваш пример:
(setq x 60)
(setq y 40)
(+ x y)
и давайте попробуем скомпилировать это в действительную программу C. Как только это будет сделано, компиляторы C изобилуют, поэтому мы можем перевести LISP -> c -> machine language или почти все остальное. Имейте в виду, что компиляция только перевод (оптимизация тоже классная, но необязательная).
(setq
это выделяет значение. Но мы не знаем, что на что выделено. Давайте продолжим!--32-->
(setq x 60)
хорошо, мы выделяем 60 на x. 60 является целочисленным литералом, поэтому его тип C равен int
. Поскольку нет причин предполагать x
имеет другой тип, это эквивалентно C:
int x = 60;
аналогично (setq y 40)
:
int y = 40;
теперь мы есть:
(+ x y)
+
- это функция, которая, в зависимости от реализации, может иметь несколько типов аргументов, но мы знаем, что x
и y
- целые числа. Наши составители знают, что существует эквивалентный оператор C, который:
x + y;
поэтому мы просто переведем его. Наша окончательная программа C:
int x = 60;
int y = 40;
x + y;
что вполне допустимо программы C. Это может быть сложнее, чем это. Например, если x
и y
очень большой, большинство LISP не позволит им переполняться, а C будет, поэтому вы можете закодировать свой компилятор, чтобы иметь свой собственный целочисленный тип как массив ints (или все, что вы найдете уместным). Если вы можете определить общие операции (например,+
) на этих типах ваш новый компилятор, возможно, переведет предыдущий код в это:
int* x = newbigint("60");
int* y = newbigint("40");
addbigints(x, y);
С помощью функции newbigint
и addbigints
определяется в другом месте или генерируется компилятором. Он по-прежнему будет действительным C, поэтому он будет компилироваться. На самом деле, ваш собственный интерпретатор, вероятно, реализован на каком-то языке более низкого уровня и уже имеет представления для объектов LISP в своей собственной реализации, поэтому он может использовать их напрямую.
кстати, это именно то, что на Cython компилятор делает для кода Python:)
вы можете определить типы статически в Cython, чтобы получить дополнительную скорость/оптимизацию, но это не требуется. Cython может перевести ваш код Python непосредственно на C, а затем на компьютер код.
я надеюсь, что это делает его более четким ! Помните:
- весь код интерпретируется, в конечном счете
- Сост перевести код в то, что легче / быстрее интерпретировать. Они часто выполняют оптимизацию по пути, но это не является частью определения