Are String.Equals And String.IndexOf That Much Faster In .NET Core 2.1?
国际惯例,先上博文的原文地址
在本周,有一篇博文微软Bing搜索引擎转向.NET Core 2.1延迟降低34%(英文原始地址),里面提及切换到.net core后性能有很大的提升,特别是string.Equals 和 string.IndexOf / LastIndexOf方法。
.NET Core 2.1 中的多项改进带来了大量的性能改进,包括 string.Equals 和 string.IndexOf / LastIndexOf 的矢量化,它们提高了 HTML 渲染和操作等字符串繁重工作负载的性能。
期初,我们的博主没有想到这几个方法会有多大的影响,因为我们的应用里不会存在大量的的字符串搜索与比较功能。但是从bing的经验里看到,web系统里还是存在的大量的字符串查找与比较功能,否则bing团队不会特意说道这点。因此我们敬爱的博主就对几个方法做了一下测试。
String.Equals Performance Benchmarks
(在阅读结果前,最好先看下一节,里面有一些有趣的东西)
我们可以写一个超大的循环来做测试,也可以用 BenchmarkDotNet来做,当然我们的博主就这样做了。
public class MultipleRuntimeConfig : ManualConfig
{
public MultipleRuntimeConfig()
{
Add(Job.Default.With(CsProjCoreToolchain.NetCoreApp21).WithBaseline(true));
Add(Job.Default.With(CsProjClassicNetToolchain.Net472));
}
}
[Config(typeof(MultipleRuntimeConfig))]
public class StringEquals
{
private string String1 = "Hello World!";
private string String2 = "Hello World!";
[Benchmark]
public bool IsEqual() => String1.Equals(String2);
}
class Program
{
static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<StringEquals>();
Console.ReadLine();
}
}
这里需要指出,上面的代码会跑在2个不同的框架下,一个 .net core 2.1,另外一个 .net Framework 4.7.2。这两个都是目前最新版本。
测试本身很简单,就是比较两个 “Hello World!” 字符串,没太多花头。
通常代码的基准测试可以在自己的机器上运行,不同的机器上可能会存在不同的结果差异,在这里我们不是太关注测试花了多少时间,而是关注在代码在两个框架上时间的差异。
为此,我们的博主特意在Azure里租了一个VM来做测试。D2s_V3 型号的机器。2核CPU+8G内存(我怎么感觉有点浪费)。当然这个通常是web服务器的标配。
说了好多,让我们来看看结果吧:
Method | Toolchain | Mean | Error | Scaled |
---|---|---|---|---|
IsEqual | .NET Core 2.1 | 0.9438 ns | 0.0686 ns | 1.00 |
IsEqual | CsProjnet472 | 1.9381 ns | 0.0844 ns | 2.06 |
我们的博主信誓旦旦的保证,测量很多次,结果都是一样的,.Net Freamework 就是比 .net core 要慢一倍。
不信,可以按照下面的配置自己去撸一遍。
.NET Core 2.1.2 (CoreCLR 4.6.26628.05, CoreFX 4.6.26629.01), 64bit RyuJIT
.NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3062.0
String.Equals Performance Benchmarks Updated (2018-08-23)
因为我们的博主发布blog后,收到了网友的评论,发现测试用例有一些问题。
例子程序里,两个字符串都可以作为常量被编译器设置到同一个内存地址里,所以实际比较时,会变成同一个字符串实例的比较。
你应该新建一个字符串,这样就不会被编译器给优化掉
对于广大网友指出的问题,我们的博主很快的更新了它的测试用例
public class StringEquals
{
private string String1 = new string("Hello World!".ToCharArray());
private string String2 = new string("Hello World!".ToCharArray());
[Benchmark]
public bool IsEqual() => String1.Equals(String2);
}
so,这次是比较了2个不同的字符串实例,结果你会了解到:
Method | Toolchain | Mean | Error | Scaled |
---|---|---|---|---|
IsEqual | .NET Core 2.1 | 7.370 ns | 0.1855 ns | 1.00 |
IsEqual | CsProjnet472 | 7.152 ns | 0.1928 ns | 0.97 |
好吧,两者的差距相当的小,小到可以忽略不计,可能是因为字符串不够大的原因,我们再次修改测试用例,换更大的字符来测试:
[Config(typeof(MultipleRuntimeConfig))]
public class StringEquals
{
private string String1;
private string String2;
[GlobalSetup]
public void StringEqualsSetup()
{
for(int i=0; i < 100; i++)
{
String1 += "Hello World!";
}
String2 = new string(String1.ToCharArray());
}
[Benchmark]
public bool IsEqual() => String1.Equals(String2);
}
这次的字符串有12000个字节(在LOH分配了),让我们来瞅瞅结果:
Method | Toolchain | Mean | Error | StdDev | Scaled | ScaledSD |
---|---|---|---|---|---|---|
IsEqual | .NET Core 2.1 | 128.7 ns | 4.367 ns | 12.88 ns | 1.00 | 0.00 |
IsEqual | CsProjnet472 | 211.7 ns | 6.989 ns | 20.28 ns | 1.66 | 0.24 |
这次的性能测试结果符合我们的预期,从上面的测试我们可以看出:
- 如果是相同的字符串实例 .net core 做出了相当大的优化
- 如果字符串很短,则两者没有太大的性能差距
- 如果是字符串很长,则 .net core 会对性能有显著的提升
(这里补充思考一下,如果字符串数量从1.2k字节,上升到10k,100k,性能差距会有多少呢)
String.IndexOf Performance Benchmarks
接下来,我们来看一下IndexOf的表现。这个测试很有意思,因为在字符串上既可以按照string来查找,也可以按照char来查找。从GitHub上的某个pr来看,性能提升只针对char类型的查找,string的没差别。但我们不管了,两种查询方式都一起测测看。
public class MultipleRuntimeConfig : ManualConfig
{
public MultipleRuntimeConfig()
{
Add(Job.Default.With(CsProjCoreToolchain.NetCoreApp21).WithBaseline(true));
Add(Job.Default.With(CsProjClassicNetToolchain.Net472));
}
}
[Config(typeof(MultipleRuntimeConfig))]
public class IndexOf
{
public IEnumerable<string> hayStacks()
{
yield return haystackSmall;
yield return haystackLarge;
}
private string haystackSmall = "Hello World!";
private string haystackLarge;
public IndexOf()
{
for (int i = 0; i < 1000; i++)
{
haystackLarge += haystackSmall;
}
}
[Benchmark]
[ArgumentsSource(nameof(hayStacks))]
public int IndexOfString(string haystack) => haystack.IndexOf("1");
[Benchmark]
[ArgumentsSource(nameof(hayStacks))]
public int IndexOfChar(string haystack) => haystack.IndexOf('1');
}
class Program
{
static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<IndexOf>();
Console.ReadLine();
}
}
你会注意到,在测试用例里,我们为每个测试传入了2个产生,一个是12字节长的字符串,一个是12000个字节的字符串。这主要是在PR里说
对于比较长的字符串,在结尾匹配到或者根本没匹配到的时候,会有明显的优势。
因此,测试代码里,故意让字符串没能匹配成功。
Method | Toolchain | haystack | Mean | Error | Scaled |
---|---|---|---|---|---|
IndexOfString | CsProjnet472 | Hello World! | 184.194 ns | 3.6937 ns | 1.08 |
IndexOfChar | .NET Core 2.1 | Hello World! | 7.962 ns | 0.4588 ns | 1.00 |
IndexOfChar | CsProjnet472 | Hello World! | 12.305 ns | 0.2841 ns | 1.59 |
IndexOfString | .NET Core 2.1 | Hello(…)orld! [12000] | 39,964.455 ns | 781.2495 ns | 1.00 |
IndexOfString | CsProjnet472 | Hello(…)orld! [12000] | 40,476.489 ns | 805.1209 ns | 1.01 |
IndexOfChar | .NET Core 2.1 | Hello(…)orld! [12000] | 765.894 ns | 15.2256 ns | 1.00 |
IndexOfChar | CsProjnet472 | Hello(…)orld! [12000] | 7,522.823 ns | 147.9425 ns | 9.83 |
这里有一些小结论
对于”IndexOfString”,我们可以看到,在两个框架下差距不是很大,.net core 略微快一点点,这可能归结为它们用的还是一套算法。
对于”IndexOfChar”,但字符串很小的时候,差距不是很大,但一旦放大到大字符串,时间就不一样了,差不多有10倍的差距,.net core 干得漂亮。
最后的一点说明
以上测试仅限于 .net core 和 .net fx4.7的比较,因为 .net core 2.1 实现了c# 7.2 Span的新特性,而两个方法的内部算法里使用了这些特性。等未来 .net fx 4.8 推出后,可能也会实现这些特性,这时 .net core 在这两个函数上的优势就不一定存在了。