LINUX.ORG.RU

Оптимизации в Scheme

 ,


0

5

Всем привет!
Продолжаю пилить свою имплементацию Scheme R5RS на Java. В качестве тестов беру примеры кода из Интернета.
Где-то месяц назад взял следующий замечательный пример: https://rosettacode.org/wiki/Integer_roots#Scheme

Тут и internal definitions, и много вычислений, и большие числа, и рекурсия.
Изначально оно падало почти мгновенно со StackOverflowError (что логично). Реализовал tail-call optimizations, падать перестало. Теперь результат немного не совпадает из-за того, что в некоторых местах теряется точность (вообще, числа в Java реализованы кривовато ИМХО), но это ладно. Еще оно работает существенно медленнее Racket и Guile.
Я, конечно, и не ожидал супер производительности, но всё же решил запустить профайлер и по возможности увеличить производительность.

В результате работы профайлера выяснилось, что как минимум 2 вещи существенно снижают производительность:
1) Вызовы процедур
2) Частые environment lookups

1) Вызовы процедур

Одно из очевидных решений - инлайнить некоторые процедуры.
Так, например, делают и Racket, и Guile.

Что-то вроде:

(define (test n) (zero? n)) => (define (test n) (= n 0))

Попытался это реализовать, вроде, получилось, но как-то кривовато.

Вопрос:
Есть ли какие-нибудь алгоритмы для этого дела?
Как определить когда можно инлайнить, а когда нельзя?

Например:

(define (t n)
  (zero? n)   ;; можно инлайнить
  '(zero? n)) ;; нельзя инлайнить

Можно ли как-то статически определить, что перед нами вызов функции, а не quoted form или еще что-нибудь?

В данный момент я просто рекурсивно пробегаю по всем формам в теле функции, и если какой-нибудь элемент совпадает с телом функции, которую можно инлайнить, то я подменяю вызов функции на ее тело (попутно подставляя правильные имена аргументов). Все quoted/quasiquoted формы пропускаю.
Оно, вроде, и работает, но не уверен, что оно будет работать всегда правильно. Плюс, каждый раз приходится создавать кучу новых объектов, потому что списки в Java копируются как shallow copies, а мне здесь всегда нужна deep copy (потому что я не могу менять тело той функции, которую инлайню).

Racket пишут, что они это делают на этапе компиляции - как они это делают?
У меня компиляции нет, ее заменяет вычисление lambda.

2) Частые environment lookups

На пальцах я понимаю, почему оно работает не так шустро и чисто теоретически представляю, как это исправить. На практике же ничего не получается.

Допустим, есть простая функция:

(define (perf n)
  (if (zero? n)
    "DONE"
    (perf (- n 1))))

У меня, когда вычисляется ее тело, то evaluator на каждой итерации постоянно забирает из окружения символы if, zero?, n и perf. Более того, для всех символов, кроме perf, он всегда делает по 2 lookup'а: сначала пытается найти в локальном окружении, не находит, потом поднимается выше и находит их в глобальном окружении.
Итого, даже если вычислить (perf 1), то lookups будут примерно такие (воспроизвожу по памяти):

Lookups:

perf
if (miss!)
if
zero? (miss!)
zero?
n
perf
- (miss!)
n
if (miss!)
if
zero? (miss!)
zero?
n

(здесь я еще опускаю lookups, которые делаются для тела zero?: = и n)

Видно, что многие lookups являются лишними и можно бы их прокешировать. Но как?
Во-первых, что и как кешировать? Во-вторых, как при этом не поломать lexical scope?

★★★★★

Можно ли как-то статически определить, что перед нами вызов функции, а не quoted form или еще что-нибудь?

