【问题标题】:cached schwartzian transform缓存的施瓦茨变换
【发布时间】:2011-06-08 16:38:40
【问题描述】:

我正在学习“中级 Perl”,它非常酷。我刚刚完成了关于“施瓦茨变换”的部分,在它沉没之后,我开始想知道为什么变换不使用缓存。在具有多个重复值的列表中,转换会重新计算每个值的值,所以我想为什么不使用哈希来缓存结果。这是一些代码:

# a place to keep our results
my %cache;

# the transformation we are interested in
sub foo {
  # expensive operations
}

# some data
my @unsorted_list = ....;

# sorting with the help of the cache
my @sorted_list = sort {
  ($cache{$a} //= &foo($a)) <=> ($cache{$b} //= &foo($b))
} @unsorted_list;

我错过了什么吗?为什么没有在书籍中列出缓存版本的 Schwartzian 变换,并且通常只是更好地传播,因为乍一看我认为缓存版本应该更有效?

编辑:daxim 在 cmets 中指出这被称为orcish maneuver。所以我并没有发疯,虽然我不太了解这个名字。

【问题讨论】:

  • @daxim:哈哈,就是这样。我只是不知道这个名字。
  • Orcish 是 Or Cache 的损坏。

标签: perl caching optimization sorting memoization


【解决方案1】:

(许多其他 cmets 已编辑)

在某种程度上,数组查找比哈希查找更有效(即,$a-&gt;[1]$cache{$a} 快),规范形式可能比您的代码更有效,即使有很多重复。


基准测试结果:

这是我的基准测试代码:

# when does an additional layer of caching improve the performance of 
# the Schwartzian transform?

# methods:
#   1. canonical Schwartzian transform
#   2. cached transform
#   3. canonical with memoized function

# inputs:
#   1. few duplicates (rand)
#   2. many duplicates (int(rand))

# functions:
#   1. fast
#   2. slow

use Benchmark;
use Math::BigInt;
use strict qw(vars subs);
use warnings;
no warnings 'uninitialized';

# fast_foo: a cheap operation,  slow_foo: an expensive operation
sub fast_foo { my $x = shift; exp($x) }
sub slow_foo { my $x = shift; my $y = new Math::BigInt(int(exp($x))); $y->bfac() }

# XXX_memo_foo: put caching optimization inside call to 'foo'
my %fast_memo = ();
sub fast_memo_foo {
  my $x = shift;
  if (exists($fast_memo{$x})) {
    return $fast_memo{$x};
  } else {
    return $fast_memo{$x} = fast_foo($x);
  }
}

my %slow_memo = ();
sub slow_memo_foo {
  my $x = shift;
  if (exists($slow_memo{$x})) {
    return $slow_memo{$x};
  } else {
    return $slow_memo{$x} = slow_foo($x);
  }
}

my @functions = qw(fast_foo slow_foo fast_memo_foo slow_memo_foo);
my @input1 = map { 5 * rand } 1 .. 1000;         # 1000 random floats with few duplicates
my @input2 = map { int } @input1;                # 1000 random ints with many duplicates

sub canonical_ST {
  my $func = shift @_;
  my @sorted = map { $_->[0] }
    sort { $a->[1] <=> $b->[1] }
    map { [$_, $func->($_)] } @_;
  return;
}

sub cached_ST {
  my $func = shift @_;
  my %cache = ();
  my @sorted = sort {
    ($cache{$a} //= $func->($a)) <=> ($cache{$b} //= $func->{$b})
  } @_;
  return;
}

foreach my $input ('few duplicates','many duplicates') {
  my @input = $input eq 'few duplicates' ? @input1 : @input2;
  foreach my $func (@functions) {

    print "\nInput: $input\nFunction: $func\n-----------------\n";
    Benchmark::cmpthese($func =~ /slow/ ? 30 : 1000,
             {
              'Canonical' => sub { canonical_ST($func, @input) },
              'Cached'    => sub { cached_ST($func, @input) }
             });
  }
}

和结果(Strawberry Perl 5.12):

输入:少量重复 功能:fast_foo ----------------- 速率规范缓存 标准 160/s -- -18% 缓存 196/s 22% -- 输入:少量重复 功能:slow_foo ----------------- 速率规范缓存 标准 7.41/s -- -0% 缓存 7.41/s 0% -- 输入:少量重复 功能:fast_memo_foo ----------------- 速率规范缓存 标准 153/s -- -25% 缓存 204/s 33% -- 输入:少量重复 功能:slow_memo_foo ----------------- 速率缓存规范 缓存 20.2/s -- -7% 标准 21.8/s 8% -- 输入:许多重复 功能:fast_foo ----------------- 速率规范缓存 标准 179/s -- -50% 缓存 359/s 101% -- 输入:许多重复 功能:slow_foo ----------------- 速率规范缓存 标准 11.8/s -- -62% 缓存 31.0/s 161% -- 输入:许多重复 功能:fast_memo_foo ----------------- 速率规范缓存 标准 179/s -- -50% 缓存 360/s 101% -- 输入:许多重复 功能:slow_memo_foo ----------------- 速率规范缓存 标准 28.2/s -- -9% 缓存 31.0/s 10% --

我对这些结果有点吃惊——规范的 Schwartzian 变换在最有利的条件下(昂贵的函数调用、很少的重复或没有记忆)只有一点优势,而在其他情况下则处于相当大的劣势. sort 函数内的 OP 缓存方案甚至优于 sort 外的记忆。当我做基准测试时,我并没有预料到这一点,但我认为 OP 正在做一些事情。

【讨论】:

  • 这仍然对@unsorted 中的每个项目调用foo()。 davidk01 的想法是,由于@unsorted 中可能存在重复项,因此应该缓存处理后的值,以避免再次调用foo() 获取重复值。
  • 考虑列表(2,2) 和函数x -&gt; x*x,然后施瓦茨变换应该将其转换为([2,4],[2,4]),但不清楚为什么foo(2) 只计算一次。在这种情况下,应该使用缓存计算两次而不是一次。
  • 我不是为了可读性,也没有考虑 0 的情况,但是对于具有大量冗余的列表,我认为缓存版本应该更快,因为它一次性完成所有操作并使用查找操作而不是对每个元素进行子例程调用。
  • 在这种情况下,您可以只使用记忆化的foo 函数——效率提升可能来自施瓦茨变换内部或外部。
  • 感谢您进行基准测试。当我发布问题时,我自己只执行了一些微不足道的基准测试。您的结果更加彻底。
【解决方案2】:

当您在多个转换中调用 foo() 时,缓存 Schwartzian 转换会很有用:

@sorted1 = map { $_->[0] }
           sort { $a->[1] cmp $b->[1] }
           map  { [$_, foo($_)] }
           @unsorted1;
@sorted2 = map { $_->[0] }
           sort { $a->[1] cmp $b->[1] }
           map  { [$_, foo($_)] }
           @unsorted2;

如果@unsorted1@unsorted2 具有大致相同的值,那么您将为相同的值调用foo() 两次。如果此函数的计算量很大,您可能希望缓存结果。

最简单的方法是使用Memoize module

use Memoize;
memoize('foo');

如果你添加这两行,你就不用担心自己为foo()设置缓存,Memoize会为你处理。

编辑:我只是注意到您的排序不进行 Schwartzian 变换。 ST 背后的全部要点是,您只为列表的每个成员运行一次昂贵的函数,这就是为什么您要执行整个 map sort map 构造。虽然您可能可以像您所做的那样进行一些手写缓存,但这将是非标准 Perl(从某种意义上说,有人会期望看到 ST,然后必须坐在那里弄清楚您的代码是什么做)并且可能很快成为维护的噩梦。

但是是的,如果您的列表具有重复值,则使用缓存(手动或使用 Memoize)可能会导致更快的 Schwartzian 变换。我说“可以”是因为在某些情况下,进行哈希查找实际上比调用 foo() 更昂贵(Memoize 文档使用 sub foo { my $x = shift; return $x * $x } 作为这些实例之一的示例)。

【讨论】:

  • 感谢模块指针。我对整个 CPAN 业务还是新手,我只是想在开始使用 CPAN 模块之前从基本原理中了解一些东西。
  • 我认为学习 CPAN /is/ 是学习 Perl 的首要原则。 :-) 另外,Memoize 是一个核心模块,所以你不必去 CPAN 获取它,你已经拥有它。
  • 我的代码确实执行了 schwartzian 变换,但我们没有使用 [$x, &amp;foo($x)] 模拟结果,而是使用 {$x =&gt; &amp;foo($x)} 模拟结果,因此我们进行哈希查找而不是投影到第二个组件上。
  • 其实这不是 Schwartzian 变换,它是一种带有缓存的排序。 ST 强制为列表中的每个成员计算 foo(),而您的缓存排序只为列表的唯一成员计算 foo()。 ST 实际上是指map/sort/map 成语而不是算法。
  • 那么我的重点是错误的。
猜你喜欢
  • 1970-01-01
  • 2010-10-10
  • 2015-06-15
  • 1970-01-01
  • 2021-10-04
  • 1970-01-01
  • 2011-12-18
  • 2012-08-06
  • 2023-02-13
相关资源
最近更新 更多