Реализация рекурсии в компиляторах ФЯ
От: mik1  
Дата: 20.03.06 11:17
Оценка:
Всем доброго времени суток!

Моя диссертация напрямую связана с одним "домашним" ФЯ. Сейчас пробую создать экспериментальную версию компилятора для него. Сам компилятор генерит код на Java. Всё бы хорошо с создаваемым кодом, но есть одна проблема — РЕКУРСИИ. Функциональные языки, как известно, построены, главным образом, на рекурсивных функциях. Но как быть с рекурсией, если при даже небольших глубинах рекурсии (~1000 уровней) получается переполнение стека?
В интерпретаторах это лечится просто — вызвали первую функцию, интерпретируем ее код, встретили в нем вызов другой функции, выполнение первой функции приостановили, стали выполнять вторую функцию и т.д. Рекурсии как таковой нет.
Если же мы генерим код на императивном языке, то получаем классическую рекурсию.
Пока единственный видимый мне вариант состоит в генерации кода, который бы напоминал интерпретатор. То есть каждый вызов невстроенной функции оборачивать в код, который бы либо получал результат уже вычисленной функции, либо выходил из данной функции в вызывающий ее код "интерпретатора".

Может быть уважаемый All подскажет более красивые методы решения проблемы рекурсии в компиляторах ФЯ?
Re: Реализация рекурсии в компиляторах ФЯ
От: Курилка Россия http://kirya.narod.ru/
Дата: 20.03.06 11:24
Оценка: 2 (1) +2
Здравствуйте, mik1, Вы писали:


M>Может быть уважаемый All подскажет более красивые методы решения проблемы рекурсии в компиляторах ФЯ?


Про хвостовую рекурсию ты слыхал?
Re[2]: Реализация рекурсии в компиляторах ФЯ
От: mik1  
Дата: 20.03.06 11:35
Оценка:
Здравствуйте, Курилка, Вы писали:

К>Здравствуйте, mik1, Вы писали:


M>>Может быть уважаемый All подскажет более красивые методы решения проблемы рекурсии в компиляторах ФЯ?


К>Про хвостовую рекурсию ты слыхал?


Либо я про нее не все слышал, либо она всё-таки не всюду может помочь.
Например, с числами Фиббоначчи.

int fib(i)
{
if (i==0 || i==1)
return 1;
return fib(i-1) + fib(i-2);
}

Как здесь может помочь хвостовая рекурсия — не очень понимаю...
Re[3]: Реализация рекурсии в компиляторах ФЯ
От: Курилка Россия http://kirya.narod.ru/
Дата: 20.03.06 11:43
Оценка: 6 (1)
Здравствуйте, mik1, Вы писали:

M>Здравствуйте, Курилка, Вы писали:


К>>Здравствуйте, mik1, Вы писали:


К>>Про хвостовую рекурсию ты слыхал?


M>Либо я про нее не все слышал, либо она всё-таки не всюду может помочь.

Никто и не говорит про серебрянную пулю, но вот помогает она очень много где
M>Например, с числами Фиббоначчи.

...

M>Как здесь может помочь хвостовая рекурсия — не очень понимаю...

Поиском пользоваться не пытался ? Вот первая же ссылка с гугла:
http://triton.towson.edu/~akayabas/COSC455_Spring2000/Recursion_Iteration.htm
Фибоначчи преобразуется в хвостовую рекурсию довольно просто
Re[4]: Реализация рекурсии в компиляторах ФЯ
От: mik1  
Дата: 20.03.06 11:53
Оценка:
К>>>Про хвостовую рекурсию ты слыхал?

M>>Либо я про нее не все слышал, либо она всё-таки не всюду может помочь.

К>Никто и не говорит про серебрянную пулю, но вот помогает она очень много где
M>>Например, с числами Фиббоначчи.

К>...


M>>Как здесь может помочь хвостовая рекурсия — не очень понимаю...

