【翻译】Are String.Equals And String.IndexOf That Much Faster In .NET Core 2.1?

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

这次的性能测试结果符合我们的预期,从上面的测试我们可以看出:

  1. 如果是相同的字符串实例 .net core 做出了相当大的优化
  2. 如果字符串很短,则两者没有太大的性能差距
  3. 如果是字符串很长,则 .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 在这两个函数上的优势就不一定存在了。

«   2023年9月   »
123
45678910
11121314151617
18192021222324
252627282930
网站分类
文章归档

Powered By Z-BlogPHP 1.6.5 Valyria

Copyright csharptools.cn Rights Reserved. 桂ICP备17007292号-1