2009-02
19

在Perl的各种文件测试运算符中,最有用的运算符之一就是 -C 了。 它返回的是文件的ctime到程序启动的那一时刻经过的天数。 如果不是整数天,返回值就带有小数。

这个运算符经常用来检测文件是否过期,比如判断文件距离上次修改是否超过了3天, 如果超过就将其删除。但是,你是否真正理解 -C 的工作原理呢?

阅读全文 »

2009-01
21

1. 脱引用(dereference)并不是只能用在引用上,返回引用的函数也能使用。如:

sub foo { my $a = 10; return \$a; }
print ${foo()};      # 返回10
print "${foo()}";    # 甚至在字符串替换中也能用!

2. 两个相邻的下标之间可以省去 -> 。如:

print $sue{children}->[1]->{age};
print $sue{children}[1]{age};     # 与上一条语句相同

3. 将字符串当作变量名的行为叫做符号引用:

$a = "x"; $x = 10; print $$a     # 输出10

注意符号引用只能引用到全局变量,用my声明过的就引用不到了。使用use strict可以禁止符号引用。

阅读全文 »

2008-12
29

之前曾经介绍过 SmallProfDProf。 之所以说NYTProf 是最强测试工具,是因为它给出了极其详尽的测试信息, 并且整理成便于阅读的HTML格式,将函数按着执行速度排序,而且测试速度相当快。

fcicq@2008/12/30:NYTProf是 New York Times Profiler的意思,想不到吧~

测试方法如下:

$ perl -d:NYTProf testfile.pl

执行后生成nytprof.out文件,执行以下命令将其转换成html:

$ nytprofhtml

然后打开nytprof/index.html,尽情查看测试结果吧。 结果中几乎每一项都有说明,这里就不多说了。

nytprof.png


2008-10
30

代码写多了,程序就会变得臃肿;程序臃肿了,就会变慢。 这时提高代码执行效率就非常重要了。 但是,代码优化并不是几条best practice就能完成的。 那些无关痛痒的空间分配、减少复制等优化措施,虽然有效,但却微乎其微。 优化的关键,是要找出瓶颈并解决之,这样才能以最小的代价获得最佳的效果。

这就用到Perl的一个强大的工具:DProf。 它可以测定程序执行的每个函数所花费的时间, 通过它,你可以迅速找到瓶颈在什么地方,再对症下药。

最近我做的一次性能分析是这样的。 我们的系统在某种条件下发送邮件时特别慢, 甚至等待十几分钟也无法结束。究竟问题出在哪里却不得而知。 于是DProf上场了。

首先写了个脚本,专门调用发送邮件的功能,排除其他功能的影响。 然后运行命令:

$ perl -d:DProf ./sendmail.pl

运行结束之后,会在当前目录下生成一个tmon.out文件。 使用dprofpp即可查看统计信息:

$ dprofpp
Total Elapsed Time = 19.70195 Seconds
 User+System Time = 18.52195 Seconds
Exclusive Times
%Time ExclSec CumulS #Calls sec/call Csec/c  Name
 83.5   15.46 15.770     11   1.4059 1.4337  Foo::Bar::crypt
 9.34   1.730  1.730      2   0.8650 0.8650  Net::Cmd::datasend
 1.24   0.230  0.305  17459   0.0000 0.0000  Crypt::Blowfish::encrypt
 0.70   0.129  0.818     36   0.0036 0.0227  base::import
 0.59   0.110  0.709     24   0.0046 0.0296  Foo::Bar::FooBarList::BEGIN
 0.44   0.082  0.082   9108   0.0000 0.0000  IO::Wrap::read
 0.40   0.075  0.075  17647   0.0000 0.0000  Crypt::Blowfish::crypt
 0.27   0.050  0.887      7   0.0071 0.1268  main::BEGIN

统计信息的意思分别是:

  • ExclSec: 函数自身的执行时间(不包括函数调用的其他函数)
  • CumulS: 函数的执行时间(包括函数调用的其他函数)
  • #Calls: 调用次数
  • sec/call: ExceSec/#Calls
  • Csec/c: CumulS/#Calls
  • Name: 函数名称

从上面的统计信息中可以看出,Foo::Bar::crypt占用了最多的时间(83.5%)。 而且更为重要的信息是,CumulS - ExclSec = 15.770 - 15.46 = 0.31, 也就是说,绝大部分时间都耗在了 Foo::Bar::crypt 函数本身,而不是它调用的其他函数。 这样,只需针对Foo::Bar::crypt函数进行分析就可以了。

还可以使用另一个性能测试工具Devel::SmallProf



