Как Lisp может быть одновременно динамическим и компилируемым?

Итак, сначала, чтобы разобраться с этим: я прочитал следующий ответ:

Как Lisp динамичен и компилируется?

но я не совсем понимаю его ответ.

На таком языке, как Python, выражение:

x = a + b

На самом деле не может быть скомпилирован, поскольку для «компилятора» было бы невозможно узнать типы a и b (поскольку типы известны только во время выполнения), и, следовательно, как их добавить.

Это то, что делает такой язык, как Python, невозможным для компиляции без объявлений типов, верно? С объявлениями компилятор знает, что, например, a и b - целые числа, поэтому знает, как их складывать и переводить в собственный код.

Так как же:

(setq x 60)
(setq y 40)
(+ x y)

Работа?

Скомпилированный определяется как нативная предварительная компиляция.

ИЗМЕНИТЬ

На самом деле, этот вопрос больше о том, можно ли скомпилировать динамические языки без объявлений типов, и если да, то как?

ИЗМЕНИТЬ 2

После долгих исследований (например, активного просмотра Википедии) я думаю, что понял следующее:

  • языки с динамической типизацией - это языки, в которых типы проверяются во время выполнения
  • статические типизированные языки - это языки, в которых типы проверяются при компиляции программы
  • объявления типов позволяют компилятору сделать код более эффективным, потому что вместо постоянных вызовов API он может использовать больше собственных `` функций '' (вот почему вы можете добавить объявления типов в код Cython, чтобы ускорить его, но у вас нет к, потому что он все еще может просто вызывать библиотеки Python в коде C)
  • в Лиспе нет типов данных; следовательно, нет типов для проверки (тип - это сами данные)
  • Obj-C имеет как статические, так и динамические объявления; первые проверяются во время компиляции, вторые - во время выполнения

Поправьте меня, если я ошибаюсь по любому из вышеперечисленных пунктов.


person Aristides    schedule 21.08.2013    source источник
comment
И как Objective-C может быть динамичным и скомпилированным? Что ж ... динамизм против статичности и компилируемости не описывают одно и то же свойство. Язык может быть статически типизирован и скомпилирован (например, C), статически типизирован и интерпретирован (например, C ++, интерпретируемый Cling), динамически типизирован и скомпилирован (например, Objective-C, Lisp или JIT-ed JavaScript) и динамически типизирован и интерпретирован (например, Python, PHP, Lua, ...). Они действительно не имеют ничего общего друг с другом. Тот факт, что статическая типизация упрощает компилятору обнаружение ошибок и создание более эффективного кода, не имеет значения.   -  person    schedule 21.08.2013
comment
А как их добавить: полиморфизм. Компилятор генерирует код, который выполняет какие-то динамические уловки на основе (времени выполнения) типов a и b.   -  person    schedule 21.08.2013
comment
Тогда зачем скомпилированному Python аннотации типов? И разве у Obj-C нет аннотаций типов?   -  person Aristides    schedule 21.08.2013
comment
Objective-C не имеет аннотаций типов, но в нем есть объявления, как и в C. Действительно ли они ему нужны? Точно сказать не могу. Объекты Objective-C могут, по крайней мере, запрашиваться для их класса во время выполнения.   -  person    schedule 21.08.2013
comment
извини, это то, что я имел в виду   -  person Aristides    schedule 21.08.2013
comment
но как компилятор узнает типы переменных, чтобы иметь возможность переводить в соответствующий машинный код? Т   -  person Aristides    schedule 21.08.2013
comment
Он не знает типов (по крайней мере, в Objective-C). Ознакомьтесь с некоторыми статьями о полиморфизме.   -  person    schedule 21.08.2013
comment
Lisp по умолчанию не имеет типов данных для переменных, поэтому он не может их проверить. Сами данные содержат информацию о типе.   -  person Rainer Joswig    schedule 21.08.2013
comment
Утверждение, что в Лиспе нет типов данных, неверно. Существуют типы данных, но они являются свойством не переменных, а значений (на которые может ссылаться переменная). Например, если вы (let ((n 8))), то n не имеет типа, но значение 8, к которому n привязано в рамках этого let, имеет тип integer.   -  person Svante    schedule 22.08.2013
comment
@Aristides Компилятору не нужно знать об этом во время компиляции. Однако перед вычислением может потребоваться какой-то тип. Возможно, вас заинтересует 90-минутная схема компилятора C talk, который поставляется с кодом. Это не поэтапно и не так сложно, но довольно впечатляюще от Марка Фили. (автор Гамбита)   -  person Sylwester    schedule 22.08.2013
comment
@Aristides У Мэтта Майта тоже есть компилятор, в котором больше типов.   -  person Sylwester    schedule 22.08.2013


