首页 > 解决方案 > 如何测量字符串实习?

问题描述

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

我想出了这个:

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.net-4.8

解决方案


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

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

但是,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

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

第三个数字是 interned 和 not interned string 的足迹影响之间的足迹差异。你可能会问为什么它这么小。这很容易解释:Stephen Toub 在dotnet/coreclr#18383中为单个数字字符串实现了特殊缓存,在他的博客文章中有描述:

在此处输入图像描述

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

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

以下是更新的结果:

4002432
800344
3202088

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

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

  • 仔细定义您实际想要衡量的指标。不要说“我要查找各种受影响的指标”,源代码的任何更改都可能影响数百个不同的指标;在每个实验中都很难测量所有这些。仔细考虑什么样的指标对你来说真的很重要。
  • 尝试获取接近您实际工作场景的输入数据。使用一些“虚拟”数据进行基准测试可能会导致不正确的结果,因为运行时有太多棘手的优化可以很好地处理这种“虚拟”情况。

推荐阅读