【问题标题】:Perl DBI - For loop killing performance?Perl DBI - 用于循环终止性能?
【发布时间】:2015-03-27 17:55:41
【问题描述】:

我正在编写一个使用 DBI 将数据从数据库表中卸载为特定格式的 perl 脚本。我有一些工作,但性能......缺乏。

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

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 循环 (for $col (@$row) { ; }) 会产生 18 秒的开销,比替换本身还多。 (这已通过完全删除循环来验证。)

总结:

  • 清理循环大约需要总执行时间的 1/3,我的测试数据大约需要 30 秒。根据源数据中的列数,它会相应地花费更多时间。
  • 清理循环 ($col =~ s/...//g;) 的替换部分需要 12 秒来处理我的测试数据。
  • 剩下的 18 秒是 for 循环结构本身。

问题:

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

注意事项:

  • 清理本身只是在任何特殊字符之前放置一个反斜杠。

  • 需要进行清理,并且必须在join 发生之前对每一列进行清理。这是一个技术限制,因为$COLUMN_DELIM_STR 可能包含特殊字符,我们需要它们被转义。此外,$COLUMN_DELIM_STR 的长度和值可能因脚本运行而异。

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

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

【问题讨论】:

  • 数据库在哪里 - 跨网络?什么是数据库?
  • 您可以使用qr 编译正则表达式以获得更高的性能。 (但由于这不是最大的问题,所以我没有发布作为答案)
  • @MarkSetchell - 数据库在本地机器上,但这不是重点。我更关心的是消毒程序会浪费 1/3 的总处理时间。随着表中列数的增加,清理数据所花费的总时间百分比显着增加。
  • 尝试使用Devel::NYTProf 以获得更细粒度的配置文件。
  • @Sobrique,qr//m//s/// 中的常量正则表达式模式在编译时编译。 perl -Mre=debug -c -e'qr/qr/; m/m/; s/s//;' 2>&1 | grep Compiling

标签: performance perl loops dbi


【解决方案1】:

如果您只想将表转储为分隔文件,请让数据库来完成。 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 @$rows 的编译方式与 for (@$rows) { s/1/2/ } 几乎相同。

【讨论】:

  • 是的,几乎正好是 18 秒。至于转储:数据源是不支持转储到 CSV 的 Oracle。此外,输出格式不是 CSV。它是字符串分隔格式,是的,但分隔符可以是任意长度,并且可能包含二进制数据。
  • @Mr.Llama 如果您服务器的 CPU 比我的笔记本电脑慢三倍,那么您可能需要在服务器硬件上进行投资。与 300 美元的 CPU 升级相比,您将在优化上花费更多的时间/金钱。
  • @Mr.Llama 我已经更新以说明为什么 for 循环很慢并对其进行了微优化。您仍然应该考虑进行硬件升级。
  • map 解决方案就像一个魅力!我之前曾尝试使用map,但我提供的块完全抵消了性能提升。
  • 只是一个快速更新,通过在join 中嵌入map 并使用s///rg 返回值而不是就地修改,我获得了更好的性能。 map 无论如何都会返回结果,因此更改原始数据没有意义。我已将最终结果包含在原始问题中。
【解决方案2】:

对我来说,

  • 测试工具加上替换每个元素需要 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

【讨论】:

    猜你喜欢
    • 2016-10-31
    • 2010-10-26
    • 2011-10-24
    • 2011-08-17
    • 1970-01-01
    • 2011-09-06
    • 2014-03-12
    • 1970-01-01
    • 2012-09-17
    相关资源
    最近更新 更多