简介
我有一个很喜欢的算法,我很早以前就做了,我总是用新的编程语言,平台等来编写和重写,以此作为某种基准。尽管我的主要编程语言是C#,但我只是从字面上复制粘贴了代码,并稍稍更改了语法,使用Java进行了构建,发现其运行速度提高了1000倍。
代码
有很多代码,但是我只打算介绍这个片段,这似乎是主要问题:
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中的代码段执行速度却快了100-1000倍。
理由
我认为发生这种情况的原因与以下事实有关:C#中的字符串是不可变的,因此String.Substring()必须创建一个副本,并且由于它位于嵌套的for循环中且包含许多迭代,因此我推测很多复制和垃圾回收的过程正在进行中,但是,我不知道Substring是如何在Java中实现的。
问题
目前我有什么选择?子字符串的数量和长度没有办法解决(这已经得到了最大程度的优化)。是否有我不知道的方法(或数据结构)可以为我解决这个问题?
要求的最低限度实施(根据评论)
我省略了后缀树的实现,后缀树在构造中为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;
}
探查器的屏幕截图
请注意,已使用字符串s1进行了测试,该字符串的大小为300.000个字符。出于某种原因,一百万个字符在C#中从未完成,而在Java中仅需要0.75秒。.消耗的内存和垃圾回收的数量似乎并不表示存在内存问题。峰值约为400 MB,但考虑到巨大的后缀树,这似乎是正常的。也没有发现奇怪的垃圾收集模式。
答案 0 :(得分:85)
问题来源
经过持续两天三夜的光荣战役(以及评论中令人惊奇的想法和想法),我终于设法解决了这个问题!
我想为遇到类似问题的任何人发布答案,其中string.Substring(i, j)
函数不是获取字符串子字符串的可接受解决方案,因为字符串太大或者您负担不起由string.Substring(i, j)
完成的复制(由于C#字符串是不可变的,因此无法复制,因此必须进行复制),或者string.Substring(i, j)
被同一字符串多次调用(例如在我的嵌套for循环)给垃圾收集器带来了麻烦,或者在我的情况下都是如此!
尝试
我尝试了许多建议的操作,例如 StringBuilder ,流,使用 Intptr 和 Marshal 的非托管内存分配在unsafe{}
块中,甚至创建IEnumerable并产生yield,都通过给定位置中的引用返回字符。所有这些尝试最终都以失败告终,因为必须进行某种形式的数据连接,因为没有一种简单的方法可以让我逐字遍历我的树而又不损害性能。如果只有一种方法可以一次跨越数组中的多个内存地址,就像您可以在C ++中使用某些指针算法那样。
(获得@Ivan Stoev的评论)
解决方案
该解决方案使用的是System.ReadOnlySpan<T>
(由于字符串是不可变的,因此无法System.Span<T>
),它使我们能够读取现有数组中的内存地址子数组而无需创建副本
这段代码发布了:
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功能,我必须转到“项目”>“管理NuGet程序包”,选中“包括预发行版”复选框,然后浏览以找到System.Memory并安装它。
重新运行初始测试(在长度为1百万个字符(即1MB)的字符串上),速度从2分钟以上(2分钟后我放弃等待)增加到约86毫秒!