我正在使用一个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秒的开销,超过替换本身。 (这是通过完全删除循环来验证的。)
要点:
$col =~ s/...//g;
)的替换部分需要12秒才能获得我的测试数据。问题:
如何改善消毒步骤的性能?
额外:为什么for循环开销很高?
备注:
清理本身只是在任何特殊字符之前加上反斜杠。
清理是必需的,必须在join
发生之前应用于每一列。这是一个技术限制,因为$COLUMN_DELIM_STR
可能包含特殊字符,我们需要它们不进行转义。此外,$COLUMN_DELIM_STR
的长度和值可能会在脚本运行之间有所不同。
可以预先确定列数,但不能确定列名称或数据类型。该脚本不知道哪些列可能包含或不包含需要转义的特殊字符。
如果有更好的方法来清理列数据,请随时提出建议。我愿意接受其他想法。
答案 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)
对我来说,
测试线束加上环路每个元素需要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