Ответы (2)


Пример кода:

(setq x 60)
(setq y 40)
(+ x y)

Выполнение с помощью интерпретатора Лиспа

В Лиспе на основе интерпретатора, описанном выше, будут данные Лиспа, а интерпретатор просматривает каждую форму и запускает вычислитель. Поскольку он запускает структуры данных Lisp, он будет делать это каждый раз, когда увидит приведенный выше код

  • получить первую форму
  • у нас есть выражение
  • это специальная форма SETQ
  • оцените 60, результат 60
  • найдите место для переменной x
  • установите переменную x на 60
  • получить следующую форму ... ...
  • у нас есть вызов функции +
  • оценить x -> 60
  • оценить y -> 40
  • вызовите функцию + с 60 и 40 -> 100 ...

Теперь + - это некий фрагмент кода, который действительно узнает, что делать. Lisp обычно имеет разные числовые типы, и (почти) ни один процессор не поддерживает все из них: fixnums, bignums, ratios, complex, float, ... Таким образом, функция + должна выяснить, какие типы имеют аргументы и что она может сделать с добавить их.

Выполнение с помощью компилятора Lisp

Компилятор просто испускает машинный код, который будет выполнять операции. Машинный код будет делать все, что делает интерпретатор: проверять переменные, проверять типы, проверять количество аргументов, вызывать функции и т. д. .

Если вы запустите машинный код, он будет намного быстрее, поскольку выражения Лиспа не нужно рассматривать и интерпретировать. Интерпретатору потребуется декодировать каждое выражение. Компилятор это уже сделал.

Он по-прежнему медленнее, чем некоторый код C, поскольку компилятор не обязательно знает типы и просто генерирует полностью безопасный и гибкий код.

Таким образом, этот скомпилированный код Lisp намного быстрее, чем интерпретатор, выполняющий исходный код Lisp.

Использование оптимизирующего компилятора Lisp

Иногда этого недостаточно. Затем вам нужен лучший компилятор и сообщите компилятору Lisp, что он должен приложить больше усилий для компиляции и создать оптимизированный код.

Компилятор Lisp может знать типы аргументов и переменных. Затем вы можете указать компилятору не проверять время выполнения. Компилятор также может предположить, что + - это всегда одна и та же операция. Таким образом, он может встроить необходимый код. Поскольку он знает типы, он может генерировать код только для этих типов: сложение целых чисел.

