[英]Fastest way to replace multiple strings in a huge string
我正在寻找替换大(~1mb)字符串的多个(~500)子字符串的最快方法。 无论我尝试过什么,似乎 String.Replace 都是最快的方法。
我只关心最快的方式。 不是代码可读性、可维护性等。我不在乎是否需要使用不安全的代码或预处理原始字符串。
每次替换迭代都会用其他字符串替换字符串上的 ABC(每次替换迭代都不同)。 要替换的字符串始终相同 - ABC 将始终是 ABC。 从不ABD。 因此,如果有400.000
万次替换迭代。 相同的字符串 - ABC - 每次都会被其他(不同的)字符串替换。
我可以控制什么是 ABC。 只要不影响结果,我可以让它超短或超长。 显然 ABC 不能是hello因为 hello 将作为一个单词存在于大多数输入字符串中。
示例输入: ABCDABCABCDABCABCDABCABCDABCD
从字符串替换示例: BC
用字符串替换示例: AA, BB, CC, DD, EE (5 iterations)
示例输出:
AAADAAAAAADAAAAAADAAAAAADAAAD
ABBDABBABBDABBABBDABBABBDABBD
ACCDACCACCDACCACCDACCACCDACCD
ADDDADDADDDADDADDDADDADDDADDD
AEEDAEEAEEDAEEAEEDAEEAEEDAEED
平均情况:输入字符串为 100-200kb,有 40.000 次替换迭代。 最坏的情况:输入字符串是 1-2mb,有 400.000 次替换迭代。
我可以做任何事情。 并行做,做不安全,等等。我怎么做都无所谓。 重要的是它需要尽可能快。
由于我对这个问题有点兴趣,所以我制定了一些解决方案。 通过核心优化,可能会下降更多。
获取最新源码: https : //github.com/ChrisEelmaa/StackOverflow/blob/master/FastReplacer.cs
和输出
------------------------------------------------------- | Implementation | Average | Separate runs | |----------------------+---------+--------------------| | Simple | 3485 | 9002, 4497, 443, 0 | | SimpleParallel | 1298 | 3440, 1606, 146, 0 | | ParallelSubstring | 470 | 1259, 558, 64, 0 | | Fredou unsafe | 356 | 953, 431, 41, 0 | | Unsafe+unmanaged_mem | 92 | 229, 114, 18, 8 | -------------------------------------------------------
在制作自己的替换方法时,您可能不会击败 .NET 人员,它很可能已经在使用 unsafe。 我相信如果你完全用 C 语言编写它,你可以把它降低两倍。
我的实现可能有问题,但您可以大致了解。
使用unsafe
并编译为 x64
结果:
Implementation | Exec | GC
#1 Simple | 4706ms | 0ms
#2 Simple parallel | 2265ms | 0ms
#3 ParallelSubstring | 800ms | 21ms
#4 Fredou unsafe | 432ms | 15ms
拿Erti-Chris Eelmaa
的代码并用这个替换我之前的代码。
我不认为我会再做一次迭代,但我确实学到了一些不安全的东西,这是一件好事:-)
private unsafe static void FredouImplementation(string input, int inputLength, string replace, string[] replaceBy)
{
var indexes = new List<int>();
//input = "ABCDABCABCDABCABCDABCABCDABCD";
//inputLength = input.Length;
//replaceBy = new string[] { "AA", "BB", "CC", "DD", "EE" };
//my own string.indexof to save a few ms
int len = inputLength;
fixed (char* i = input, r = replace)
{
int replaceValAsInt = *((int*)r);
while (--len > -1)
{
if (replaceValAsInt == *((int*)&i[len]))
{
indexes.Add(len--);
}
}
}
var idx = indexes.ToArray();
len = indexes.Count;
Parallel.For(0, replaceBy.Length, l =>
Process(input, inputLength, replaceBy[l], idx, len)
);
}
private unsafe static void Process(string input, int len, string replaceBy, int[] idx, int idxLen)
{
var output = new char[len];
fixed (char* o = output, i = input, r = replaceBy)
{
int replaceByValAsInt = *((int*)r);
//direct copy, simulate string.copy
while (--len > -1)
{
o[len] = i[len];
}
while (--idxLen > -1)
{
((int*)&o[idx[idxLen]])[0] = replaceByValAsInt;
}
}
//Console.WriteLine(output);
}
听起来您正在对字符串进行标记? 我会考虑生成一个缓冲区并索引你的令牌。 或者使用模板引擎
作为一个天真的例子,您可以使用代码生成来制作以下方法
public string Produce(string tokenValue){
var builder = new StringBuilder();
builder.Append("A");
builder.Append(tokenValue);
builder.Append("D");
return builder.ToString();
}
如果您运行迭代次数足够多,则构建模板的时间将是物有所值的。 然后,您还可以并行调用该方法,而不会产生副作用。 还要看看你的字符串实习
您可能不会获得比 String.Replace 更快的任何东西(除非您使用本机),因为 iirc String.Replace 是在 CLR 本身中实现的,以获得最佳性能。 如果您想要 100% 的性能,您可以通过 C++/CLI 方便地与本机 ASM 代码交互并从那里开始。
我对 Fredou 的代码做了一个变体,它需要较少的比较,因为它适用于int*
而不是char*
。 对于一个长度为n
的字符串,它仍然需要n
次迭代,它只需要做更少的比较。 你可以有n/2
次迭代,如果该字符串整齐2(所以要替换的字符串只能出现在索引0,2,4,6,8,等)对齐,甚至n/4
,如果它是由4对齐(你会使用long*
)。 我不太擅长像这样摆弄,所以有人可能能够在我的代码中找到一些可能更有效的明显缺陷。 我验证了我的变化结果与简单string.Replace
的结果相同。
此外,我希望在 500x string.Copy
一些收益。 string.Copy
它确实如此,但还没有研究过。
我的结果(弗雷多二世):
IMPLEMENTATION | EXEC MS | GC MS
#1 Simple | 6816 | 0
#2 Simple parallel | 4202 | 0
#3 ParallelSubstring | 27839 | 4
#4 Fredou I | 2103 | 106
#5 Fredou II | 1334 | 91
所以大约有 2/3 的时间(x86,但 x64 大致相同)。
对于此代码:
private unsafe struct TwoCharStringChunk
{
public fixed char chars[2];
}
private unsafe static void FredouImplementation_Variation1(string input, int inputLength, string replace, TwoCharStringChunk[] replaceBy)
{
var output = new string[replaceBy.Length];
for (var i = 0; i < replaceBy.Length; ++i)
output[i] = string.Copy(input);
var r = new TwoCharStringChunk();
r.chars[0] = replace[0];
r.chars[1] = replace[1];
_staticToReplace = r;
Parallel.For(0, replaceBy.Length, l => Process_Variation1(output[l], input, inputLength, replaceBy[l]));
}
private static TwoCharStringChunk _staticToReplace ;
private static unsafe void Process_Variation1(string output, string input, int len, TwoCharStringChunk replaceBy)
{
int n = 0;
int m = len - 1;
fixed (char* i = input, o = output, chars = _staticToReplace .chars)
{
var replaceValAsInt = *((int*)chars);
var replaceByValAsInt = *((int*)replaceBy.chars);
while (n < m)
{
var compareInput = *((int*)&i[n]);
if (compareInput == replaceValAsInt)
{
((int*)&o[n])[0] = replaceByValAsInt;
n += 2;
}
else
{
++n;
}
}
}
}
带有固定缓冲区的结构在这里并不是绝对必要的,可以用一个简单的int
字段替换,但是将char[2]
扩展为char[3]
并且这段代码也可以用于三个字母的字符串,即如果它是int
字段,则不可能。
它还需要对 Program.cs 进行一些更改,所以这里是完整的要点:
https://gist.github.com/JulianR/7763857
编辑:我不知道为什么我的 ParallelSubstring 这么慢。 我在 x86 或 x64 中以 Release 模式运行 .NET 4,没有调试器。
我有一个项目类似的问题,我已经实现了一个正则表达式的解决方案,以在文件上执行多个和不区分大小写的替代品。
出于效率目的,我设置了仅通过原始字符串一次的标准。
我已经发布了一个简单的控制台应用程序来测试https://github.com/nmcc/Spikes/tree/master/StringMultipleReplacements 上的一些策略
Regex 解决方案的代码与此类似:
Dictionary<string, string> replacements = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
// Fill the dictionary with the proper replacements:
StringBuilder patternBuilder = new StringBuilder();
patternBuilder.Append('(');
bool firstReplacement = true;
foreach (var replacement in replacements.Keys)
{
if (!firstReplacement)
patternBuilder.Append('|');
else
firstReplacement = false;
patternBuilder.Append('(');
patternBuilder.Append(Regex.Escape(replacement));
patternBuilder.Append(')');
}
patternBuilder.Append(')');
var regex = new Regex(patternBuilder.ToString(), RegexOptions.IgnoreCase);
return regex.Replace(sourceContent, new MatchEvaluator(match => replacements[match.Groups[1].Value]));
编辑:在我的计算机上运行测试应用程序的执行时间是:
由于您的输入字符串可以长达 2Mb,因此我预计不会出现任何内存分配问题。 您可以将所有内容加载到内存中并替换您的数据。
如果从BC
您总是需要替换AA
,则String.Replace
就可以了。 但是,如果您需要更多控制,可以使用Regex.Replace
:
var input = "ABCDABCABCDABCABCDABCABCDABCD";
var output = Regex.Replace(input, "BC", (match) =>
{
// here you can add some juice, like counters, etc
return "AA";
});
我的方法有点像模板 - 它获取输入字符串并拉出(删除)要替换的子字符串。 然后它将字符串的剩余部分(模板)与新的替换子字符串组合起来。 这是在并行操作(模板 + 每个替换字符串)中完成的,该操作构建输出字符串。
我认为我上面解释的代码可能会更清楚。 这使用您从上面的示例输入:
const char splitter = '\t'; // use a char that will not appear in your string
string input = "ABCDABCABCDABCABCDABCABCDABCD";
string oldString = "BC";
string[] newStrings = { "AA", "BB", "CC", "DD", "EE" };
// In input, replace oldString with tabs, so that we can do String.Split later
var inputTabbed = input.Replace(oldString, splitter.ToString());
// ABCDABCABCDABCABCDABCABCDABCD --> A\tDA\tA\tDA\tA\tDA\tA\tDA\tD
var inputs = inputTabbed.Split(splitter);
/* inputs (the template) now contains:
[0] "A"
[1] "DA"
[2] "A"
[3] "DA"
[4] "A"
[5] "DA"
[6] "A"
[7] "DA"
[8] "D"
*/
// In parallel, build the output using the template (inputs)
// and the replacement strings (newStrings)
var outputs = new List<string>();
Parallel.ForEach(newStrings, iteration =>
{
var output = string.Join(iteration, inputs);
// only lock the list operation
lock (outputs) { outputs.Add(output); }
});
foreach (var output in outputs)
Console.WriteLine(output);
输出:
AAADAAAAAADAAAAAADAAAAAADAAAD
ABBDABBABBDABBABBDABBABBDABBD
ACCDACCACCDACCACCDACCACCDACCD
ADDDADDADDDADDADDDADDADDDADDD
AEEDAEEAEEDAEEAEEDAEEAEEDAEED
所以你可以做一个比较,这是一个完整的方法,可以在 Erti-Chris Eelmaa 的测试代码中使用:
private static void TemplatingImp(string input, string replaceWhat, IEnumerable<string> replaceIterations)
{
const char splitter = '\t'; // use a char that will not appear in your string
var inputTabbed = input.Replace(replaceWhat, splitter.ToString());
var inputs = inputTabbed.Split(splitter);
// In parallel, build the output using the split parts (inputs)
// and the replacement strings (newStrings)
//var outputs = new List<string>();
Parallel.ForEach(replaceIterations, iteration =>
{
var output = string.Join(iteration, inputs);
});
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.