技术频道导航
HTML/CSS
.NET技术
IIS技术
PHP技术
Js/JQuery
Photoshop
Fireworks
服务器技术
操作系统
网站运营

赞助商

分类目录

赞助商

最新文章

搜索

C#连接字符串6种方法速度比较:StringBuilder/Format/Join/Concat等

作者:admin    时间:2023-5-14 1:46:55    浏览:

C#连接字符串,我们通常简单地用加号“+”来实现,这当然是C#连接字符串的一种方法。不过,C#连接字符串的方法有很多种,本文将介绍C#连接字符串的6种方法,并通过实例比较它们性能的优劣,比较它们速度的快慢。

  • 使用加号“+”,包括“+=”
  • String.Concat
  • String.Join
  • StringBuilder
  • String.Format
  • 使用字符串插值(例如 $”My string {variable}”)。

我想现在每个人都知道使用 “+” 来连接大字符串(据说)是不行的。但这让我开始思考性能影响到底是什么?如果你有两个要连接的字符串,是否值得启动一个 StringBuilder 实例?为此,我想做一些基准测试,比较一下C#连接字符串的各种方法的性能。

我使用带有 .NET Core SDK的 AMD Ryzen CPU作为我的运行时。详细信息在这里:

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
AMD Ryzen 7 2700X, 1 CPU, 16 logical and 8 physical cores
.NET Core SDK=3.1.100
  [Host]     : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
  DefaultJob : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT

初始基准

我将使用 BenchmarkDotNet  来完成我的基准测试,BenchmarkDotNet可帮助你将方法转化为基准、跟踪其性能的测量实验。你不需要是一位经验丰富的性能工程师,你可以使用简单的 API 以声明式的方式设计非常复杂的性能实验。参阅文章:

对于我的基准测试,我将尝试执行“单行连接”。我所说的“单行连接”的意思是我说了 5 个变量,我想将它们全部连接成一个长字符串,它们之间有一个空格。我不是在循环内执行此操作,而且我手头有所有 5 个变量。

我的基准看起来像这样:

public class SingleLineJoin
{
    public string string1 = "a";
    public string string2 = "b";
    public string string3 = "c";
    public string string4 = "d";
    public string string5 = "e";

    [Benchmark]
    public string Interpolation()
    {
        return $"{string1} {string2} {string3} {string4} {string5}";
    }

    [Benchmark]
    public string PlusOperator()
    {
        return string1 + " " + string2 + " " + string3 + " " + string4 + " " + string5;
    }

    [Benchmark]
    public string StringConcatenate()
    {
        return string.Concat(string1, " ", string2, " ", string3, " ", string4, " ", string5);
    }

    [Benchmark]
    public string StringJoin()
    {
        return string.Join(" ", string1, string2, string3, string4, string5);
    }

    [Benchmark]
    public string StringFormat()
    {
        return string.Format("{0} {1} {2} {3} {4}", string1, string2, string3, string4, string5);
    }

    [Benchmark]
    public string StringBuilderAppend()
    {
        StringBuilder builder = new StringBuilder();
        builder.Append(string1);
        builder.Append(" ");
        builder.Append(string2);
        builder.Append(" ");
        builder.Append(string3);
        builder.Append(" ");
        builder.Append(string4);
        builder.Append(" ");
        builder.Append(string5);
        return builder.ToString();
    }
}

结果在这里:

方法 平均 错误 偏差
Interpolation 98.58 ns 1.310 ns 1.225 ns
PlusOperator 98.35 ns 0.729 ns 0.646 ns
StringConcatenate 94.65 ns 0.929 ns 0.869 ns
StringJoin 78.52 ns 0.846 ns 0.750 ns
StringFormat 233.67 ns 3.262 ns 2.892 ns
StringBuilderAppend 51.13 ns 0.237 ns 0.210 ns

可以看到,InterpolationPlusOperatorConcat 大致相同。String.Join 速度很快,StringBuilder 明显领先。String.Format 最慢。这里发生了什么?我们将不得不深入挖掘幕后发生的事情。

String.Format

为什么 String.Format 这么慢?原来,String.Format 也在幕后使用 StringBuilder,但它归结为一个名为“AppendFormatHelper”的方法:

https://github.com/microsoft/referencesource/blob/master/mscorlib/system/text/stringbuilder.cs#L1322

现在这有点说得通了,因为你必须记住,String.Format 可以做这样的事情:

String.Format("Price : {0:C2}", 14.00M); //打印 $14.00 (货币格式)

因此,它必须做更多的工作来尝试格式化字符串,同时考虑到正确格式化货币等问题,即使检查这些格式类型也需要一点额外的时间。

String.Join

String.Join 是一个有趣的代码,因为在我看来幕后代码并没有太大意义。如果你传入一个 IEnumerable 或一个对象的参数列表,那么它只使用一个 StringBuilder 而不会做太多其他事情:

https://github.com/microsoft/referencesource/blob/master/mscorlib/system/string.cs#L161

