如何衡量字符串实习?

How to measure string interning?

我正在尝试衡量应用程序中字符串驻留的影响。

我想到了这个:

class Program
{
    static void Main(string[] args)
    {
        _ = BenchmarkRunner.Run<Benchmark>();
    }
}

[MemoryDiagnoser]
public class Benchmark
{
    [Params(10000, 100000, 1000000)]
    public int Count { get; set; }

    [Benchmark]
    public string[] NotInterned()
    {
        var a = new string[this.Count];
        for (var i = this.Count; i-- > 0;)
        {
            a[i] = GetString(i);
        }
        return a;
    }

    [Benchmark]
    public string[] Interned()
    {
        var a = new string[this.Count];
        for (var i = this.Count; i-- > 0;)
        {
            a[i] = string.Intern(GetString(i));
        }
        return a;
    }

    private static string GetString(int i)
    {
        var result = (i % 10).ToString();
        return result;
    }
}

但我总是得到相同数量的分配。

是否有任何其他措施或诊断可以让我使用 string.Intern() 节省内存?

这里的主要问题是你想衡量什么样的影响?更具体地说:你的目标指标是什么?以下是一些示例:性能指标、内存流量、内存占用。

在 BenchmarkDotNet Allocated 列中,您可以获得内存流量。 string.Intern 在您的示例中无助于优化它,每个 (i % 10).ToString() 调用都会分配一个新字符串。因此,预计 BenchmarkDotNet 在分配列中显示相同的数字。

但是,string.Intern 应该可以帮助您在最后优化应用程序的内存占用(总托管堆大小,可以通过 GC.GetTotalMemory() 获取)。可以使用不带 BenchmarkDotNet 的简单控制台应用程序进行验证:

using System;

namespace ConsoleApp24
{
    class Program
    {
        private const int Count = 100000;
        private static string[] notInterned, interned;

        static void Main(string[] args)
        {
            var memory1 = GC.GetTotalMemory(true);
            notInterned = NotInterned();
            var memory2 = GC.GetTotalMemory(true);
            interned = Interned();
            var memory3 = GC.GetTotalMemory(true);
            Console.WriteLine(memory2 - memory1);
            Console.WriteLine(memory3 - memory2);
            Console.WriteLine((memory2 - memory1) - (memory3 - memory2));
        }

        public static string[] NotInterned()
        {
            var a = new string[Count];
            for (var i = Count; i-- > 0;)
            {
                a[i] = GetString(i);
            }
            return a;
        }

        public static string[] Interned()
        {
            var a = new string[Count];
            for (var i = Count; i-- > 0;)
            {
                a[i] = string.Intern(GetString(i));
            }
            return a;
        }

        private static string GetString(int i)
        {
            var result = (i % 10).ToString();
            return result;
        }
    }
}

在我的机器上(Linux,.NET Core 3.1),我得到了以下结果:

802408
800024
2384

第一个数字和第二个数字是两种情况下的内存占用影响。它非常大,因为字符串数组消耗大量内存来保存对所有字符串实例的引用。

第三个数字是实习字符串和非实习字符串的足迹影响之间的足迹差异。你可能会问为什么这么小。这很容易解释:Stephen Toub 在 dotnet/coreclr#18383, it's described in his blog post:

中为单个数字字符串实现了一个特殊的缓存

因此,在 .NET Core 上测量 "0".."9" 字符串的驻留没有意义。我们可以很容易地修改我们的程序来解决这个问题:

private static string GetString(int i)
{
    var result = "x" + (i % 10).ToString();
    return result;
}

更新结果如下:

4002432
800344
3202088

现在影响差异(第三个数字)非常大(3202088)。这意味着实习帮助我们在托管堆中节省了 3202088 字节。

因此,对于您未来的实验,有最重要的建议:

  • 仔细定义您实际想要衡量的指标。不要说 "I want to find all kinds of affected metrics," 源代码的任何更改都可能影响数百个不同的指标;很难在每个实验中测量所有这些。仔细想想什么样的指标对你来说是真正重要的。
  • 尽量采用接近实际工作场景的输入数据。使用一些 "dummy" 数据进行基准测试可能会导致不正确的结果,因为在运行时有太多棘手的优化可以很好地处理这种 "dummy" 情况。