String.Substring() 似乎是这段代码的瓶颈

Posted

技术标签:

【中文标题】String.Substring() 似乎是这段代码的瓶颈【英文标题】:String.Substring() seems to bottleneck this code 【发布时间】:2019-01-11 09:47:49 【问题描述】:

简介

我有一个我很喜欢的算法,这是我很久以前制作的,我总是用新的编程语言、平台等编写和重写它作为某种基准。虽然我的主要编程语言是 C#,但我只是从字面上复制粘贴代码并稍微更改语法,用 Java 构建它,发现它的运行速度提高了 1000 倍。

守则

有相当多的代码,但我只会介绍这个似乎是主要问题的 sn-p:

for (int i = 0; i <= s1.Length; i++) 

    for (int j = i + 1; j <= s1.Length - i; j++)
    
        string _s1 = s1.Substring(i, j);
        if (tree.hasLeaf(_s1))
         ...

数据

需要指出的是,这个特定测试中的字符串 s1 的长度为 1 百万个字符 (1MB)。

测量

我在 Visual Studio 中分析了我的代码执行,因为我认为我构建树的方式或遍历它的方式不是最佳的。检查结果后,string _s1 = s1.Substring(i, j); 行似乎可容纳超过 90% 的执行时间!

其他观察

我注意到的另一个区别是,尽管我的代码是单线程的,但 Java 设法使用所有 8 个内核(100% 的 CPU 利用率)来执行它,而即使使用 Parallel.For() 和多线程技术,我的 C# 代码也能做到最多使用 35-40%。由于算法随内核数量(和频率)线性扩展,我对此进行了补偿,Java 中的 sn-p 执行速度仍然快 100-1000 倍。

推理

我认为发生这种情况的原因与 C# 中的字符串是不可变的这一事实有关,因此 String.Substring() 必须创建一个副本,并且由于它位于具有多次迭代的嵌套 for 循环中,我推测很多复制和垃圾收集正在进行中,但是,我不知道 Substring 在 Java 中是如何实现的。

问题

此时我有哪些选择?子字符串的数量和长度无法解决(这已经被最大限度地优化了)。有没有我不知道的方法(或者可能是数据结构)可以为我解决这个问题?

请求的最小实现(来自 cmets)

我省略了后缀树的实现,在构造中为 O(n),在遍历中为 O(log(n))

public static double compute(string s1, string s2)

    double score = 0.00;
    suffixTree stree = new suffixTree(s2);
    for (int i = 0; i <= s1.Length; i++) 
    
        int longest = 0;
        for (int j = i + 1; j <= s1.Length - i; j++)
        
            string _s1 = s1.Substring(i, j);
            if (stree.has(_s1))
            
                score += j - i;
                longest = j - i;
            
            else break;
         ;

        i += longest;
    ;
    return score;

分析器的截图 sn-p

请注意,这是使用大小为 300.000 个字符的字符串 s1 进行的测试。由于某种原因,100 万个字符在 C# 中永远不会完成,而在 Java 中只需要 0.75 秒。消耗的内存和垃圾收集的数量似乎并不表示内存问题。峰值约为 400 MB,但考虑到巨大的后缀树,这似乎是正常的。也没有发现奇怪的垃圾收集模式。

【问题讨论】:

String 在 Java 中也是不可变的。你试过StringBuilder吗? 我猜你有内存问题。你看过吗? 这八个 Java 内核中的七个可能用于垃圾收集您的子字符串 :) 哈哈,可能是它.. :')。从语法上讲,您是否知道如何在不一直在 C# 中复制的情况下获取子字符串?我不能只使用 const char*& 并像在 C++ 中那样使用指针算法.. 直到 C# 获得 Span&lt;char&gt;,正如其他评论者指出的那样,只需在 stree.has 等方法中使用 (string, startIndex, endIndex)。在方法内部使用字符串索引器 (s[i]),它返回 char w/o 分配。 【参考方案1】:

问题来源

在经历了两天三夜的光荣战斗(以及来自 cmets 的惊人想法和想法)之后,我终于设法解决了这个问题!

我想为遇到类似问题的任何人发布答案由string.Substring(i, j) 完成(它必须制作一个副本,因为C# 字符串是不可变的,无法绕过它)或者string.Substring(i, j) 在同一个字符串上被调用了很多次(比如在我的嵌套for循环中)给出垃圾收集器很难,或者就像我的情况一样!

尝试

我已经尝试了很多建议的东西,例如 StringBuilderStreams、使用 IntptrMarshal 的非托管内存分配strong> 在unsafe 块内,甚至创建一个 IEnumerable 和 yield 通过引用返回给定位置内的字符。所有这些尝试最终都失败了,因为必须进行某种形式的数据连接,因为我没有简单的方法可以在不影响性能的情况下逐个字符地遍历我的树。如果只有一种方法可以一次跨越一个数组中的多个内存地址,就像您可以在 C++ 中使用一些指针算术一样.. 除非有.. (感谢@Ivan Stoev 的评论)

解决方案

解决方案是使用System.ReadOnlySpan&lt;T&gt;(不能是System.Span&lt;T&gt;,因为字符串是不可变的),除其他外,它允许我们在现有数组中读取内存地址的子数组,而无需创建副本。

贴出这段代码:

string _s1 = s1.Substring(i, j);
if (stree.has(_s1))

    score += j - i;
    longest = j - i;

改为如下:

if (stree.has(i, j))

    score += j - i;
    longest = j - i;

stree.has() 现在采用两个整数(子字符串的位置和长度)并执行以下操作:

ReadOnlySpan<char> substr = s1.AsSpan(i, j);

注意substr 变量实际上是对初始s1 数组的字符子集的引用,而不是副本! (s1 变量已可从此函数访问)

请注意,在撰写本文时,我使用的是 C#7.2 和 .NET Framework 4.6.1,这意味着要获得 Span 功能,我必须转到 Project > Manage NuGet Packages,勾选“Include prerelease”复选框并浏览 System.Memory 并安装它。

重新运行初始测试(在长度为 100 万个字符的字符串上,即 1MB)速度从 2 多分钟(我在 2 分钟后放弃等待)增加到约 86 毫秒!!

【讨论】:

可以将切片作为创建 Span 的一部分:s1.AsSpan(i, j),应该快一点吗? 可能是因为我不知道 span 究竟是如何实现的。它似乎并没有更快,但直觉上认为它是..至少我是这么认为的。我将编辑我的帖子并使用您的建议,因为这可能是使用 span @BenAdams 的预期方式 更多关于 Span 的信息,如果你有兴趣的话。 (只是为了完整性)@9​​87654321@

以上是关于String.Substring() 似乎是这段代码的瓶颈的主要内容,如果未能解决你的问题,请参考以下文章

C# String.Substring 等效于 StringBuilder?

如何删除有问题的服务人员,或实施“终止开关”?

Instagram max_id 不起作用

java String.substring 乱码

String.subString() 和 String.subSequence() 有啥区别

string.substring 与 string.take