【发布时间】: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<char>,正如其他评论者指出的那样,只需在stree.has等方法中使用(string, startIndex, endIndex)。在方法内部使用字符串索引器 (s[i]),它返回charw/o 分配。
标签: c# performance substring