当前位置:Linux教程 - Linux资讯 - 优化 Perl 榨取代码的最大性能

优化 Perl 榨取代码的最大性能

  Perl 是一门非常灵活的语言,然而,其易用特性会使程序员滋生出一种懒散的编程习惯。我们应该对这些坏习惯负责,同时可以采取一些快捷步骤来提高 Perl 应用程序的性能。在本文中,我们将介绍优化的一些关键内容,了解哪些解决方案有效、哪些无效,以及如何继续构建并扩展设计时就考虑到优化和速度的应用程序。    拙劣的性能源自草率的编程  坦率地说,我喜欢 Perl,而且到处使用 Perl。我已经使用 Perl 开发了很多 Web 站点,编写了很多管理脚本,并编写了一些游戏。通常使用 Perl 是为了节省时间,并为我自动检查一些信息:从彩票号码到股票价格,我甚至使用 Perl 来自动编写邮件。由于使用 Perl 让一切都变得如此简单,因此很容易忘记对其进行优化。许多情况下,这并不是世界末日。因此多花几个毫秒来查询股票价格或处理日志文件又有什么关系呢?    然而,这些相同的懒惰习惯在小程序中可能只是多花费几毫秒的时间,但是在大规模开发项目中,多耗费的时间就变成数倍了。这就是 Perl 的 TMTOWTDI (There's More Than One Way To Do It) 颂歌开始变坏的地方。如果您需要很快的速度,不管有多少种慢速的方法,但是可能只有一两种方法可以达到最快的结果。最终,即使您可以得到预期的结果,但草率的编程还是会导致拙劣的性能。因此,在本文中,我们将介绍一些可以用来取消 Perl 应用程序额外执行周期的关键技术。    优化方法  首先,有必要随时记住 Perl 是一门编译语言程序。您所编写的源代码是转换为执行的字节码时进行编译的。字节码本身就有一个指令范围,所有的指令都是使用高度优化的 C 语言编写的。然而,即使在这些指令中,有些操作仍然可以进行优化,得到相同的结果,但是执行的效率更高。总体来讲,这意味着您要使用逻辑序列与字节码的组合,后者是从前者中生成的,最终会影响性能。某些相似操作之间性能的差距可能非常巨大。现在让我们考虑清单 1 和清单 2 中的代码。这两段代码都是将两个字符串连接为一个字符串,一个是通过普通的连接方法实现,而另外一个是通过生成一个数组并使用 join 方法进行连接。    清单 1. 连接字符串,版本 1    my $string = 'abcdefghijklmnopqrstuvwxyz';  my $concat = '';    foreach my $count (1..999999)  {  $concat .= $string;  }    清单 2. 连接字符串,版本 2    my $string = 'abcdefghijklmnopqrstuvwxyz';  my @concat;    foreach my $count (1..999999)  {  push @concat,$string;  }  my $concat = join('',@concat);    执行清单 1 需要 1.765 秒,而执行清单 2 则需要 5.244 秒。这两段代码都生成一个字符串,那么是什么操作耗费了这么多时间呢?传统上讲(包括 Perl 开发组),我们都认为连接字符串是一个非常耗时的过程,因为我们需要为变量扩展内存,然后将字符串及新添加的内容复制到新的变量中。另一方面,向一个数组中添加一个字符串应该非常简单。我们还添加了使用 join() 复制连接字符串的问题,这会额外增加 1 秒的时间。    这种情况下的问题在于,将字符串 push() 到一个字符串中非常耗时;首先,我们要执行一次函数调用(这会涉及压栈和出栈操作),还要添加额外的数组管理工作。相反,连接字符串操作非常简单,只是运行一个操作码,将一个字符串变量附加到一个现有的字符串变量中即可。即使设置数组的大小来减少其他工作的负载(使用 $#concat = 999999),也只能节省 1 秒钟的时间。    上面这个例子是非常极端的一个例子,在使用数组时,速度可以比使用字符串快数倍;如果需要重用一个特定的序列,但要使用不同的次序或不同的空格字符,那么这就是很好的一个例子。当然,如果想重新排列序列的内容,那么数组也非常有用。顺便说一下,在这个例子中,产生一个重复 999,999 次字符的字符串的更简便方法是:    $concat = 999999 x 'abcdefghijklmnopqrstuvwxyz';    这里介绍的很多技术单独使用都不会引起多大的差异,但是当您在应用程序中组合使用这些技术时,就可以在 Perl 应用程序中节省几百毫秒的时间,甚至是几秒的时间。    使用引用  如果使用大型数组或 hash 表,并使用它们作为函数的参数,那么应该使用它们的一个引用,而不应该直接使用它们。通过使用引用,可以告诉函数指向信息的指针。如果不使用引用,就需要将整个数组或 hash 表复制到该函数的调用栈中,然后在函数中再次对其进行复制。引用还可以节省内存(这可以减少足迹和管理的负载),并简化您的编程。    字符串处理  如果在程序中使用了大量的静态字符串,例如,在 Web 应用程序中,那么就要记得使用单引号,而不是使用双引号。双引号会强制 Perl 检查可能插入的信息,这会增加打印字符串的负载:    print 'A string','another string',"\n";    我使用逗号来分隔参数,而不是使用句号将这些字符串连接在一起。这样可以简化处理过程;print 只是简单地向输出文件发送一个参数。连接操作会首先将字符串连接在一起,然后将其作为一个参数打印。    循环  正如您已经看到的一样,带有参数的函数调用的开销很高,因为要想让函数调用正常工作,Perl 只能将这些参数压入调用堆栈之后,再调用函数,然后从堆栈中再次接收响应。所有这些操作都需要尽避免我们不需要的负载和处理操作。由于这个原因,在一个循环中使用太多函数调用不是个好主意。同样,这减少了比较的次数。循环 1,000 次并向函数传递信息会导致调用该函数 1,000 次。要解决这个问题,只需要调整一下代码的顺序即可。我们不使用 清单 3 的格式,而是使用清单 4 中的格式。    清单 3. 循环调用函数    foreach my $item (keys %{$values})  {  $values->{$item}->{result} = calculate($values->{$item});  }    sub calculate  {  my ($item) = @_;  return ($item->{adda}+$item->{addb});  }    清单 4. 函数使用循环    calculate_list($values);    sub calculate_list  {  my ($list) = @_;  foreach my $item (keys %{$values})  {  $values->{$item}->{result} = ($item->{adda}+$item->{addb});  }  }    更好的方式是在这种简单的计算中或者在简单的循环中使用 map:    map { $values->{$_}->{result} = $values->{$_}->{adda}+$values->{$_}->{addb} } keys %{$values};    还要记住的是,在循环中,每次反复都是在浪费时间,因此不要多次使用相同的循环,而是要尽量在一个循环中执行所有的操作。    排序  另外一种有关循环的通用操作是排序,特别是对 hash 表中的键值进行排序。在这个例子中嵌入对列表元素进行排序的操作是非常诱人的,如清单 5 所示。    清单 5. 不好的排序    my @marksorted = sort {sprintf('%s%s%s',  $marked_items->{$b}->{'upddate'},  $marked_items->{$b}->{'updtime'},  $marked_items->{$a}->{itemid})   sprintf('%s%s%s',  $marked_items->{$a}->{'upddate'},  $marked_items->{$a}->{'updtime'},  $marked_items->{$a}->{itemid}) } keys %{$marked_items};    这是一个典型的复杂数据排序操作,在该例中,要对日期、时间和 ID 号进行排序,这是通过将数字连接在一个数字上,然后对其进行数字排序实现的。问题是排序操作要遍历列表元素,并根据比较操作上下移动列表。这是一种类型的排序,但是与我们已经看到的排序的例子不同,它对每次比较操作都调用 sprintf。每次循环至少执行两次,遍历列表需要执行的精确循环次数取决于列表最初排序的情况。例如,对于一个 10,000 个元素的列表来说,您可能会调用 sprintf 超过 240,000 次。    解决方案是创建一个包含排序信息的列表,并只生成一次排序域信息。参考清单 5 中的例子,我将这段代码改写为清单 6 的代码。    清单 6. 较好的排序    map { $marked_items->{$_}->{sort} = sprintf('%s%s%s',  $marked_items->{$_}->{'upddate'},  $marked_items->{$_}->{'updtime'},  $marked_items->{$_}->{itemid}) } keys %{$marked_items};  my @marksorted = sort { $marked_items->{$b}->{sort}   $marked_items->{$a}->{sort} } keys %{$marked_items};    现在不需要每次都调用 sprintf,对 hash 表中的每一项,只需要调用一次该函数,就可以在 hash 表中生成一个排序字段,然后在排序时就可以使用这个排序字段了。排序操作只能访问排序字段的值。您可以将对包含 10,000 个元素的 hash 表的调用从 240,000 次减少到 10,000 次。这取决于最初对排序部分执行的操作,但是如果使用清单 6 中的方法,则可能节省一半的时间。    如果使用从数据库(例如 mysql 或类似的数据库)查询的结果来构建 hash 表,并在查询中使用使用排序操作,然后按照这个顺序来构建 hash 表,那么就无需再次遍历这些信息来进行排序。    使用简短的逻辑  与排序相关的是如何遍历可选值列表。使用很多 if 语句耗费的时间可能会令人难以置信。例如,请参阅清单 7 中的代码。    清单 7. 进行选择    if ($userchoice > 0)  {  $realchoice = $userchoice;  }
[1] [2] 下一页 

(出处:http://www.sheup.com)


上一页 [1] [2]