К>Поиском пользоваться не пытался ? Вот первая же ссылка с гугла:
К>http://triton.towson.edu/~akayabas/COSC455_Spring2000/Recursion_Iteration.htm
К>Фибоначчи преобразуется в хвостовую рекурсию довольно просто

Мда, похоже я был слегка не прав.
Спасибо.
Re: Реализация рекурсии в компиляторах ФЯ
От: Трурль  
Дата: 20.03.06 11:55
Оценка:
Здравствуйте, mik1, Вы писали:

M>В интерпретаторах это лечится просто — вызвали первую функцию, интерпретируем ее код, встретили в нем вызов другой функции, выполнение первой функции приостановили, стали выполнять вторую функцию и т.д. Рекурсии как таковой нет.


А куда же она делась?
Re[2]: Реализация рекурсии в компиляторах ФЯ
От: mik1  
Дата: 20.03.06 12:03
Оценка: +1
Здравствуйте, Трурль, Вы писали:

Т>Здравствуйте, mik1, Вы писали:


M>>В интерпретаторах это лечится просто — вызвали первую функцию, интерпретируем ее код, встретили в нем вызов другой функции, выполнение первой функции приостановили, стали выполнять вторую функцию и т.д. Рекурсии как таковой нет.


Т>А куда же она делась?


Пусть мы (интерпретатор) — есть как бы процессор. Мы идем по коду функции, выполняем условные операторы, арифметику и прочие ВСТРОЕННЫЕ функции (будем считать их командами процессора). Теперь встретили вызов функции. Мы сохраняем текущий контекст в массив (а его то размеры у нас ничем не ограничены, в отличии от стека!) и начинаем исполнять эту вызываемую функцию. Ну и так далее. Получив результат вызываемой функции мы ищем то место, где мы прервали вычисления и идем выполнять нашу первую функцию дальше. Картина огрубленная, но работает все примерно так.
Никаким стеком здесь и не пахнет, как видите.
Re[3]: Реализация рекурсии в компиляторах ФЯ
От: Serginio1 СССР https://habrahabr.ru/users/serginio1/topics/
Дата: 20.03.06 12:29
Оценка:
Здравствуйте, mik1, Вы писали:

M>Здравствуйте, Трурль, Вы писали:


Т>>Здравствуйте, mik1, Вы писали:


M>>>В интерпретаторах это лечится просто — вызвали первую функцию, интерпретируем ее код, встретили в нем вызов другой функции, выполнение первой функции приостановили, стали выполнять вторую функцию и т.д. Рекурсии как таковой нет.


Т>>А куда же она делась?


M>Пусть мы (интерпретатор) — есть как бы процессор. Мы идем по коду функции, выполняем условные операторы, арифметику и прочие ВСТРОЕННЫЕ функции (будем считать их командами процессора). Теперь встретили вызов функции. Мы сохраняем текущий контекст в массив (а его то размеры у нас ничем не ограничены, в отличии от стека!)

Ограничена свободной памятью Хранить состояние в стеке или массиве небольшая разница, но принципиальна в доступе, т.к. при обращении к массиву нужно вводить дополнительную коственность, для изменения размера и соответственно скоростью доступа.
В принципе любая рекурсия обходится ручным хранением состояний, что не есть удобно, но это суть одного итого же просто под стеком понимается динамическая память
и солнце б утром не вставало, когда бы не было меня
Re[4]: Реализация рекурсии в компиляторах ФЯ
От: mik1  
Дата: 20.03.06 12:37
Оценка: +1
M>>>>В интерпретаторах это лечится просто — вызвали первую функцию, интерпретируем ее код, встретили в нем вызов другой функции, выполнение первой функции приостановили, стали выполнять вторую функцию и т.д. Рекурсии как таковой нет.

Т>>>А куда же она делась?


M>>Пусть мы (интерпретатор) — есть как бы процессор. Мы идем по коду функции, выполняем условные операторы, арифметику и прочие ВСТРОЕННЫЕ функции (будем считать их командами процессора). Теперь встретили вызов функции. Мы сохраняем текущий контекст в массив (а его то размеры у нас ничем не ограничены, в отличии от стека!)

