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 秒的开销,比替换本身还要多。 (通过完全删除循环验证了这一点。)
总结:
- 清理循环大约需要总执行时间的 1/3,我的测试数据大约需要 30 秒。根据源数据中的列数,它会按比例增加时间。
- 我的测试数据的清理循环 (
$col =~ s/...//g;
) 的替换部分需要 12 秒。
- 剩下的18秒是for循环结构本身。
问题:
如何提高消毒步骤的性能?
奖励:为什么 for 循环开销很高?
备注:
清理本身只是在任何特殊字符前放置一个反斜杠。
需要进行清理,并且必须在 join
发生之前对每一列进行清理。这是一个技术限制,因为 $COLUMN_DELIM_STR
可能包含特殊字符,我们需要它们 而不是 被转义。此外,$COLUMN_DELIM_STR
的长度和值可能会因脚本运行而异。
可以预先确定列数,但不能预先确定列名或数据类型。该脚本事先不知道哪些列可能包含或不包含需要转义的特殊字符。
如果有更好的清理列数据的方法,请随时提出建议。我愿意接受其他想法。
对我来说,
- 测试工具加上替换每个元素需要 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 @$rows
与 for (@$rows) { s/1/2/ }
.
的编译几乎相同
我正在开发一个 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 秒的开销,比替换本身还要多。 (通过完全删除循环验证了这一点。)
总结:
- 清理循环大约需要总执行时间的 1/3,我的测试数据大约需要 30 秒。根据源数据中的列数,它会按比例增加时间。
- 我的测试数据的清理循环 (
$col =~ s/...//g;
) 的替换部分需要 12 秒。 - 剩下的18秒是for循环结构本身。
问题:
如何提高消毒步骤的性能?
奖励:为什么 for 循环开销很高?
备注:
清理本身只是在任何特殊字符前放置一个反斜杠。
需要进行清理,并且必须在
join
发生之前对每一列进行清理。这是一个技术限制,因为$COLUMN_DELIM_STR
可能包含特殊字符,我们需要它们 而不是 被转义。此外,$COLUMN_DELIM_STR
的长度和值可能会因脚本运行而异。可以预先确定列数,但不能预先确定列名或数据类型。该脚本事先不知道哪些列可能包含或不包含需要转义的特殊字符。
如果有更好的清理列数据的方法,请随时提出建议。我愿意接受其他想法。
对我来说,
- 测试工具加上替换每个元素需要 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/sfor
(0 个元素):1041030.65/ss///
: 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 @$rows
与 for (@$rows) { s/1/2/ }
.