《高阶Perl》——3.12 速度的好处

简介: 本节书摘来自华章计算机《高阶Perl》一书中的第3章,第3.12节,作者(美)Mark Jason Dominus,译 滕家海,更多章节内容可以访问云栖社区“华章计算机”公众号查看。

3.12 速度的好处

此刻有个说法比较诱人,那就是记忆术只比手动缓存技术改善了一点,因为它做了同样的事情,仅有的额外的好处是你可以快速启动或者关闭它。但这不完全正确。当工具之间的速度和便利的差别够大,它会改变你思考和使用工具的方式。自动地使一个函数带记忆仅需要消耗手动书写代码的1/100的时间。这和飞机与牛车的速度差别一样。说飞机只是更快的牛车就忽略了本质:量变如此巨大以致它也变成了实质的质变。

例如,有了自动的记忆术,就有可能为函数增加缓存行为而不必预先仔细考虑性能细节。记忆术这么简单以至于可以采取一种称为“走着瞧”的策略。如果一个函数很慢,试着为它增加一些缓存看看是否有帮助。如果一个递归函数可能会有错误的递归行为,放一些缓存看看问题有没有消失。如果没有,你可以再次把缓存移除并更彻底地调查。所有的代价就是十秒左右的编程时间,你可以尝试,而不用预先考虑太多它将来是否会成功。要是手动缓存,你将不得不花费至少一刻钟,这足够去钓次鱼了。

有了自动的记忆术,你可以在运行时启动缓存行为。例如:

sub function {
  if (++$CALLS == 100) { memoize 'function'}
  ...
}

这里直到程序运行到某种程度时再使函数带记忆。当函数意识到自己被频繁使用时,它就激活缓存行为。要是没有自动的记忆术,那么做同样的事情就需要重写函数而不是增加单独的一行了。

3.12.1 剖析和性能分析

自动的记忆术使缓存以某种方式应用于剖析和性能分析中,否则就不实际了。典型的情况是卷入一个运行得太慢的巨大应用。如果希望加速它,就将重写部分程序以使它更快,也许引入一个更好的算法,也许牺牲一定的清晰度和可维护性。

试图加速程序的每一部分是不好的资源分配。这是因为所谓的“90-10法则”,一个程序的90%的运行时间发生在仅10%的代码中,剩余的是只执行一次的初始化代码,或者像错误处理那样很少或根本不执行的特殊情况的代码。如果检查全部程序并对每部分都加速5%,那么赚到5%。但是如果能识别并重写关键的10%并得到同样的加速,那么将以10%的代价净赚程序全部运行时间的4.5%,付出回报比改善了九倍。因此在优化之前,非常希望识别出程序中贡献最多运行时间的部分,并只集中改善那些部分。

悲惨的是当一个程序员花了一周时间仔细优化了一个子例程让它快了20%,却发现那个子例程的运行时间只占全部的2%,那一周的辛勤劳动对全局只产生了0.4%的提速。长期以来,程序员在猜测哪部分程序被频繁使用上很糟糕,因此需要真实的测量。

通常,测量时使用一种称为剖析(profiler)的工具。程序运行在一个特殊的剖析环境中,后者使程序非常频繁地(通常是每秒许多次)产生一个它正在做什么的记录。随后,数据被处理成一个报告以列出程序中占用最多运行时间的子例程。有一些针对Perl的剖析工具,但它们可能是陌生的且难以使用的。自动的记忆术是另一个选择。

运行一次程序并记下运行耗时。然后猜测程序的哪部分是瓶颈,并使它们带记忆。安排好带记忆的数据存放在固定的文件里。(记住,这仅需要在程序中添加一行代码。)第二次运行这个程序,它将占据磁盘上的缓存。第三次运行这个程序。对带记忆的函数的所有调用都将几乎立即返回,因为数据驻留在一个磁盘数据库上,除了被要求从数据库得到答案,函数根本不用做什么。第三次运行,便在模拟如果可能把目标函数的耗时移除后程序会有多快。如果这次运行比不带记忆的快得相当多,就有了优化候选了;如果运行时间类似,就该知道应当到别处看看了。