2008-10
29
下面总结一下Perl中的各个对象、函数等在标量环境和列表环境下的返回值。 这个总结基本上用作cheatsheet,所以每个函数的用法不会详加解释。 详细情况请参考《Perl语言编程》。
类型标量环境列表环境
标量'hello'标量本身标量本身
列表('a','b','c')最后一个标量'c'列表本身
数组数组中的元素个数数组元素的列表
散列斜线分隔的已用空间和分配的总空间的值组成的字符串键值对的列表
行输入操作符<FH>文件的一行文件剩余所有行组成的列表
文件名glob <*.xml>一个文件名所有文件名的列表
范围操作符 ..左操作数为真时,返回真,直到右操作数为真后,返回假左操作数到右操作数之间的所有值
each %hash散列的一个键散列的一个键值对
gmtimectime(3)风格的字符串struct tm结构的9个值
localtimectime(3)风格的字符串struct tm结构的9个值
grep EXPR LISTgrep表达式匹配的次数匹配表达式的所有值
keys %hash散列中键的数目散列的所有键
m//表示匹配是否成功的布尔值捕获的$1、$2、$3...的列表
m//g执行一次匹配并返回表示是否成功的布尔值反复执行匹配,返回所有捕获组成的列表
readdir一个文件名剩余所有文件名的列表
readline文件的一行文件剩余所有行组成的列表
reverse LIST连接LIST中的所有元素成字符串,然后将这个字符串的字符顺序反转LIST的反转列表
split分割结果的子字符串的数量分割的子字符串
stat表示执行是否成功的布尔值文件的统计信息(包含13个元素的列表)


2008-10
28

在处理多语言编码、使用UTF-8时,偶尔会遇到这个我称之为“c3c2问题”的问题。花了一天时间好不容易找到了原因所在,写在这里希望对遇到的人有所帮助。基于Perl语言写的,可能是Perl的专有问题,不过其他的语言若遇到类似的现象也可作为参考。

现象

进行编码转换时出现乱码。转换后的结果类似于下面的样子:

c3 a3 c2 81 c2 82 ...

看起来就像是正确的字符序列中加入了许多\xc3、\xc2的字符。

原因

对非utf-8字符序列进行 utf8::encode 或者 Encode::from_to($str, 'utf8', '...') 等,就会出现 c3 a3 c2 81... 一样的字符。 也就是说,把不是utf8编码的东西当作utf8编码来使用,就出现这个现象。

例如下面的例子。

#!/usr/bin/perl

use Encode;

$str1 = "あああ";
utf8::decode($str1);
$str1 .= "あああ";
utf8::encode($str1);
print $str1;

print "----";

$str2 = "あああ";
Encode::from_to($str2, "UTF-8", "Shift_JIS");
utf8::encode($str2);
print $str2;

执行结果如下:

$ perl mojibake.pl  | xxd
0000000: e381 82e3 8182 e381 82c3 a3c2 81c2 82c3  ................
0000010: a3c2 81c2 82c3 a3c2 81c2 822d 2d2d 2dc2  ...........----.
0000020: 82c2 a0c2 82c2 a0c2 82c2 a0              ...........

这里$str1是utf8字符流和字节流的混合体(实际上这是不对的),$str2是将shift-jis的字节流进行encode。其结果都会产生大量的\xc3和\xc2。

可能的原因:

  1. 将decode过的字符串和未decode的字符串连接在一起使用
  2. 对字符串连续进行两次 utf8 -> 其他编码的转换


2008-10
15

这个题目似乎有些不太合适,因为这并不是我们常说的引用传递和值传递。但是我想提醒大家这一点,否则很容易犯错误。

问题:map语句的代码段中使用的 $_,是列表中的值本身,还是将列表中的值复制到 $_ 中?

例如,下面这段程序的运行结果是什么呢?

#!/usr/bin/perl

$,=',', $\="\n";

$a = [1,2,3];
print map { ++$_; } @$a;     # (A)
print @$a;                   # (B)

C程序员们通常会认为map会将@$a中的每个值复制到 $_ 中,加一后返回(这里我特地用了 ++$_ 使得它返回加一之后的值), 因此(A)行输出 2,3,4 ,而(B)行输出 1,2,3 。实则不然。运行结果为

2,3,4
2,3,4

可见,并非@$a中的元素复制到$_中,而是$_本身就是@$a中的元素。而map的目的就是要改变操作数的值。 下面这个例子可以很清楚地看到这一点。

map { ++$_ } (1,2,3);    # 运行时出错,++$_ 不能改变常量

类似地,grep的结果也是数组本身,返回值就是由操作数中的符合条件的元素本身组成的列表。如上例:

map { ++$_; } grep { $_ > 1 } @$a;  # 运行后 $a 为 [1,3,4]

如果实在不想让map改变操作数的值,可以将操作数转成引用之后再强制转成数组:

