如果我只有ReadOnlySpan切片,连接字符串的最有效方法是什么?
简化示例:
public class Program {
public string ConcatSpans(string longstring) {
var span = longstring.AsSpan();
var sb = new StringBuilder(longstring.Length);
sb.Append(span.Slice(40, 10));
sb.Append(span.Slice(30, 10));
sb.Append(span.Slice(20, 10));
sb.Append(span.Slice(10, 10));
sb.Append(span.Slice(0, 10));
return sb.ToString();
}
[Benchmark]
public void ConcatSpansBenchmark() {
ConcatSpans("aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee");
}
public static void Main(string[] args) {
var summary = BenchmarkRunner.Run<Program>();
}
}
结果:
BenchmarkDotNet=v0.11.2, OS=Windows 10.0.17134.345 (1803/April2018Update/Redstone4)
Intel Core i5-2500K CPU 3.30GHz (Sandy Bridge), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=2.1.403
[Host] : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
DefaultJob : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
Method | Mean | Error | StdDev | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |
--------------------- |---------:|---------:|---------:|------------:|------------:|------------:|--------------------:|
ConcatSpansBenchmark | 126.6 ns | 1.712 ns | 1.601 ns | 0.0966 | - | - | 304 B |
StringBuilder
真的是我们能做的最好的吗?有没有比这更快的方法了?分配更少?毕竟StringBuilder
对象本身就是一个堆对象。
如果有一个ref struct
StringBuilder
仅保留对ReadOnlySpans
的引用,而最后一个ToString
仅分配一个字符串对象?
答案 0 :(得分:4)
具有多个(但已知)输入范围的方案非常适合“预分配虚拟字符串,然后假装字符串是可变的,并在将其公开之前将其覆盖”方案。这看起来有些粗糙,但是在处理字符串(尤其是不连续的缓冲区等)时,此技巧在IO代码中非常常见,因此很好理解和支持。
我们到了(编辑:现在添加了“混合”方法,该方法避免了所有Slice()
的调用,而无需unsafe
):
Method | Mean | Error | StdDev | Median |
------------------------------ |---------:|----------:|----------:|---------:|
ConcatSpansBenchmark | 97.17 ns | 2.1335 ns | 4.0072 ns | 97.20 ns |
OverwiteStringBenchmark | 63.34 ns | 1.2914 ns | 2.0854 ns | 62.29 ns |
UnsafeOverwriteBenchmark | 17.95 ns | 0.3697 ns | 0.3796 ns | 17.80 ns |
OverwiteStringHybridBenchmark | 53.59 ns | 0.5534 ns | 0.5176 ns | 53.49 ns |
注意:涉及MemoryMarshal.*
,Unsafe.*
或unsafe
关键字的任何内容都是“我知道我在做什么...爆炸的任何内容都可能是我的错”。 >
代码:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
public class Program
{
public string ConcatSpans(string longstring)
{
var span = longstring.AsSpan();
var sb = new StringBuilder(longstring.Length);
sb.Append(span.Slice(40, 10));
sb.Append(span.Slice(30, 10));
sb.Append(span.Slice(20, 10));
sb.Append(span.Slice(10, 10));
sb.Append(span.Slice(0, 10));
return sb.ToString();
}
public string OverwiteString(string longstring)
{
var span = longstring.AsSpan();
var s = new string('\0', longstring.Length);
var writeable = MemoryMarshal.AsMemory(s.AsMemory()).Span;
span.Slice(40, 10).CopyTo(writeable);
writeable = writeable.Slice(10);
span.Slice(30, 10).CopyTo(writeable);
writeable = writeable.Slice(10);
span.Slice(20, 10).CopyTo(writeable);
writeable = writeable.Slice(10);
span.Slice(10, 10).CopyTo(writeable);
writeable = writeable.Slice(10);
span.Slice(0, 10).CopyTo(writeable);
return s;
}
public string OverwiteStringHybrid(string longstring)
{
var source = MemoryMarshal.AsBytes(MemoryMarshal.AsMemory(longstring.AsMemory()).Span);
var s = new string('\0', longstring.Length);
var target = MemoryMarshal.AsBytes(MemoryMarshal.AsMemory(s.AsMemory()).Span);
Unsafe.CopyBlock(ref target[0], ref source[40 * sizeof(char)], 10 * sizeof(char));
Unsafe.CopyBlock(ref target[10], ref source[30 * sizeof(char)], 10 * sizeof(char));
Unsafe.CopyBlock(ref target[20], ref source[20 * sizeof(char)], 10 * sizeof(char));
Unsafe.CopyBlock(ref target[30], ref source[10 * sizeof(char)], 10 * sizeof(char));
Unsafe.CopyBlock(ref target[40], ref source[0], 10 * sizeof(char));
return s;
}
public unsafe string UnsafeOverwrite(string longstring)
{
var s = new string('\0', longstring.Length);
fixed (char* source = longstring)
fixed (char* target = s)
{
Unsafe.CopyBlock(target, source + 40, 10 * sizeof(char));
Unsafe.CopyBlock(target + 10, source + 30, 10 * sizeof(char));
Unsafe.CopyBlock(target + 20, source + 20, 10 * sizeof(char));
Unsafe.CopyBlock(target + 30, source + 10, 10 * sizeof(char));
Unsafe.CopyBlock(target + 40, source, 10 * sizeof(char));
}
return s;
}
[Benchmark]
public void ConcatSpansBenchmark()
=> ConcatSpans("aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee");
[Benchmark]
public void OverwiteStringBenchmark()
=> OverwiteString("aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee");
[Benchmark]
public void UnsafeOverwriteBenchmark()
=> UnsafeOverwrite("aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee");
[Benchmark]
public void OverwiteStringHybridBenchmark()
=> OverwiteStringHybrid("aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee");
public static void Main(string[] args)
=> BenchmarkRunner.Run<Program>();
}
注意:通常,要从切片中获取unsafe
代码:
使用C#7.3:
fixed(char* p = theSpan)
{
...
}
否则:
fixed(char* p = &MemoryMarshal.GetReference(theSpan))
{
}