【问题标题】:performance issue with substr on a very long UTF-8 stringsubstr 在很长的 UTF-8 字符串上的性能问题
【发布时间】:2014-09-20 07:51:10
【问题描述】:

我在一个很长的 UTF-8 字符串(约 250,000,000 个字符)上使用 substr。 问题是我的程序几乎在第 200,000,000 个字符附近冻结。

有人知道这个问题吗?我有哪些选择?

当我使用后缀数组索引文档时,我需要:

  1. 让我的绳子保持在一个整体;
  2. 使用索引访问可变长度子字符串。

至于 MWE:

use strict;
use warnings;
use utf8;

my $text = 'あいうえお' x 50000000;

for( my $i = 0 ; $i < length($text) ; $i++ ){
    print "\r$i";
    my $char = substr($text,$i,1);
}
print "\n";

【问题讨论】:

  • 请注意,对于这么大的字符串,它可能会因为您最终使用虚拟内存而变慢。
  • 当您说您需要索引时,您的实际意思是您需要随机访问字符串字符?
  • @salva 是的。但是我会说“可变长度的子字符串”而不是“字符”。

标签: perl unicode utf-8 substring


【解决方案1】:

Perl 有两种字符串存储格式。一种能够存储 8 位字符,另一种能够存储 72 位字符(实际上限制为 32 或 64)。您的字符串必须使用后一种格式。这种宽字符格式使用每个字符的可变字节数,就像 UTF-8 一样。

以第一种格式查找字符串的第 i 个元素很简单:将偏移量添加到字符串指针。对于第二种格式,查找第 i 个字符需要从头扫描字符串,就像您必须从头扫描文件才能找到第 n 行.有一种机制可以在发现字符串时缓存有关字符串的信息,但它并不完美。

如果每个字符使用固定数量的字节,问题就会消失。

use utf8;

use Encode qw( encode );

my $text = 'あいうえお' x 50000000;

my $packed = encode('UCS-4le', $text);
for my $i (0..length($packed)/4) {
    print "\r$i";
    my $char = chr(unpack('V', substr($packed, $i*4, 4)));
}

print "\n";

请注意,该字符串将为平假名字符多使用 33% 的内存。也可能没有,因为没有缓存了。

【讨论】:

  • 谢谢。在您的示例中unpack 返回一个四字节整数 rigth?我应该如何继续将我的角色作为字符串?目前我正在做chr unpack('V', substr($packed, $i*4, 4));,这很好;只是想知道是否有办法使用打包/解包模式来做到这一点......
  • 哎呀,是的,不小心放弃了chr。好吧,你可以使用decode,但我认为这样会更快。
【解决方案2】:

在您的特定示例中,您可以在处理 $text 字符串的开头删除字符以避免线性查找:

use utf8;
use Encode qw( encode );
$| = 1;
my $text = 'あいうえお' x 50000000;

while ($text ne '') {
    print ".";
    my $char = substr($text, 0, 1, '');
}
print "\n";

【讨论】:

  • 正如我的帖子中所说,我不想更改字符串。无论如何,谢谢。
【解决方案3】:

这是 Perl 5.20.0 的错误下列出的一个已知问题:

http://perldoc.perl.org/perlunicode.html#Speed
最重要的部分是我引用的第一段:

速度

处理 UTF-8 编码的字符串时,某些函数比处理字节编码的字符串要慢。当底层数据进行字节编码时,所有需要跳过字符(例如 length()substr()index())或匹配正则表达式的函数都可以更快地工作。

在 Perl 5.8.0 中,缓慢通常是相当惊人的;在 Perl 5.8.1 中引入了一个缓存方案,它有望使缓慢变得不那么引人注目,至少对于某些操作而言。通常,使用 UTF-8 编码字符串的操作仍然较慢。例如,像 \p{Nd} 这样的 Unicode 属性(字符类)比 \d 等更简单的对应物要慢很多(5-20​​ 倍)(同样,有数百个 Unicode 字符匹配 @987654327 @ 与匹配 d 的 10 个 ASCII 字符比较。

避免它的最简单方法是使用字节字符串而不是 unicode 字符串。

【讨论】:

  • 1) 我不知道你为什么提到 5.20,因为 Perl 一直将可变宽度字符用于宽字符串。 2) OP 的字符都不适合字节,所以你的建议没有意义。
  • @ikegami:我提到了 5.20,因为我不知道他们什么时候开始使用可变宽度字符,并且引用来自该手册(Perl 1 已经有了它们吗?我怀疑...... )。如果他使用字节串,他负责在适当的地方维护 unicode 文本语义,但从好的方面来说,他不会因为 Perl 实现这样做而导致速度变慢。
  • 关于“Perl 1 已经拥有它们了吗?” Perl 1 不支持大于 8 位的字符。大于 8 位的字符在内部始终是可变宽度的。
  • Re“如果他使用字节串”,同样,他不能使用字节串。他使用的字符不适合 8 位。如果您的意思是他应该处理文本的 UTF-8 编码形式,那会使事情变得慢得多,因为您必须在 perl 中做同样的工作,但在 Perl 中而不是在 C 中。
【解决方案4】:

我建议你使用正则表达式而不是substr

Benchmarking 这两种方法表明正则表达式快了近 100 倍:

use strict;
use warnings;
use utf8;

my $text = 'あいうえお' x 50_000;

sub mysubstr {
    for( my $i = 0 ; $i < length($text) ; $i++ ){
        my $char = substr($text,$i,1);
    }
}

sub myregex {
    while ($text =~ /(.)/g) {
        my $char = $1;
    }
}

use Benchmark qw(:all) ;

timethese(10, {
    'substr' => \&mysubstr,
    'regex'  => \&myregex,
});

输出:

Benchmark: timing 10 iterations of regex, substr...
     regex:  2 wallclock secs ( 2.18 usr +  0.00 sys =  2.18 CPU) @  4.58/s (n=10)
    substr: 198 wallclock secs (184.66 usr +  0.16 sys = 184.81 CPU) @  0.05/s (n=10)

【讨论】:

  • 谢谢。但是,我需要使用索引来访问我的子字符串。
  • 您是否真的需要使用index,或者您可能正在遭受XY Problem 的困扰?正则表达式可能也可以处理您正在执行的任何基本搜索,因此它有可能完全解决您使用 utf8 字符串的可伸缩性问题。
  • 我正在使用后缀数组索引一个巨大的文档。所以,是的,我需要使用索引。我的 MWE 足以指出我的问题,但我会编辑我的帖子以澄清问题。
  • 虽然这对 OP 没有帮助,但对于可以重构使用它的代码来说,它是一个很好的解决方案。我应用它来将代码从二次时间转换为行长度的线性时间。代码最初有线性时间,几年前在 pre-utf-8 天......
猜你喜欢
  • 2011-04-19
  • 1970-01-01
  • 2023-03-25
  • 1970-01-01
  • 1970-01-01
  • 2016-03-12
  • 1970-01-01
  • 1970-01-01
  • 2016-01-01
相关资源
最近更新 更多