Perl DBI - 用于循环查杀性能?

时间:2015-01-28 17:45:47

标签: performance perl loops dbi

我正在使用一个perl脚本,该脚本使用DBI将数据库表中的数据卸载到特定格式。我有一些工作,但表现是......缺乏。

以下是代码的性能关键部分:

while (my $row = $query->fetchrow_arrayref()) {
    # Sanitize the columns to make sure certain characters are escaped with a backslash.
    # The escaping is required as some binary data may be included in some columns.
    # This must occur *before* the join() as $COLUMN_DELIM_STR may contain one of the special characters.
    for $col (@$row) { $col =~ s/(?=[\x5C\x00-\x1F])/\\/g; }

    # Output the sanitized row
    print join($COLUMN_DELIM_STR, @$row) . $RECORD_DELIM_STR;
}

我有一个包含5列和1000万行的测试表。总卸载时间为90秒(输出重定向到/dev/null,因此磁盘写入不会干扰基准测试。)

在尝试删除代码块以了解它们如何影响性能之后,我开始意识到清理循环占用了大量的处理时间,大约30秒(约占执行总量的1/3)时间)。设置DBI_PROFILE=4表示提取本身大约需要45秒。

这是踢球者:删除实际的替换步骤($col =~ s/(?=[\x5C\x00-\x1F])/\\/g;)只能节省大约12秒的处理时间。这意味着for-nothing for循环(for $col (@$row) { ; })会产生18秒的开销,超过替换本身。 (这是通过完全删除循环来验证的。)

