Выполнение этого в проверке экспоненциальности замедлит случаи, когда это не простая сила двух очень незначительных, поэтому не обязательно выигрыш. Однако в тех случаях, когда экспонента известна заранее (например, используется литерал 2), генерируемый байт-код может быть оптимизирован с простой оптимизацией глазок. Предположительно, это просто не считается достойным (это довольно конкретный случай).
Вот краткое доказательство концепции, которая делает такую оптимизацию (можно использовать как декоратор). Примечание: вам понадобится модуль byteplay для его запуска.
import byteplay, timeit
def optimise(func):
c = byteplay.Code.from_code(func.func_code)
prev=None
for i, (op, arg) in enumerate(c.code):
if op == byteplay.BINARY_POWER:
if c.code[i-1] == (byteplay.LOAD_CONST, 2):
c.code[i-1] = (byteplay.DUP_TOP, None)
c.code[i] = (byteplay.BINARY_MULTIPLY, None)
func.func_code = c.to_code()
return func
def square(x):
return x**2
print "Unoptimised :", timeit.Timer('square(10)','from __main__ import square').timeit(10000000)
square = optimise(square)
print "Optimised :", timeit.Timer('square(10)','from __main__ import square').timeit(10000000)
Что дает тайминги:
Unoptimised : 6.42024898529
Optimised : 4.52667593956
[Edit] На самом деле, думая об этом немного больше, есть очень веская причина, почему этот оптимизатор не сделан. Нет никакой гарантии, что кто-то не создаст пользовательский класс, который переопределяет методы __mul__
и __pow__
и сделает что-то другое для каждого. Единственный способ сделать это безопасно, если вы можете гарантировать, что объект в верхней части стека имеет тот же результат, что и «x **2
» и «x*x
», но работа над этим намного сложнее. Например. в моем примере это невозможно, так как любой объект может быть передан квадратной функции.
Не беспокойтесь о стеке. Нет ничего фундаментального в том, что вызовы функций должны быть реализованы с использованием стековых фреймов; это всего лишь один из возможных методов их реализации.
Даже когда у вас есть «стек», нет ничего, что говорит о том, что стек должен быть ограничен небольшой долей доступной памяти. Это по сути эвристика, настроенная на императивное программирование; там, где вы не используете рекурсию в качестве метода решения проблем, очень глубокие стеки вызовов, как правило, возникают из-за ошибок бесконечной рекурсии, а ограничение размера стека до чего-то весьма небольшого означает, что такие программы быстро умирают вместо того, чтобы использовать всю доступную память и перестановку, а затем умирающих.
Для функционального программиста наличие в программе «исчерпания» памяти для выполнения большего количества вызовов функций, когда на компьютере еще есть гигабайты оперативной памяти, является нелепым недостатком языкового дизайна. Это было бы похоже на ограничивающие циклы C для некоторого произвольного числа итераций. Таким образом, даже если функциональный язык реализует вызовы функций с использованием стека, будет сильная мотивация избегать использования стандартного крошечного стека, который мы знаем из C, если это возможно.
На самом деле, у Haskell есть стек , который может переполняться, но это не тот стек вызовов, с которым вы знакомы из C. Можно написать нерекурсивные функции, которые бесконечно рекурсивно и будет использовать всю доступную память без ограничения по глубине вызова. Стек, который есть у Haskell, используется для отслеживания «ожидающих» значений, которые необходимо оценить немного больше, чтобы принять решение (я расскажу об этом чуть позже). Вы можете прочитать более подробно об этом виде переполнения стека здесь .
Давайте рассмотрим пример, чтобы увидеть, как можно оценить ваш код. 1 sup> Я приведу еще более простой пример, чем ваш:
main = do
input <- getLine
if input == "quit"
then
putStrLn "Good-bye!"
else do
putStrLn $ "You typed: " ++ input
main
Оценка Haskell ленива 2 SUP>. Проще говоря, это означает, что он будет оценивать термин только тогда, когда ему нужно значение этого термина для принятия решения. Например, если я вычисляю 1 + 1
и затем добавляю результат этого в начало списка, его можно оставить как «ожидающий» 1 + 1
в списке 3 sup>. Но если я использую if
, чтобы проверить, был ли результат равен 3, , то Haskell должен будет фактически сделать работу по превращению 1 + 1
в 2
.
main
, чтобы выполнить его.
Вернемся к примеру. main
равен этому блоку do
. Для IO
блок do
выполняет большое действие IO
из серии меньших, которые должны выполняться по порядку. Таким образом, во время выполнения Haskell main
оценивается как input <- getLine
, за которым следуют некоторые неоцененные вещи, которые ему пока не нужны. Этого достаточно, чтобы знать, читать с клавиатуры и вызывать получившийся String
input
. Скажи, что я набрал "фу". Это оставляет Haskell с чем-то вроде следующего как его «следующее» IO
действие:
if "foo" == "quit"
then
putStrLn "Good-bye!"
else do
putStrLn $ "You typed: " ++ "foo"
main
Haskell смотрит только на самую внешнюю вещь, так что это очень похоже на « if бла бла бла бла ... ". if
IO-исполнитель - не то, с чем он может что-либо делать, поэтому его нужно оценить, чтобы увидеть, что он возвращает. if
просто оценивает либо ветку then
, либо else
, но чтобы узнать, какое решение сделать Haskell требуется для оценки условия. Таким образом, мы получаем:
if False
then
putStrLn "Good-bye!"
else do
putStrLn $ "You typed: " ++ "foo"
main
, который позволяет сократить все if
до:
do
putStrLn $ "You typed: " ++ "foo"
main
И снова, do
дает нам действие IO
, которое состоит из упорядоченная последовательность вложенных действий. Итак, следующая вещь, которую должен выполнить IO-исполнитель - putStrLn $ "You typed: " ++ "foo"
. Но это также не действие IO
(это неоцененное вычисление, которое должно привести к одному). Так что нам нужно это оценить.
«Самая внешняя» часть putStrLn $ "You typed: " ++ "foo"
на самом деле $
. Избавившись от синтаксиса инфиксного оператора, чтобы вы могли видеть его так же, как это делает runtiem Haskell, это выглядело бы так:
($) putStrLn ((++) "You typed: " "foo")
Но оператор $
только что определен как ($) f x = f x
, подставляя правая часть сразу дает нам:
putStrLn ((++) "You typed: " "foo")`
Теперь обычно мы оцениваем это, подставляя в определение putStrLn
, но это «волшебная» примитивная функция, которая не может быть прямо выражена в Haskell. код. Так что на самом деле это не оценивается так; внешний IO-исполнитель просто знает, что с ним делать. Но это требует, чтобы аргумент из putStrLn
был полностью оценен, поэтому мы не можем оставить его как (++) "You typed: " "foo"
.
На самом деле существует целый ряд шагов для полной оценки этого выражения, проходящих через определение ++
в терминах операций со списками, но давайте просто пропустим это и скажем, что оно оценивается как "You typed: foo"
. Таким образом, IO-исполнитель может выполнить putStrLn
(запись текста на консоль) и перейти ко второй части блока do
, а именно:
`main`
не то, что может быть немедленно выполнено как действие IO
(оно не встроено в Haskell, как putStrLn
и getLine
), поэтому мы оцениваем его, используя правую часть определения main
, чтобы получить :
do
input <- getLine
if input == "quit"
then
putStrLn "Good-bye!"
else do
putStrLn $ "You typed: " ++ input
main
И я уверен, что вы можете видеть, куда идут остальные.
1176 Обратите внимание, что я ничего не сказал ни о каком стеке. Все это просто построение структуры данных, описывающей действиеIO
, которое является main
, так что внешний драйвер может его выполнить. Это даже не особая структура данных; с точки зрения системы оценки, она, как и любая другая структура данных, не имеет произвольных ограничений по размеру.
В этом случае ленивая оценка означает, что генерация этой структуры данных чередуется с ее потреблением (и генерация более поздних ее частей может зависеть от того, что произошло в результате использования более ранних ее частей!), И поэтому эта программа может работать в постоянном количестве пространства. Но, как отмечается в комментарии Шахафа к этому вопросу, это не совсем оптимизация для удаления ненужных кадров стека; это просто то, что происходит автоматически с ленивой оценкой.
Так что я надеюсь, что это было достаточно полезно для вас, чтобы увидеть, что происходит. По сути, к тому времени, когда Haskell оценивает рекурсивный вызов getCommandsFromUser
, это уже сделано со всеми данными, сгенерированными в предыдущей итерации, и поэтому он собирает мусор. Таким образом, вы можете продолжать запускать эту программу бесконечно, не занимая больше фиксированного объема памяти. Это просто прямое следствие ленивой оценки, и оно существенно не отличается, когда задействован IO
.
1 sup> Я сразу откажусь от того, что я не очень подробно знаю о текущей текущей реализации Haskell. Однако я знаю общие методы реализации ленивых чистых языков, таких как Haskell. Я также постараюсь не вдаваться в подробности и просто объяснить, как все работает интуитивно. Так что эта учетная запись может быть неверной в некоторых тонких деталях того, что на самом деле происходит внутри вашего компьютера, но она должна показать вам, как эти вещи могут работать.
2 sup> Спецификация языка технически просто говорит, что оценка должна быть "нестрогой". Оценка, которую я собираюсь описать, которая неофициально называется «ленивая», на самом деле является лишь одной из возможных «нестрогих» стратегий оценки, но это то, что вы получаете на практике.
3 sup> И новый список на самом деле можно оставить как «ожидающий» результат (1 + 1) : originalList
, пока кто-то не узнает, пуст он или нет.
Эта реализация верна.
Я не думаю, что оптимизация хвостового вызова - это то, что делает эту работу эффективной. Вместо этого, что позволяет ему работать эффективно, так это, верите или нет, неизменность действий ввода-вывода. Вы удивлены, что действия IO неизменны? Я был сначала! Это означает следующее: getCommandsFromUser
- это рецепт «дел»; и каждый раз, когда вы оцениваете getCommandsFromUser
, он оценивает один и тот же рецепт . (Хотя, конечно, не каждый раз, когда вы следуете рецепту, вы получаете один и тот же результат! Но это совершенно другая фаза выполнения.)
В результате этого все оценки getCommandsFromUser
могут быть общими. - GHC просто хранит одну копию рецепта в памяти, и часть этого рецепта включает указатель на начало рецепта.
Неважно, что IO участвует. Вы можете прочитать об этом в вики Haskell:
Или, для более глубокого знакомства с IO Haskell:
Насколько я понимаю, вы должны забыть о TCO: вместо того, чтобы спрашивать, находится ли рекурсивный вызов в хвостовой позиции, подумайте в терминах защищенной рекурсии . Этот ответ Я думаю, что это правильно. Вы также можете проверить пост Data and Codata из всегда интересного и интересного блога «Neighbourhood of Infinity». Наконец, проверьте зоопарк космической утечки .
РЕДАКТИРОВАТЬ : Извините, вышеизложенное не решает ваш вопрос о монадических действиях напрямую; Мне интересно увидеть другие ответы, такие как DanielWagner, которые конкретно касаются монады IO.