S>Ограничена свободной памятью Хранить состояние в стеке или массиве небольшая разница, но принципиальна в доступе, т.к. при обращении к массиву нужно вводить дополнительную коственность, для изменения размера и соответственно скоростью доступа.
S>В принципе любая рекурсия обходится ручным хранением состояний, что не есть удобно, но это суть одного итого же просто под стеком понимается динамическая память

Попробуйте в Java сделать размер стека потока хотя бы в 100 мег (если у Вас памяти просто жутко много — у меня 1.5 гиг, то поставьте чуть больше).
Тогда выясняется неприятный факт, что в Java размер стека выставляется один и тот же для всех потоков. А их бывает немало. Так что лучше иметь вектор с контекстами в ОДНОМ месте, чем кучу потоков с огромным, но НЕНУЖНЫМ им стеком. Да и ручное сохранение контекста банально меньше памяти кушать может, чем сохранение контекста в рекурсивных функциях.
Конкретно в нашей реализации используется докрученный для наших целей дек. Работает достаточно быстро.
Re[5]: Реализация рекурсии в компиляторах ФЯ
От: Serginio1 СССР https://habrahabr.ru/users/serginio1/topics/
Дата: 20.03.06 13:54
Оценка:
Здравствуйте, mik1, Вы писали:

Сразу оговорюсь что с самим подходом полностью согласен. Кстати в NET 2 для yield (рекурсивный обход) используют список классов, вместо фиберов использующие стек.

M>Попробуйте в Java сделать размер стека потока хотя бы в 100 мег (если у Вас памяти просто жутко много — у меня 1.5 гиг, то поставьте чуть больше).


M>Тогда выясняется неприятный факт, что в Java размер стека выставляется один и тот же для всех потоков. А их бывает немало. Так что лучше иметь вектор с контекстами в ОДНОМ месте, чем кучу потоков с огромным, но НЕНУЖНЫМ им стеком. Да и ручное сохранение контекста банально меньше памяти кушать может, чем сохранение контекста в рекурсивных функциях.

M>Конкретно в нашей реализации используется докрученный для наших целей дек. Работает достаточно быстро.

Все от задач, просто суть одна и таже, но у каждого подхода свои премущества и недостатки. Хотя в целом я за динамический стек
и солнце б утром не вставало, когда бы не было меня
Re[4]: Реализация рекурсии в компиляторах ФЯ
От: Serginio1 СССР https://habrahabr.ru/users/serginio1/topics/
Дата: 20.03.06 15:16
Оценка: :)
Здравствуйте, Курилка, Вы писали:

Лучше уж эта http://am.rusimport.ru/MsAccess/topic.aspx?ID=266
и солнце б утром не вставало, когда бы не было меня
Re[5]: Реализация рекурсии в компиляторах ФЯ
От: mik1  
Дата: 21.03.06 07:11
Оценка:
Здравствуйте, Serginio1, Вы писали:

S>Здравствуйте, Курилка, Вы писали:


S>Лучше уж эта http://am.rusimport.ru/MsAccess/topic.aspx?ID=266


На самом деле одно не лучше другого. Пока мы имеем чистую хвостовую рекурсию, то ее переписать в цикл несложно. Но как только алгоритм у нас усложняется, то становится тошно. Ради примера — попробуйте понять закономерность для трансформации функции fib(n) в ту функцию, то была на приведенной Курилкой ссылке...
Re[6]: Реализация рекурсии в компиляторах ФЯ
От: Serginio1 СССР https://habrahabr.ru/users/serginio1/topics/
Дата: 21.03.06 10:07
Оценка:
Здравствуйте, mik1, Вы писали:
Есть алгоритмы чисто рекурсивные требующие своего стека. В большинстве случаев избавление от использования програмного стека это содание экземпляра класса и инициализация в каждой процедуре.

Для примера


public class Factorial
{ 
   private int _n
  public Factorial(n)
{ _n=n}