У тебя же первым этапом должен быть синтаксический разбор. После которого '(zero? n) станет (syntax-literal (list 'zero? 'n)), а (zero? n) станет (list (free-binding 'zero?) (local-binding 'n)). Соответственно, free-binding на константу (нет строк с set!) можно инлайнить.

monk ★★★★★
()

Racket пишут, что они это делают на этапе компиляции - как они это делают?
У меня компиляции нет, ее заменяет вычисление lambda.

В смысле, у тебя форма define интерпретируется и на выходе даёт функцию Java? Тогда за тебя JVM JIT по идее должен инлайн делать где надо. Или «вычисление lambda» = запоминание куска кода и интерпретация при запуске?

monk ★★★★★
()
Последнее исправление: monk (всего исправлений: 1)

lookups будут примерно такие (воспроизвожу по памяти)

Какой ужас! Это совсем интерпретатор.

Видно, что многие lookups являются лишними и можно бы их прокешировать. Но как?

Во-первых, что и как кешировать? Во-вторых, как при этом не поломать lexical scope?

При чтении кода преобразуй его в синтаксические объекты.

Если же принципиально не хочешь делать компилятор (интерпретация проще), то делай для каждой функции кэш {perf => { if => ... , zero? => ...}}. При переопределении функции кэш сбрасываешь, при выполнении смотришь, если есть кэш, то сразу загружаешь локальные привязки. Кэщ имя-функции => привязки лучше сделать хэшем. Привязки лучше сделать просто списком пар (так как при входе в функцию тебе надо читать весь список).

monk ★★★★★
()

Не сочти за самопиар, но вот: http://ilammy.github.io/lisp/ — там куча хорошего вводного материала по тому, как делать реализации схемки. В том числе как ускорить наивную интерпретацию.

ilammy ★★★
()
Ответ на: комментарий от ilammy

Спасибо! Выглядит интересно! Только поищу оригинал.

kovrik ★★★★★
() автор топика
Ответ на: комментарий от monk

Да, интерпретатор (пока что).
Стандартные процедуры и special forms реализованы на Java, а пользовательские процедуры интерпретируются.

Насчет синтаксических объектов почитаю, попробую реализовать.

Если же принципиально не хочешь делать компилятор

Если честно, то просто не задумывался об этом. Пишу на коленке, просто хочу реализовать R5RS (максимально близко).
Ну и пока слабо себе представляю как его правильно реализовать.
Посоветуй, пожалуйста, литературу/статьи?

kovrik ★★★★★
() автор топика
Ответ на: комментарий от monk

При чтении кода преобразуй его в синтаксические объекты.

Я по сути так и делаю:
"(if (zero? n) 1 0)" после reader'а превращается в список:

(SCMSymbol<if>
 (SCMSymbol<zero?>, SCMSymbol<n>),
 1L,
 0L)

Потом попадает в evaluator, который...интерпретирует.

Правильно ли я тебя понимаю, что после reader'а, но перед evaluator'ом, можно это все превратить в:
(SpecialForm<if>
 (Procedure<zero?>, SCMSymbol<n>),
 1L,
 0L)

И уже сразу выполнять, не делая никаких lookups?

kovrik ★★★★★
() автор топика
Ответ на: комментарий от kovrik

Окей, сделал так, что теперь Special Forms инлайнятся, теперь намного меньше lookups. Строки, числа, chars, booleans, списки, векторы - они и так не оборачиваются никак, а остаются as-is.
Остаются только пользовательские процедуры, но их инлайнить нельзя, так?

kovrik ★★★★★
() автор топика
Ответ на: комментарий от kovrik

И уже сразу выполнять, не делая никаких lookups?

Да. Именно так. Если будешь делать макросы, то они должны тоже выполняться сразу после чтения (им на вход синтаксическую структуру и на выход её же).

(Procedure<zero?>, SCMSymbol<n>),

Правильно (Procedure<zero?>, LocalVar<n, perf>). Для LocalVar второй параметр — место инициализации. Чтобы let работал. Для let можешь номер буквы в коде или что-то подобное использовать как идентификатор.

В (Procedure<zero?>, SCMSymbol<n>) будет превращаться (zero? 'n).

monk ★★★★★
()
Ответ на: комментарий от kovrik

Остаются только пользовательские процедуры, но их инлайнить нельзя, так?

Почему? Можно. Но надо проверять, что она не меняется. То есть, если в коде нет (set! perf ...), то можно подставлять вместо вызова тело процедуры в виде (let ...). Хотя рекурсивные, а тем более хвостово рекурсивные вызовы смысла инлайнить нету.

monk ★★★★★
()
Ответ на: комментарий от monk

Ага, понял, попробую так сделать.
Спасибо большое!

А какие еще стандартные оптимизации можно реализовать?
Посмотрел в Racket Docs про Performance, но большинство из того, что там указано к моей имплементации неприменимы.

kovrik ★★★★★
() автор топика
Последнее исправление: kovrik (всего исправлений: 1)
Ответ на: комментарий от monk

Правильно (Procedure<zero?>, LocalVar<n, perf>)

Т.е. Ридер должен всегда при себе иметь ссылку на текущий environment?

kovrik ★★★★★
() автор топика
Ответ на: комментарий от kovrik

А какие еще стандартные оптимизации можно реализовать?

constant propagation, constant folding, inlining, and dead-code elimination (с) Racket Docs про Performance

Всё это вполне реализуемо.

constant propagation — если есть константа, то внутри функций её можно заменить на её значение

constant folding — если есть чистая (результат зависит только от параметров, нет обращения к внешним ресурсам) функция и её параметры константны, её сразу можно вычислить.

inlining — уже обсудили

dead-code elimination — если есть чистая функция и её результат не используется, его можно не вычислять.

Достаточно записать, какие базовые функции чистые и считать, что если в функции используются только чистые функции, то она тоже чистая.

monk ★★★★★
()
Ответ на: комментарий от kovrik

Т.е. Ридер должен всегда при себе иметь ссылку на текущий environment?

Должно быть так: https://docs.racket-lang.org/reference/syntax-model.html

Ридер должен сформировать и записать в некую структуру привязки (какая переменная какому аргументу функции или глобальной переменной или параметру let соответствует). Значений в ридере нет. environment — это всё-таки соответствие переменных и их значений.

Можно на это дело забить и работать со списками символов (в большинстве интерпретаторов Common Lisp так и сделано). Но будет медленнее и, самое главное, макросы с гигиеной будет реализовывать неимоверно трудно.

monk ★★★★★
()
Ответ на: комментарий от monk

Ух, попробую что-нибудь из этого реализовать. Спасибо огромное!
Надо теперь это все дело переварить.

kovrik ★★★★★
() автор топика

Продолжаю пилить свою имплементацию Scheme R5RS

Когда пишешь про scheme, убери из тагов lisp. Связи никакой. И вместо «пилить свой scheme» почитай историю, литературу, что угодно про Common Lisp. Сохранишь себе много лет жизни.

The21century
()
Ответ на: комментарий от The21century

scheme, убери из тагов lisp. Связи никакой.

Scheme — это правильный лисп.

почитай историю, литературу, что угодно про Common Lisp. Сохранишь себе много лет жизни.

Если бы в Common Lisp осилили модули и call/cc, было бы о чём говорить. Очень сложно писать крупные проекты, не переписав половину CL (см. hu.dwim.*). Хотя для прототипирования язык неплохой.

Scheme даёт правильные навыки программирования (модули, функции как аргументы, нетекущие абстракции). Racket позволяет легко и удобно проектировать и реализовыать крупные программные продукты

monk ★★★★★
()
Ответ на: комментарий от monk

Scheme — это правильный лисп
Scheme даёт правильные навыки программирования

https://groups.google.com/forum/#!msg/comp.lang.lisp/Bj8Hx6mZEYI/6AWmNEwQR5YJ

«Even before L&FP broke up, it got to the point that when there was a Lisp paper, all the Scheme folks left the room and when there as a Scheme paper all the Lisp folks left the room.»

Есть лисперы. Есть (почему-то, я не знаю почему) схемеры. Им не понять друг друга. И еще раз:

Scheme — это правильный лисп

scheme ни разу не лисп.

The21century
()
Ответ на: комментарий от ilammy

Не сочти за самопиар

Капец ты скромный. Избавляйся от этой своей черты. Ты молодец)

Bad_ptr ★★★★★
()
Ответ на: комментарий от Oxdeadbeef

Да не говори. Около-лисп сумасшедшие это бред в квадрате. Скопировали скобки и «у нас лучший lisp чем lisp». Куда мир катится?)

The21century
()
Ответ на: комментарий от monk

!>Тогда C++ ни разу не C, а Qt ни разу не C++.

Ты не поверишь...

И да, схема не лисп!

anonymous
()
Ответ на: комментарий от The21century

Scheme is a functional programming language and one of the two main dialects of the programming language Lisp.

/thread

kovrik ★★★★★
() автор топика

прости, не удержался

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

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

* вызов профайлера

:-)

anonymous
()

Видно, что многие lookups являются лишними и можно бы их прокешировать. Но как?
Во-первых, что и как кешировать? Во-вторых, как при этом не поломать lexical scope?

ты CPS преобразование-то делаешь? заодно многое может и прокешироваться

anonymous
()
Ответ на: комментарий от anonymous

Не везде, но да, делаю. Пока что только для TCO.

kovrik ★★★★★
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.