简体   繁体   English

Perl:使用尾调用优化以递归方式查找数组的总和

[英]Perl: Find a sum of an array recursively with tail call optimization

I'm trying to make a tail optimized recursive function. 我正在尝试制作尾部优化的递归函数。

sub sum {
    my ($first, @rest) = @_;

    return @rest
        ? $first + sum(@rest)
        : $first;
}

say sum(1 .. 100);

It works for 100 elements however, it fails for 100_000 elements with Out of memory message. 它适用于100个元素,但是对于带有Out of memory消息的100_000个元素,它会失败。

How can improve the code to make the recursion working with more elements? 如何改进代码以使递归与更多元素一起工作?

Edit 编辑

Tail call optimized version of the above function: 尾调用以上功能的优化版:

use feature qw( current_sub );

sub sum_tco {
    my $loop = sub {
        my ($sum, $first, @rest) = @_;
        $sum += $first;

        return @rest
            ? __SUB__->($sum, @rest)
            : $sum;
    };

    return $loop->(@_);
}

It appears that Perl 5 doesn't support TCO. Perl 5似乎不支持TCO。

How to make TCO in Perl (if possible)? 如何在Perl中进行TCO(如果可能)?

You are correct that Perl doesn't perform tail call optimization. Perl不执行尾调用优化是正确的。

If you had a tail call, you could optimize it yourself. 如果你有一个尾调,你可以自己优化它。 But that said, you don't have a tail call. 但话虽如此,你没有尾调。 The recursive call is followed by an addition. 递归调用之后是添加。

So let's start by changing the sub to have only tail calls. 所以让我们从更改sub开始只有尾调用。 This is done by passing forward the information needed to perform that last operation. 这是通过传递执行最后一次操作所需的信息来完成的。

sub _sum {
   my ($acc, $first, @rest) = @_;
   $acc += $first;
   return @rest ? _sum( $acc, @rest ) : $acc;
}

sub sum {
   my (@rest) = @_;
   return undef if !@rest;
   return _sum( 0, @rest );
}

Now we can perform tail call optimizations. 现在我们可以执行尾调用优化。

  1. Place the body of the recursive subroutine in an infinite loop. 将递归子例程的主体放在无限循环中。
  2. Replace recurse(...) with do { @_ = ...; next; } do { @_ = ...; next; }替换recurse(...) do { @_ = ...; next; } do { @_ = ...; next; } do { @_ = ...; next; } . do { @_ = ...; next; }

First in the helper. 首先是助手。

sub _sum {
   while (1) {
      my ($acc, $first, @rest) = @_;
      $acc += $first;
      if (@rest) {
         @_ = ( $acc, @rest );
      } else {
         return $acc;
      }
   }
}

sub sum {
   my (@rest) = @_;
   return undef if !@rest;
   return _sum( 0, @rest );
}

Then in the main sub. 然后在主要子。

sub sum {
   my (@rest) = @_;
   return undef if !@rest;

   @_ = ( 0, @rest );
   while (1) {
      my ($acc, $first, @rest) = @_;
      $acc += $first;
      if (@rest) {
         @_ = ( $acc, @rest );
      } else {
         return $acc;
      }
   }
}

Done. 完成。

...kinda. ...有点儿。 There are so many other cleanups and optimizations we can do now. 我们现在可以做很多其他的清理和优化。

Let's start by improving the flow. 让我们从改善流程开始。

sub sum {
   my (@rest) = @_;
   return undef if !@rest;

   @_ = ( 0, @rest );
   while (1) {
      my ($acc, $first, @rest) = @_;
      $acc += $first;
      return $acc if !@rest;

      @_ = ( $acc, @rest );
   }
}

No need to create a new $acc every pass through the loop. 无需在每次循环中创建新的$acc

sub sum {
   my (@rest) = @_;
   return undef if !@rest;

   my $acc = 0;
   while (1) {
      my ($first, @rest) = @_;
      $acc += $first;
      return $acc if !@rest;

      @_ = @rest;
   }
}

There's no need to use @_ anymore. 没有必要再使用@_了。

sub sum {
   my (@rest) = @_;
   return undef if !@rest;

   my $acc = 0;
   while (1) {
      (my $first, @rest) = @rest;
      $acc += $first;
      return $acc if !@rest;
   }
}

Let's replace the expensive list assignment. 让我们替换昂贵的列表分配。

sub sum {
   my (@rest) = @_;
   return undef if !@rest;

   my $acc = 0;
   while (1) {
      my $first = shift(@rest);
      $acc += $first;
      return $acc if !@rest;
   }
}

Let's simplify the loop. 让我们简化循环。

sub sum {
   my (@rest) = @_;
   return undef if !@rest;

   my $acc = 0;
   while (@rest) {
      my $first = shift(@rest);
      $acc += $first;
   }

   return $acc;
}

Let's replace the while loop with a cheaper foreach loop. 让我们用更便宜的foreach循环替换while循环。

sub sum {
   my (@rest) = @_;
   return undef if !@rest;

   my $acc = 0;
   for my $first (@rest) {
      $acc += $first;
   }

   return $acc;
}

$first and @rest are no longer appropriate variable names. $first@rest不再是合适的变量名。 We'll get rid of a useless copy of @_ in the process. 我们将在这个过程中摆脱一个无用的@_副本。

sub sum {
   return undef if !@_;

   my $acc = 0;
   $acc += $_ for @_;
   return $acc;
}

If we initialize $acc to undef , the initial check is no longer needed. 如果我们将$acc初始化$acc undef ,则不再需要初始检查。