    public int fac()
{
    if _n<=0 return 1;
    Factorial factorial =new Factorial(n-1);

   return _n*factorial.fac()

}
}
и солнце б утром не вставало, когда бы не было меня
Re[6]: Реализация рекурсии в компиляторах ФЯ
От: z00n  
Дата: 21.03.06 10:39
Оценка:
Здравствуйте, mik1, Вы писали:

M>На самом деле одно не лучше другого. Пока мы имеем чистую хвостовую рекурсию, то ее переписать в цикл несложно. Но как только алгоритм у нас усложняется, то становится тошно. Ради примера — попробуйте понять закономерность для трансформации функции fib(n) в ту функцию, то была на приведенной Курилкой ссылке...


На самом деле, закономерность несложная и по сути напоминает CPS: сделать и результат и все промежуточные результататы аргументами функции:

-- рекурсивно:
fact(n)
  if (n == 1)
    return 1;
  return n * fact(n - 1);

-- хвосто-рекурсивно
fact(n, result)
  if (n == 0)
    return result;
  return fact((n - 1), (n * result));

С другой стороны, мне кажется, что вряд ли есть компиляторы, которые сами преобразуют древовидную рекурсию в хвостовую, как в случае с Fib. Обычно от компиляторов требуют лишь умения правильно работать с хвостовой.
Еще можно попробовать мемоизацию.
Re[7]: Реализация рекурсии в компиляторах ФЯ
От: mik1  
Дата: 21.03.06 11:19
Оценка:
Здравствуйте, z00n, Вы писали:

Z> С другой стороны, мне кажется, что вряд ли есть компиляторы, которые сами преобразуют древовидную рекурсию в хвостовую, как в случае с Fib. Обычно от компиляторов требуют лишь умения правильно работать с хвостовой.

Z> Еще можно попробовать мемоизацию.

Мемоизацию мы пробовали использовать в интерпретаторе.
Он с рекурсией она плохой помощник.

Пусть у нас есть функция

int foo(int n)
{
if (n < 0) return n;
return foo(n-1);
}

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

Другое дело, если аргумент у нас растет между вызовами. Тогда вот мемоизация уже полезна (да и то не всегда — см. дальше).

У нас есть один из тестов.
Там числа представляются в таком виде:

0 = NUL
1 = NUL.SUCC
2 = NUL.SUCC.SUCC
ну и т.д.

Вот в этой арифметике мы считаем числа Фибоначчи. Допустимая глубина рекурсии в процедуре там порядка 1500 уровней.
Сама процедура переводит число в его вышеупомянутое представление.
Вызывается она на каждое из чисел Фибоначчи. Так вот, как только разница между fib(i) и fib(i+1) превысит эти вышеупомянутые 1500, то мы опять нарвемся на стек. Даже не смотря на то, что мы помним все прошлые результаты. Такая вот петрушка.
Re[7]: Реализация рекурсии в компиляторах ФЯ
От: mik1  
Дата: 21.03.06 11:45
Оценка:
Здравствуйте, Serginio1, Вы писали:

S>Здравствуйте, mik1, Вы писали:

S> Есть алгоритмы чисто рекурсивные требующие своего стека. В большинстве случаев избавление от использования програмного стека это содание экземпляра класса и инициализация в каждой процедуре.

S>Для примера


S>
S>public class Factorial
S>{ 
S>   private int _n
S>  public Factorial(n)
S>{ _n=n}

S>    public int fac()
S>{
S>    if _n<=0 return 1;
S>    Factorial factorial =new Factorial(n-1);

S>   return _n*factorial.fac()

S>}
S>}
S>



Ээээ... Тут стек очень даже используется. Да еще и покрепче, чем в лоб писать (Object + int-поле вместо int-локальной переменной)...
Вот если бы каждый объект факториала в новом потоке считался, тогда да...
Re[8]: Реализация рекурсии в компиляторах ФЯ
От: Serginio1 СССР https://habrahabr.ru/users/serginio1/topics/
Дата: 21.03.06 12:04
Оценка: 2 (1)
Здравствуйте, mik1, Вы писали:

M>Здравствуйте, Serginio1, Вы писали:


S>>Здравствуйте, mik1, Вы писали:

S>> Есть алгоритмы чисто рекурсивные требующие своего стека. В большинстве случаев избавление от использования програмного стека это содание экземпляра класса и инициализация в каждой процедуре.

S>>Для примера


S>>
S>>public class Factorial
S>>{ 
S>>   private int _n
S>>  public Factorial(n)
S>>{ _n=n}

S>>    public int fac()
S>>{
S>>    if _n<=0 return 1;
S>>    Factorial factorial =new Factorial(_n-1);

S>>   return _n*factorial.fac()

S>>}
S>>}
S>>



M>Ээээ... Тут стек очень даже используется. Да еще и покрепче, чем в лоб писать (Object + int-поле вместо int-локальной переменной)...

M>Вот если бы каждый объект факториала в новом потоке считался, тогда да...

Стек используется только под ссылку на объект при каждом вызове, а вот переменные хранятся в динамической памяти объекта.
Выигрыш получаем если количество сохраняемых параметров в полях объекта, более чем ссылка под объект
Можно обойти и хранения ссылки на объет, если сохранять ее в пользовательской очереди (стеке) статическом поле
... << RSDN@Home 1.1.4 stable rev. 510>>
и солнце б утром не вставало, когда бы не было меня
Re[7]: Реализация рекурсии в компиляторах ФЯ
От: mik1  
Дата: 21.03.06 12:09
Оценка:
Здравствуйте, z00n, Вы писали:

Z> На самом деле, закономерность несложная и по сути напоминает CPS: сделать и результат и все промежуточные результататы аргументами функции:


Z>
Z>-- рекурсивно:
Z>fact(n)
Z>  if (n == 1)
Z>    return 1;
Z>  return n * fact(n - 1);

Z>-- хвосто-рекурсивно
Z>fact(n, result)
Z>  if (n == 0)
Z>    return result;
Z>  return fact((n - 1), (n * result));
Z>


З.Ы. Переписать код факториала в вид без рекурсии — это не та проблема. До сих пор не пойму алгоритма для переписывания tree-recursive функций.
Re[9]: Реализация рекурсии в компиляторах ФЯ
От: mik1  
Дата: 21.03.06 12:13
Оценка: +1
Здравствуйте, Serginio1, Вы писали:

M>>Ээээ... Тут стек очень даже используется. Да еще и покрепче, чем в лоб писать (Object + int-поле вместо int-локальной переменной)...

M>>Вот если бы каждый объект факториала в новом потоке считался, тогда да...

S> Стек используется только под ссылку на объект при каждом вызове, а вот переменные хранятся в динамической памяти объекта.

S> Выигрыш получаем если количество сохраняемых параметров в полях объекта, более чем ссылка под объект
S> Можно обойти и хранения ссылки на объет, если сохранять ее в пользовательской очереди (стеке) статическом поле

Думается мне, что и в этом случае компилятор что-то сохраняет в стеке при каждом вызове. Регистры (или их аналог), адрес возврата.
Это чуть ослабляет проблему, но не решает. Хотя как один из методов, да, можно использовать.
Re[8]: Реализация рекурсии в компиляторах ФЯ
От: Serginio1 СССР https://habrahabr.ru/users/serginio1/topics/
Дата: 21.03.06 12:49
Оценка:
Здравствуйте, mik1, Вы писали:

M>З.Ы. Переписать код факториала в вид без рекурсии — это не та проблема. До сих пор не пойму алгоритма для переписывания tree-recursive функций.


Если под tree-recursive функций понимается рекурсивный обход, то опять же на примере списка экземпляров класса, создается новый объект со своими состояниями и вызывается метод этого объекта который сравнивает состояния и либо возвращает результат, либо продолжает идти по дереву.
... << RSDN@Home 1.1.4 stable rev. 510>>
и солнце б утром не вставало, когда бы не было меня
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.