map { ++$_; } @{[ @$a ]};    # 运行后 $a 为 [1,2,3]

当然,上面例子中 ++ 操作符很明显会改变操作数的值,因此意识到上面的问题并不困难。 不过如果是 s/// 运算符,可能就不那么明显了。

类似的情况还有函数调用时的 @_ ,它代表调用函数时的实际参数本身,而不是实际参数的拷贝。 对@_进行修改,会直接影响到实参(就像C语言里面的传指针一样)。

一般我们在定义函数时都这么写:

sub foo {
  my $arg1 = shift;
}

参数多时可能会这么写:

sub foo {
  my ($arg1, $arg2, $arg3) = @_;
}

这样写之后,@_的值就被赋给了$arg1、$arg2等变量,以后对$arg1、$arg2的修改不会影响到实际参数。 但如果想修改实际参数,就要这样做:

my $arg1 = \$_[0];
$$arg1 = 'Hello!';        # 调用函数时的第一个实参会变成 Hello!


2008-10
09

相信很多人都写过下面这种函数,把参数当作hash引用来赋值:

sub foo {
  my $hash = shift;
  $hash->{foo} = 'bar';
}

然后这样调用:

foo($a);

调用之后,$a就变成了一个hash引用,里面包含了 foo => 'bar' 这一对值。 当然你可能会说,干嘛要在foo里面修改参数,直接return $hash不行吗。 当然可以,而且我也推荐使用return的方式, 不过有时候foo函数可能会很复杂,或者由于其他的原因而不得不使用修改参数的方式。

且慢!这种用法有时候会会失败。看下面的例子:

#!/usr/bin/perl

use Data::Dumper;

sub foo {
    my $h = shift;
    $h->{hello} = 'World!';
}

my $a;
my $b = {};
foo($a); print Dumper($a);
foo($b); print Dumper($b);

运行结果:

$VAR1 = undef;
$VAR1 = {
          'hello' => 'World!'
        };

为什么$a执行foo后仍然为undef,而$b就有值?

这是因为,my $a; 没有对$a初始化,此时$a的值为undef。 而调用foo时,参数实际上是传值调用,$a的undef值被赋给了$h变量, 此时$h = undef。而下一行将$h作为hash引用赋值时,系统会自动为$h赋一个空引用作为初始值。 ——但是,这个空引用以及接下来赋给的 hello => 'World!' 的值不会传回给外面的 $a,因为是值传递!

而$b则不同,初始化成一个空hash引用,这样传递给foo的就是个真正的引用值, 这样 hello => 'World!'的赋值可以赋给该引用。

所以,在使用hash引用时,初始化是非常必要的。



2008-09
05

今天讨论一下m//g一个的小问题。m//g表示在字符串中查找所有可能出现的模式匹配。 在标量环境下,m//g会依次查找每个出现的匹配。通常的用法是这样的:

while (/(pattern)/g) {
  print $1;    # 做些操作
}

在内部,m//g会保存一个“匹配位置”的变量,表示这次的m//g匹配到了什么位置,以便下次运行时从该处继续开始。 (这个位置可以通过pos函数获得,这里就不多说了。)通常,用在while循环里是没有任何问题的,但如果换成if会怎样?

$_ = "abc";
print "a" if /a/g;
print "b" if /b/g;
# 输出结果:ab

结果是正确的。如果将语句换个位置呢?

$_ = "abc";
print "b" if /b/g;
print "a" if /a/g;
# 输出结果:b

可见,第二次的模式 a 没有匹配。其实可以想到,第一次的m/b/g匹配后,匹配位置已经指向了字符串中的下一个位置c, 第二行的m/a/g继续从c开始匹配,当然不会匹配到任何东西。

借用一下《精通正则表达式》第7章对于Match运算符的说明:

匹配类型尝试开始位置匹配成功时的pos值匹配失败时的pos值
m/.../字符串起始位置(忽略pos)重置为undef重置为undef
m/.../g字符串的pos位置匹配结束位置的偏移值重置为undef
m/.../gc字符串的pos位置匹配结束位置的偏移值不变

话说回来,在if中使用/g选项是毫无意义的。这里只是提个醒,当你遇到诡异的无法匹配的问题时,别忘了是不是在if中使用了m//g。



2008-09
04

昨天一个同事问我关于Perl中的 -| 描述符的问题。 他的程序大概是这样的:

unless (open FH, "-|") {
  exec "foo bar";        # 用exec执行另一个程序
  exit;
}
while (<FH>) {
  ...
}
close FH;
$ret = $? >> 8;
if ($ret == 1) {
  ...
}

那么这里的 open FH, "-|" 是什么意思?$? >> 8 又是什么意思?

阅读全文 »