Perl DBI - 循环终止性能?

Perl DBI - For loop killing performance?

我正在开发一个 perl 脚本,该脚本使用 DBI 将数据从数据库 table 卸载为特定格式。我有一些东西在工作,但性能......缺乏。

这是代码的性能关键部分:

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 万行的测试 table。总卸载时间为 90 秒(输出重定向到 /dev/null,因此磁盘写入不会干扰基准测试)。

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

关键在于:删除实际的替换步骤 ($col =~ s/(?=[\x5C\x00-\x1F])/\/g;) 仅节省了大约 12 秒的处理时间。这意味着不执行任何操作的 for 循环 (for $col (@$row) { ; }) 会产生 18 秒的开销,比替换本身还要多。 (通过完全删除循环验证了这一点。)

总结:

问题:

如何提高消毒步骤的性能?
奖励:为什么 for 循环开销很高?

备注:

对我来说,

  • 测试工具加上替换每个元素需要 3.57 微秒(对于 7 个字符的字符串,其中一个字符需要转义)。
  • 测试线束加上环路每个元素需要 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/秒

如果您只想将 table 转储为分隔文件,让数据库来完成。 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)) 需要三分之一的时间。所以也许有一种方法可以微优化内循环。我会饶过你,事实证明,没有块的 void 上下文中的地图要快得多。

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 @$rowsfor (@$rows) { s/1/2/ }.

的编译几乎相同