王朝网络
分享
 
 
 

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

王朝perl·作者佚名  2008-05-19
宽屏版  字体: |||超大  

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;

}

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
>>返回首页<<
推荐阅读
 
 
频道精选
 
静静地坐在废墟上,四周的荒凉一望无际,忽然觉得,凄凉也很美
© 2005- 王朝网络 版权所有