要点:

  • 清理循环大约占总执行时间的1/3,对于我的测试数据大约需要30秒。根据源数据中的列数,按比例增加时间。
  • 清理循环($col =~ s/...//g;)的替换部分需要12秒才能获得我的测试数据。
  • 剩余的18秒是for循环结构本身。

问题:

如何改善消毒步骤的性能?
额外:为什么for循环开销很高?

备注:

  • 清理本身只是在任何特殊字符之前加上反斜杠。

  • 清理是必需的,必须在join发生之前应用于每一列。这是一个技术限制,因为$COLUMN_DELIM_STR可能包含特殊字符,我们需要它们进行转义。此外,$COLUMN_DELIM_STR的长度和值可能会在脚本运行之间有所不同。

  • 可以预先确定列数,但不能确定列名称或数据类型。该脚本不知道哪些列可能包含或不包含需要转义的特殊字符。

  • 如果有更好的方法来清理列数据,请随时提出建议。我愿意接受其他想法。

2 个答案:

答案 0 :(得分:5)

如果您只想将表转储为分隔文件,请让数据库执行此操作。 MySQL has SELECT INTO其他数据库具有类似的功能。这样可以避免将所有数据复制到程序中,改变它并再次吐出的开销。


另一种选择是在SELECT中进行转义。在Oracle中,您可以使用REGEXP_REPLACE。这应该这样做(我可能有关于反斜杠的细节错误)。

REGEXP_REPLACE(column, '([^[:print:]])', '\\\\1')

现在的问题是对每一栏都这样做。您不知道自己拥有多少列或其名称,但是您可以使用SELECT * FROM table LIMIT 1$sth->fetchrow_hashref直接轻松找到$dbh->column_info。现在,您可以构造具有正确行数的SELECT,并将REGEXP_REPLACE应用于每个行。这个可能更快。你甚至可以在SELECT中进行连接。

您甚至可以编写PL / SQL函数来为您完成所有这些操作。这可能是最有效的。这里的an example of writing a string join function可以适应也可以进行转义。


至于为什么空循环很慢,你运行它5000万次,虽然18秒似乎很高。我的2011 Macbook Pro可以在大约6秒钟内运行它,让我们验证空循环是否有问题。这段代码需要多长时间?

time perl -wle 'my $rows = [1..5]; for my $row (1..10_000_000) { for $col (@$rows) {} }'

简单地迭代5000万次(for (1..50_000_000))需要三分之一的时间。所以也许有一种方法可以对内循环进行微观优化。我会饶了你,它在无效的上下文中显示出一个地图,没有阻止的速度明显更快。

map s{(?=[\x5C\x00-\x1F])}{\\}g, @$rows;

为什么呢?使用B :: Terse转储字节码告诉我们Perl在地图中的工作量较少。这是内部for循环正在做的事情:

    UNOP (0x1234567890ab) null 
        LOGOP (0x1234567890ab) and 
            OP (0x1234567890ab) iter 
            LISTOP (0x1234567890ab) lineseq 
                COP (0x1234567890ab) nextstate 
                BINOP (0x1234567890ab) leaveloop 
                    LOOP (0x1234567890ab) enteriter 
                        OP (0x1234567890ab) null [3] 
                        UNOP (0x1234567890ab) null [147] 
                            OP (0x1234567890ab) pushmark 
                            UNOP (0x1234567890ab) rv2av [7] 
                                OP (0x1234567890ab) padsv [1] 
                        PADOP (0x1234567890ab) gv  GV (0x1234567890ab) *_ 
                    UNOP (0x1234567890ab) null 
                        LOGOP (0x1234567890ab) and 
                            OP (0x1234567890ab) iter 
                            LISTOP (0x1234567890ab) lineseq 
                                COP (0x1234567890ab) nextstate 
                                PMOP (0x1234567890ab) subst 
                                    SVOP (0x1234567890ab) const [12] PV (0x1234567890ab) "2" 
                                OP (0x1234567890ab) unstack 
                OP (0x1234567890ab) unstack 

这是地图。

    UNOP (0x1234567890ab) null 
        LOGOP (0x1234567890ab) and 
            OP (0x1234567890ab) iter 
            LISTOP (0x1234567890ab) lineseq 
                COP (0x1234567890ab) nextstate 
                LOGOP (0x1234567890ab) mapwhile [8] 
                    LISTOP (0x1234567890ab) mapstart 
                        OP (0x1234567890ab) pushmark 
                        UNOP (0x1234567890ab) null 
                            PMOP (0x1234567890ab) subst 
                                SVOP (0x1234567890ab) const [12] PV (0x1234567890ab) "2" 
                        UNOP (0x1234567890ab) rv2av [7] 
                            OP (0x1234567890ab) padsv [1] 
                OP (0x1234567890ab) unstack 

基本上,for循环必须经历为每次迭代设置新词法上下文的额外工作。地图没有,但你不能使用一个块。有趣的是,s/1/2/ for @$rows编译与for (@$rows) { s/1/2/ }几乎相同。

答案 1 :(得分:1)

对我来说,

  • 测试线束加上替换每个元素需要3.57μs(对于七个字符的字符串,其中一个字符需要转义)。
  • 测试线束加上环路每个元素需要0.960μs+0.141μs。

  • 因此,超过5个元素的循环变为1.66μs

这些数字在实践中可能会有所不同,但这个数字与我的预期相比更符合您的要求。执行基于正则表达式的替换是相当昂贵的,但是递增计数器不是,因此循环应该比替换便宜得多。


use strict;
use warnings;

use Benchmark qw( timethese );

my %tests = (
   'for'  => 'my $_col = our $col; our $row; for my $col (@$row) { }',
   's///' => 'my $_col = our $col; $_col =~ s/(?=[\\x5C\\x00-\\x1F])/\\\\/g;',
);

$_ = 'use strict; use warnings; '.$_ for values %tests;

{
   local our $row = [('a')x1000];
   local our $col = "abc\x00def";
   timethese(-3, \%tests);
}
{
   local our $row = [];
   local our $col = "abc\x00def";
   timethese(-3, \%tests);
}

输出:

  • for(1000个元素):7065.42 / s
  • for(0个元素):1041030.65 / s
  • s///:284348.25 / s