但是,如果你传入字符串参数,它会使用一个字符数组并执行一些非常低级的操作:

https://github.com/microsoft/referencesource/blob/master/mscorlib/system/string.cs#L204

所以我马上想到……有区别吗?那么这个基准:

public class StringJoinComparison
{
    public string string1 = "a";
    public string string2 = "b";
    public string string3 = "c";
    public string string4 = "d";
    public string string5 = "e";

    public List<string> stringList;

    [GlobalSetup]
    public void Setup()
    {
        stringList = new List<string> { string1, string2, string3, string4, string5 };
    }


    [Benchmark]
    public string StringJoin()
    {
        return string.Join(" ", string1, string2, string3, string4, string5);
    }


    [Benchmark]
    public string StringJoinList()
    {
        return string.Join(" ", stringList);
    }
}

结果

方法 平均 错误 偏差
StringJoin 80.32 ns 0.730 ns 0.683 ns
StringJoinList 141.16 ns 1.109 ns 1.038 ns

巨大差距。事实上,它要慢得多。

String.Concat

ConcatJoin 非常相似。例如,如果我们传入一个 IEnumerable,它会使用一个 StringBuilder

https://github.com/microsoft/referencesource/blob/master/mscorlib/system/string.cs#L3145

但是如果我们传入字符串的参数列表,它就会下降到 ConcatArray 方法:

https://github.com/microsoft/referencesource/blob/master/mscorlib/system/string.cs#L3292

你可能会开始注意到很多方法都调用了“FastAllocateString”,从用法而非我所拥有的特殊知识推断,这似乎为字符串的完整大小分配了内存,然后在稍后“填充”。例如,给定一个字符串列表,你已经提前知道该字符串的大小,因此你可以预先分配该内存,然后在稍后简单地填充字节。 

加号(+) 运算符

加号运算符连接字符串,它的性能如何?我写了一个小基准测试:

[MemoryDiagnoser]
public class OperatorTest
{
    public string string1 = "a";
    public string string2 = "b";
    public string string3 = "c";
    public string string4 = "d";
    public string string5 = "e";


    [Benchmark]
    public string PlusOperatorWithResult()
    {
        var result = string1 + " ";
        result += string2 + " ";
        result += string3 + " ";
        result += string4 + " ";
        result += string5 + " ";
        return result;
    }


    [Benchmark]
    public string PlusOperator()
    {
        var result = string1 + " " + string2 + " " + string3 + " " + string4 + " " + string5;
        return result;
    }
}

每个字符串连接都在它自己的行上,理论上它应该每次都创建一个新字符串。

结果:

方法 平均 错误 偏差
PlusOperatorWithResult 106.52 ns 0.560 ns 0.497 ns
PlusOperator 95.10 ns 1.818 ns 1.701 ns

显然,随着时间的推移,随着更大的字符串和更多的连接,这可能会变得更成问题,我认为这是人们在尖叫“一切都使用 StringBuilder!”时试图指出的。

另请注意,我将 MemoryDiagnoser 添加到该基准测试中以表明是的,当您使用 += 运算符进行处理时会分配更多内存,因为它必须在内存中创建一个全新的字符串来处理此问题。

StringBuilder

StringBuilder 的源代码可以在这里找到:

https://github.com/microsoft/referencesource/blob/master/mscorlib/system/text/stringbuilder.cs

它相对简单,因为它持有一个 char 数组直到最后一刻,然后在最后将所有内容连接起来。它如此之快的原因是因为在真正需要它之前你不会分配字符串。

使用 StringBuilder 最让我感到惊讶的是,即使追加 5 个(或者如果我们计算空格我猜更多),它也比仅使用 + 运算符快得多。

在前文,我展示了 String 如何比 StringBuilder 消耗更多的资源,参阅文章:

Interpolation(插补)

我找不到 C# 中插值的源代码。事实上,我甚至不确定要搜索什么。因为它类似于加号运算符,所以我假设它可能只是同一段代码,在代码深处连接字符串。

总结

那么,这会告诉我们什么?通过这些基准测试,我们知道了 StringBuilder 是构建字符串的最佳实践,我们还发现,即使在构建较小的字符串时,StringBuilder 也能完成其余的工作。这是否意味着立即重写你的代码以在所有地方使用 StringBuilder?我个人对此表示怀疑。仅基于可读性,几纳秒对你来说可能不值得。

我们还可看到,即使我们没有进行任何特殊格式化,String.Format 的性能也非常差。事实上,我们可以使用任何其他方法将字符串连接在一起并使其更快。

最后,我们还发现字符串连接仍然是一个奇怪的事情,使用 String.ConcatString.Join 之类的东西,根据你传入的内容做不同的事情。10 次中有 9 次你可能甚至不认为传入 IEnumerableParams 之间存在差异,但确实存在差异。

相关文章

x
  • 站长推荐
/* 左侧显示文章内容目录 */