0

我正在使用此处找到的实现对.NET 和 C# 中的 StringReader 类进行一些研究: https ://referencesource.microsoft.com/#mscorlib/system/io/stringreader.cs

我做了一个小类,我认为它使用相同的基本实现来读取字符串,但令我惊讶的是,我的代码比 .NET StringReader 慢两倍多。

这是我的课:

public class DataReader
{
    private String source;
    private int pos;
    private int length;

    public DataReader(string data)
    {
        source = data;
        length = source.Length;
    }

    public int Peek()
    {
        if (pos == length) return -1;
        return source[pos];
    }

    public int Read()
    {
        if (pos == length) return -1;
        return source[pos++];
    }
}

这是我的测试代码:

using System;
using System.IO;
using System.Diagnostics;
using System.Text;
using System.Collections.Generic;                   

public class Program
{
    public static void Main()
    {
        var s = new String('x', 10000000);

        StringReaderTest(s);
        
        DataReaderTest(s);
    }
    
    private static void StringReaderTest(string s)
    {
        var stopwatch = new Stopwatch();
        
        stopwatch.Start();
        
        var reader = new StringReader(s);
        
        while (reader.Peek() > -1)
        {
            reader.Read();
        }
        
        stopwatch.Stop();
        
        Console.WriteLine(stopwatch.ElapsedMilliseconds);
    }

    private static void DataReaderTest(string s)
    {
        var stopwatch = new Stopwatch();
        
        stopwatch.Start();

        var reader = new DataReader(s);

        while (reader.Peek() > -1)
        {
            reader.Read();
        }

        stopwatch.Stop();
        
        Console.WriteLine(stopwatch.ElapsedMilliseconds);
    }
}

这是整个事情的 .NET 小提琴。 https://dotnetfiddle.net/MqbU5q

这是小提琴的输出。我的实现速度慢了一倍。

77
159

我一定错过了什么,有人可以解释一下吗?

4

1 回答 1

15

因此,首先,Stopwatch它不是一个合法的基准测试工具,它不合适的原因有很多。

您应该使用BenchmarkDotNet或类似的东西,它会在每次运行前预热预 JIT、垃圾收集、多次运行测试,并在调试时提醒您等。

下面是一个示例,说明如何生成更可靠的基准。

免责声明:在一个好的基准测试中应该有更多的理由,比如使用更真实的实际数据和用例表示。

测试代码

[SimpleJob(RuntimeMoniker.Net60, baseline: true)]  
public class ReaderTest
{ 
    private const string Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    private string _s;
    public static Random R = new(42);

    [Params(1000, 10000, 100000)] public int N;

    [GlobalSetup]
    public void Setup() => _s = new string(Enumerable.Repeat(Chars, N).Select(s => s[R.Next(s.Length)]).ToArray());

    public static void Main() => BenchmarkRunner.Run<ReaderTest>();

    [Benchmark]
    public int StringReader()
    {
        var stringReader = new StringReader(_s);
        var result = 0;
        while (stringReader.Peek() > -1)
            result ^= stringReader.Read();
        return result;
    }

    [Benchmark]
    public int DataReader()
    {
        var dataReader = new DataReader(_s);
        var result = 0;
        while (dataReader.Peek() > -1)
            result ^= dataReader.Read();      
        return result;
    }   
}

基准

环境

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1348 (21H2)
Intel Core i7-7700 CPU 3.60GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.100
  [Host]   : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT  [AttachedDebugger]
  .NET 6.0 : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT

Job=.NET 6.0  Runtime=.NET 6.0

结果

方法 ñ 意思是 错误 标准差 比率
字符串读取器 1000 3.424 我们 0.0186 我们 0.0174 我们 1.00
数据读取器 1000 1.459 我们 0.0126 我们 0.0098 我们 1.00
字符串读取器 10000 34.157 我们 0.2244 我们 0.2099 我们 1.00
数据读取器 10000 14.693 我们 0.0577 我们 0.0540 我们 1.00
字符串读取器 100000 348.650 我们 6.7857 我们 7.2606 我们 1.00
数据读取器 100000 146.257 我们 1.0318 我们 0.9651 我们 1.00

概括

CallVirt正如您所看到的(正如您所期望的),您的实现更快,检查更少,并且不必通过继承层来获得相同的结果(因此编译器不会发出在运行时

callvirt 指令调用对象的后期绑定方法。也就是说,方法是根据 obj 的运行时类型而不是方法指针中可见的编译时类来选择的。

于 2021-12-05T23:20:45.963 回答