你可能疑惑当带记忆的运行耗时短得多时为什么不简单地把记忆术留在那里,答案是记忆术可能使目标函数运行更快,也可能使它们不正确。假设你怀疑瓶颈函数是那个排版报告的函数。当使之带记忆并让它放出预先缓存的报告可能使它运行得更快,然而报告可能不是接受者满意的。

3.12.2 自动剖析

另一种剖析技术,甚至更巧妙,使用在这章介绍过的技术,但不带任何实际的缓存。memoize函数接受一个存在的函数并把缓存管理放在它的前端。没有任何理由让前端必须做缓存管理,它也可以做别的事情:

### Code Library: profile
use Time::HiRes 'time';
my (%time, %calls);

sub profile {
  my ($func, $name) = @_;
  my $stub = sub {
    my $start = time;
    my $return = $func->(@_);
    my $end = time;
    my $elapsed = $end - $start;
    $calls{$name} += 1;
    $time{$name} += $elapsed;
    return $return;
  };
  return $stub;
}

此处展现的profile函数在结构上与先前介绍的memoize函数类似。像memoize,它接受一个函数作为它的参数并构造和返回一个存根,后者可以被直接调用或安装入符号表以代替原始的。

当存根被执行时,它把当前时间记录在$start。通常Perl的time函数返回最接近当前时间的秒数,如果可能Time::HiRes模块提供一个具有更高精度的函数以替代time函数。存根调用真实的函数并保存它的返回值。然后它计算经过的时间并更新两个散列。一个散列记录了每个函数被调用的次数,存根简单地增加那个数字。另一个散列记录了花在运行每个函数上的总耗时,存根把刚结束的调用的耗时加到总数上。

在程序运行的结尾,可以打印出一个报告:

END {
  printf STDERR "%-12s %9s %6s\n", "Function", "# calls", "Elapsed";
  for my $name (sort {$time{$b} <=> $time{$a}} (keys %time)) {
    printf STDERR "%-12s %9d %6.2f\n", $name, $calls{$name}, $time{$name};
  }
}

输出将如下:

            Function           # calls             Elapsed
            printout                 1               10.21
           searchfor                 1                0.34
                page                 1                0.06
          check_file                18                0.01

这是来自标准Perl的perldoc程序的输出。从这份输出可以发现多数的运行时间出现在printout函数;如果想要使perldoc更快,这个就是应当关注的函数。

3.12.3 钩子

很明显这是一个非常简陋的剖析工具。更好的版本应该使用times()函数测量CPU消耗时间而不是挂钟时间。但是这个技术的灵活性应该是清楚的,可以在函数上放置任意前端,或者在运行时改变前端。前端可以执行缓存,或者保持函数调用数据的轨迹;如果想要,它可以确认函数的参数,强置一些前置或后置条件,或任何别的想要的东西。

相关文章
|
1月前
|
机器学习/深度学习 算法 数据可视化
Python数学模块的应用与性能优化
【2月更文挑战第4天】 Python数学模块的应用与性能优化
50 2
|
Web App开发 缓存 算法
《高阶Perl》——3.3 好主意
本节书摘来自华章计算机《高阶Perl》一书中的第3章,第3.3节,作者(美)Mark Jason Dominus,译 滕家海,更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1173 0
|
缓存 自然语言处理 C语言
《高阶Perl》——3.5 MEMOIZE模块
本节书摘来自华章计算机《高阶Perl》一书中的第3章,第3.5节,作者(美)Mark Jason Dominus,译 滕家海,更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1501 0
《高阶Perl》——1.4 层次化数据
本节书摘来自华章计算机《高阶Perl》一书中的第1章,第1.4节,作者(美)Mark Jason Dominus,译 滕家海,更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1385 0
|
缓存 程序员 Perl
《高阶Perl》——3.6 CAVEATS
本节书摘来自华章计算机《高阶Perl》一书中的第3章,第3.6节,作者(美)Mark Jason Dominus,译 滕家海,更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1389 0
|
缓存 Perl
《高阶Perl》——3.4 记忆术
本节书摘来自华章计算机《高阶Perl》一书中的第3章,第3.4节,作者(美)Mark Jason Dominus,译 滕家海,更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1206 0