| 導購 | 订阅 | 在线投稿
分享
 
 
 

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

2008-05-19 06:25:39  編輯來源:互聯網  简体版  手機版  評論  字體: ||
 
 
  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;

  }

  
 
 
 
  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;   }   
󰈣󰈤
日版宠物情人插曲《Winding Road》歌词

日版宠物情人2017的插曲,很带节奏感,日语的,女生唱的。 最后听见是在第8集的时候女主手割伤了,然后男主用嘴帮她吸了一下,插曲就出来了。 歌手:Def...

兄弟共妻,我成了他们夜里的美食

老钟家的两个儿子很特别,就是跟其他的人不太一样,魔一般的执着。兄弟俩都到了要结婚的年龄了,不管自家老爹怎么磨破嘴皮子,兄弟俩说不娶就不娶,老父母为兄弟两操碎了心...

 
 
>>返回首頁<<
 
 
 
 
 熱帖排行
 
 
王朝网络微信公众号
微信扫码关注本站公众号 wangchaonetcn
 
  免责声明:本文仅代表作者个人观点,与王朝网络无关。王朝网络登载此文出于传递更多信息之目的,并不意味著赞同其观点或证实其描述,其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
 
© 2005- 王朝網路 版權所有