Но все же семантика Lisp отличается от C или машинных операций. A + не только работает с различными типами чисел, он также автоматически переключается с малых целых чисел (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   

Но имейте в виду, что приведенный выше код делает нечто иное, чем интерпретатор или безопасный код:

  • он рассчитывает только фиксированные числа
  • он бесшумно переполняется
  • результат тоже фикс
  • он не проверяет ошибки
  • он не работает для других числовых типов данных

Теперь неоптимизированный код:

       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 компилируется быстро? Две ситуации:

  • неоптимизированный код Lisp: система времени выполнения Lisp оптимизирована для динамических структур данных, и код не нужно интерпретировать

  • оптимизированный код Лиспа: компилятор Лиспа нуждается в информации или выводит ее и делает много работы для создания оптимизированного машинного кода.

Как программист на Лиспе вы хотели бы большую часть времени работать с неоптимизированным, но скомпилированным кодом Лиспа. Он достаточно быстрый и предлагает много комфорта.

Различные режимы выполнения предлагают выбор

У программиста на Лиспе есть выбор:

  • интерпретируемый код: медленно, но легче всего отлаживать
  • скомпилированный код: быстро во время выполнения, быстрая компиляция, много проверок компилятора, немного сложнее отлаживать, полностью динамический
  • оптимизированный код: очень быстро во время выполнения, возможно, небезопасно во время выполнения, много шума компиляции из-за различных оптимизаций, медленная компиляция

Обычно мы оптимизируем только те части кода, которым нужна скорость.

Имейте в виду, что во многих ситуациях даже хороший компилятор Lisp не может творить чудеса. Полностью универсальная объектно-ориентированная программа (использующая объектную систему Common Lisp) почти всегда будет иметь некоторые накладные расходы (диспетчеризация на основе классов времени выполнения, ...).

Динамический тип и динамический - не одно и то же

Также обратите внимание, что динамически типизированный и динамический - это разные свойства языка программирования:

  • Lisp динамически типизирован, потому что проверки типов выполняются во время выполнения, а переменные по умолчанию могут быть установлены для всех типов объектов. Для этого Лиспу также нужны типы, прикрепленные к самим объектам данных.

  • Lisp является динамическим, потому что и язык программирования Lisp, и сама программа могут быть изменены во время выполнения: мы можем добавлять, изменять и удалять функции, мы можем добавлять, изменять или удалять синтаксические конструкции, мы можем добавлять, изменять или удалить типы данных (записи, классы, ...), мы можем изменить поверхностный синтаксис Лиспа различными способами и т. д. Помогает то, что Лисп также динамически типизирован для обеспечения некоторых из этих функций.

Пользовательский интерфейс: компиляция и разборка

ANSI Common Lisp предоставляет

  • две стандартные функции для компиляции кода: compile и файл компиляции
  • одна стандартная функция для загрузки исходного или скомпилированного кода: load
  • одна стандартная функция для дизассемблирования кода: дизассемблировать
person Rainer Joswig    schedule 21.08.2013
comment
TL; DR: Абзац №4 - это суть. - person ; 21.08.2013
comment
'ответил 18 минут назад'? Блин, я медленная машинистка :) Очень красивое объяснение! - person val; 21.08.2013
comment
Поскольку я не видел ссылок в ответе, обратите внимание, что в Common Lisp вы можете скомпилировать код с помощью _ 1_ и исследуйте результат с помощью disassemble . - person Joshua Taylor; 21.08.2013

Компиляция - это простой перевод с одного языка на другой. Если вы можете выразить то же самое на языке A и языке B, вы можете скомпилировать это, выраженное на языке A, в то же самое на языке B.

После того, как вы выразили свое намерение на каком-либо языке, оно выполняется путем интерпретации. Даже при использовании C или какого-либо другого компилируемого языка ваша инструкция:

  1. Переведено с C -> язык ассемблера
  2. В переводе с ассемблера -> машинный код
  3. Интерпретирует машина.

Компьютер на самом деле является интерпретатором очень базового языка. Поскольку он настолько прост и с ним так сложно работать, люди придумали другие языки, с которыми легче работать и которые можно легко перевести в эквивалентные операторы в машинном коде (например, 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 -> машинный язык или что-нибудь еще. Имейте в виду, что компиляция - это всего лишь перевод (оптимизация тоже крутая, но необязательная).

(setq 

Это присвоить значение. Но мы не знаем, что на что отведено. Давай продолжим

(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 будет, поэтому вы можете закодировать свой компилятор так, чтобы он имел собственный целочисленный тип в виде массива целых чисел (или того, что вы сочтете подходящим). Если вы можете определять общие операции (например, +) для этих типов, ваш новый компилятор, возможно, вместо этого переведет предыдущий код в этот:

int* x = newbigint("60");
int* y = newbigint("40");
addbigints(x, y);

С вашими функциями newbigint и addbigints, определенными в другом месте или сгенерированными компилятором. Он по-прежнему будет действительным C, поэтому он будет компилироваться. Фактически, ваш собственный интерпретатор, вероятно, реализован на каком-то языке более низкого уровня и уже имеет представления для объектов LISP в своей собственной реализации, поэтому он может использовать их напрямую.

Кстати, именно это и делает компилятор Cython для кода Python :)

Вы можете определять типы статически в Cython, чтобы получить дополнительную скорость / оптимизацию, но это не требуется. Cython может переводить ваш код Python непосредственно на C, а затем в машинный код.

Надеюсь, это проясняет! Помните:

  1. ВСЕ код интерпретируется, в конечном итоге
  2. Компиляторы переводят код во что-то, что легче / быстрее интерпретировать. Они часто проводят оптимизацию в процессе, но это не часть определения.
person val    schedule 21.08.2013
comment
(Примечание: большинство языков C'ish, включая C ++ и C # (те, которые мне известны, определяют как bool, так и const), зарезервировано do в качестве ключевого слова. Пример int main(), вероятно, не будет компилироваться. Не имеет смысла хотя менее актуален.) - person cHao; 21.08.2013