sub sum {
   my $acc;
   $acc += $_ for @_;
   return $acc;
}

Tada! 田田!

Here's a TCO version using that goto feature I mentioned in a comment: 这是使用我在评论中提到的goto功能的TCO版本:

#!/usr/bin/env perl
use warnings;
use strict;
use feature qw/say/;

sub sum {
    return undef if @_ == 0;
    return $_[0] if @_ == 1;
    splice @_, 0, 2, $_[0] + $_[1];
    goto ∑
}

say sum(1..100);
say sum(1..100_000);

From the documentation : 文档

The goto &NAME form is quite different from the other forms of goto. goto和NAME表单与其他形式的goto完全不同。 In fact, it isn't a goto in the normal sense at all, and doesn't have the stigma associated with other gotos. 事实上,它根本不是正常意义上的结果,并且没有与其他结果相关的耻辱感。 Instead, it exits the current subroutine (losing any changes set by local) and immediately calls in its place the named subroutine using the current value of @_ 相反,它退出当前子例程(丢失由本地设置的任何更改)并立即使用@_的当前值调用命名子例程

I don't recommend actually using this as it's really, really slow compared to anything else, but it can be done. 我不建议实际使用它,因为它真的,非常慢,相比其他任何东西,但它可以做到。

Here's a technique using a generic run - recur interface. 这是一种使用通用run - recur接口的技术。 This is effectively a trampoline - 这实际上是一个蹦床 -

sub recur (*@values) {
  :{ 'recur' => &recur, 'values' => @values }
}

sub run (&f) {
  my $r = &f();
  while $r.isa(Hash) && $r{'recur'} === &recur {
    $r = &f(|$r{'values'});
  }
  return $r;
}

To use it, we pass a subroutine to run with loop arguments and their initial values - 要使用它,我们传递一个子程序来run循环参数及其初始值 -

sub sum ($n = 0) {
  run (sub ($m = $n, $r = 0) {
    if $m == 0 {
      return $r;
    }
    else {
      recur($m - 1, $r + $m);
    }
  })
}

NB we use recur with the updated arguments, instead of calling sum directly. 注意我们使用recur和更新的参数,而不是直接调用sum Here's the output - 这是输出 -

say sum(100_000);
# 100_000 + 99_999 + 99_997 + ... + 3 + 2 + 1 =
# => 5000050000

# cpu time: 10.61 sec

Here it is working on a range instead. 在这里,它正在研究范围。 We use loop variables to keep track of the range index, $i and the return value, $r - 我们使用循环变量来跟踪范围索引, $i和返回值$r -

sub sum (@range) {
  run (sub ($i = 0, $r = 0) {
    if $i >= @range {
      return $r;
    }
    else {
      recur($i + 1, $r + @range[$i]);
    }
  })
}

say sum(5..10);
# 5 + 6 + 7 + 8 + 9 + 10 =
# => 45

say sum(0..0);
# => 0

say sum(1..100_000);
# => 5000050000

# cpu time: 14.37 sec

The other techniques presented here ask that you dramatically change your program in order to avoid stack overflow. 此处介绍的其他技术要求您大幅更改程序以避免堆栈溢出。 The unique run - recur interface allows you to think about your problem recursively and allow it to run in constant space. 独特的run - recur接口允许您递归地思考问题允许它在恒定的空间中运行。


Here is a revision compatible with Perl 5. To my surprise, this program is almost 50 times faster. 这是与Perl 5兼容的版本。令我惊讶的是,这个程序快了近50倍。 Maybe the slow down is owed to the poor implementation of newer syntax sugars? 也许减速是由于较新的语法糖的执行不力? It's anyone's guess... 这是任何人的猜测......

use strict;
use warnings;

sub recur {
  { recur => \&recur, values => \@_ }
}

sub run {
  my ($f, @init) = @_;
  my $r = &{$f}(@init);
  while (ref $r eq ref {} && $r->{'recur'} == \&recur) {
    $r = &{$f}(@{$r->{'values'}});
  }
  return $r;
}

sub sum {
  my ($n) = @_;
  run (sub {
    my ($m, $r) = @_;
    if ($m == 0) {
      return $r;
    }
    else {
      recur($m - 1, $r + $m);
    }
  }, $n, 0);
}

print sum(100_000);
# => 5000050000

# cpu: 0.25 sec
# mem: 3 Mb

And the sum variant that takes a range input - 并且采用范围输入的sum变量 -

sub sum {
  my (@range) = @_;
  run (sub {
    my ($i, $r) = @_;
    if ($i >= @range) {
      return $r;
    }
    else {
      recur($i + 1, $r + $range[$i]);
    }
  }, 0, 0);
}

print sum(1..100_000);
# => 5000050000

# cpu: 0.27 sec
# mem: 12 Mb

A file calls itself with a reducing number of arguments, inspired by this post: A simple perl recursion example . 在这篇文章的启发下,一个文件调用自身的参数数量减少: 一个简单的perl递归示例

This, of course, is far away from a practical solution. 当然,这远非实际的解决方案。

#!/usr/bin/perl
use strict;
use warnings;
use feature qw(say);

# Init the args at the first call
if (!@ARGV) {exec join(' ', $^X, $0, 1 .. 100_000)}

# Show progress
if (@ARGV % 100 == 0) {say scalar @ARGV}

my ($sum, $first, @rest) = @ARGV;
$sum += $first;

@rest
    ? exec join(' ', $^X, $0, $sum, @rest)
    : say $sum;

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM