【问题标题】:Fastest way to replace occurences in small string using span in c#在 c# 中使用 span 替换小字符串中出现的最快方法
【发布时间】:2021-05-04 15:32:37
【问题描述】:

我正在尝试最大化 C# 中 string.Replace 方法的 cpu 性能和内存优化。 目标是减少内存分配和 cpu 时间,因为项目位于 10000 rps 的 asp.net 核心中。

我发现了两个提高性能的技巧: 1)使用跨度结构 2)使用String.Create

   internal struct ContextData
    {
        public string Origin { get; set; }
        public string Replace { get; set; }
        public string With { get; set; }
    }




    internal string SpanReplaceWithCreate(ContextData context)
    {
        int count = 0;
     
        ReadOnlySpan<char> origin_span = context.Origin.AsSpan();
        ReadOnlySpan<char> replace_span = context.Replace.AsSpan();
        ReadOnlySpan<char> replace_with = context.With.AsSpan();

        int index;
        ReadOnlySpan<char> tmp = origin_span;

        while ((index = tmp.IndexOf(replace_span)) > 0)
        {
            count++;
            tmp = tmp.Slice(index + replace_span.Length, tmp.Length - replace_span.Length - index);
        }

        string a = string.Create(context.Origin.Length + (context.Replace.Length - context.With.Length) * count, context, (chars, state) =>
           {
               // NOTE: We don't access the context variable in this delegate since 
               // it would cause a closure and allocation.
               // Instead we access the state parameter.

               // will track our position within the string data we are populating
               var position = 0;
               ReadOnlySpan<char> origin = state.Origin.AsSpan();
               ReadOnlySpan<char> replace = state.Replace.AsSpan();
               ReadOnlySpan<char> with = state.With.AsSpan();

               ReadOnlySpan<char> tmp_context = origin;

               while ((index = tmp_context.IndexOf(replace)) > 0)
               {
                   tmp_context.Slice(0, index).CopyTo(chars.Slice(position));
                   with.CopyTo(chars.Slice(position + index));
                   position += (index + with.Length);
                   tmp_context = tmp_context.Slice(index + replace.Length, tmp_context.Length - replace.Length - index);
               }

               if (position < chars.Length) {
                   tmp_context.CopyTo(chars.Slice(position));
               }

           });


        return a;
    }

但与 string.Replace 相比,我的性能仍然最差

Method URL find replace Mean Error StdDev Median Rank Gen 0 Gen 1 Gen 2 Allocated
StringReplace http(...)ogle [196] google afd 370.4 ns 9.37 ns 27.33 ns 360.7 ns 1 0.0319 - - 336 B
StringReplaceWithCreate http(...)ogle [196] google afd 492.8 ns 9.60 ns 12.15 ns 490.4 ns 2 0.0563 - - 592 B

有什么建议吗?

这里是测试参数

https://www.example.com/xxxxx?campaign={camp}&adgroup={publisher_id}&install_callback=https%3A%2F%2Fpostback.example.com%3Ftransaction%3D{transaction}&session_callback=https%3A%2F%2Fpostback.example.com%3Ftransaction%3D{aff_sub1}&affsite={aff_site}&clickid={transaction}&adset_id={creative_id}&user_agent={ua}&ip={ip}&language={lang}



{camp} : "campiagn_it_banner_size_360"
{publisher_id} : "78983"
{transaction} : "c1032072-f815-413b-a57c-4a027f681e60"
{aff_sub1} : "78bea32a-6ead-4ea0-b9f2-9489ebc43d6a"
{aff_site} : "vbvsdgdavhdgdvjs_46_789-p90"
{creative_id} : "360x360"
{ua} : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36"
{ip} : "192.168.1.1"
{lang} : "en"

更新 1

[Benchmark]
    public string FastTokenReplace()
    {

        string request = "http://wwww.example.com?a=campiagn_it_banner_size_360&b=78983&h=c1032072-f815-413b-a57c-4a027f681e6&y=78bea32a-6ead-4ea0-b9f2-9489ebc43d6a&ty=vbvsdgdavhdgdvjs_46_789-p90&yhhh=360x360&sua=Mozilla%2F5.0%20(Windows%20NT%2010.0%3B%20Win64%3B%20x64)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F90.0.4430.93%20Safari%2F537.36&ppp=192.168.1.1";
        string redirecturl = "https://www.example.com/xxxxx?campaign={camp}&adgroup={publisher_id}&install_callback=https%3A%2F%2Fpostback.example.com%3Ftransaction%3D{transaction}&session_callback=https%3A%2F%2Fpostback.example.com%3Ftransaction%3D{aff_sub1}&affsite={aff_site}&clickid={transaction}&adset_id={creative_id}&user_agent={ua}&ip={ip}&language={lang}&ieruiero{343454";

        int max_allocation = Math.Max(request.Length, redirecturl.Length) * 3;

        return string.Create(max_allocation, redirecturl, (chars, state) =>
        {
            ReadOnlySpan<char> tmp = state.AsSpan();
            int position = 0;
            int placeholder_start;
            while ((placeholder_start = tmp.IndexOf('{')) > 0)
            {
                int placeholder_end = tmp.Slice(placeholder_start).IndexOf('}');
                if (placeholder_end < 0)
                {
                    //copy the last part
                    tmp.CopyTo(chars.Slice(position));
                    break;
                }
                else
                {
                    tmp.Slice(0, placeholder_start).CopyTo(chars.Slice(position));
                    ReadOnlySpan<char> replace = tmp.Slice(placeholder_start, placeholder_end + 1);

                    //OPTIMIZE HERE?
                    ReadOnlySpan<char> with = Placeholders.getVal(replace.ToString()).AsSpan();

                    with.CopyTo(chars.Slice(position + placeholder_start));
                    position += (placeholder_start + with.Length);
                    tmp = tmp.Slice(placeholder_start + replace.Length, tmp.Length - replace.Length - placeholder_start);
                }

            }

        });
    }

 class Placeholders
{



    public const string camp = "{camp}";
    public const string publisher_id = "{publisher_id}";
    public const string creative_id = "{creative_id}";
    public const string ua = "{ua}";
    public const string lang = "{lang}";
    public const string ip = "{ip}";
    public const string Transaction = "{transaction}";
    public const string AffSite = "{aff_site}";
    public const string AdsetId = "{adset}";
    public const string AffSub1 = "{affsub1}";


    public static string getVal(string key)
    {

        switch (key)
        {
            case camp:
                return "campiagn_it_banner_size_360";
            case publisher_id:
                return "78983";
            case Transaction:
                return "c1032072-f815-413b-a57c-4a027f681e60";
            case AffSub1:
                return "78bea32a-6ead-4ea0-b9f2-9489ebc43d6a";
            case AffSite:
                return "vbvsdgdavhdgdvjs_46_789-p90";
            case creative_id:
                return "360x360";
            case ua:
                return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36";
            case ip:
                return "192.168.1.1";
            default:
                return "";
        }
    }

    public static ReadOnlySpan<char> getVal(ReadOnlySpan<char> key)
    {

        if (MemoryExtensions.Equals(key, camp, StringComparison.Ordinal))
            return "campiagn_it_banner_size_360".AsSpan();
        else if (MemoryExtensions.Equals(key, publisher_id, StringComparison.Ordinal))
            return "78983".AsSpan();
        else if (MemoryExtensions.Equals(key, Transaction, StringComparison.Ordinal))
            return "c1032072-f815-413b-a57c-4a027f681e6".AsSpan();
        else if (MemoryExtensions.Equals(key, AffSub1, StringComparison.Ordinal))
            return "78bea32a-6ead-4ea0-b9f2-9489ebc43d6a".AsSpan();
        else if (MemoryExtensions.Equals(key, AffSite, StringComparison.Ordinal))
            return "vbvsdgdavhdgdvjs_46_789-p90".AsSpan();
        else if (MemoryExtensions.Equals(key, creative_id, StringComparison.Ordinal))
            return "360x360".AsSpan();
        else if (MemoryExtensions.Equals(key, ua, StringComparison.Ordinal))
            return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36".AsSpan();
        else if (MemoryExtensions.Equals(key, ip, StringComparison.Ordinal))
            return "192.168.1.1".AsSpan();
        else
            return "".AsSpan();


    }




}
   [Benchmark]
    public string StandardTokenReplace()
    {

        string request = "http://wwww.example.com?a=campiagn_it_banner_size_360&b=78983&h=c1032072-f815-413b-a57c-4a027f681e6&y=78bea32a-6ead-4ea0-b9f2-9489ebc43d6a&ty=vbvsdgdavhdgdvjs_46_789-p90&yhhh=360x360&sua=Mozilla%2F5.0%20(Windows%20NT%2010.0%3B%20Win64%3B%20x64)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F90.0.4430.93%20Safari%2F537.36&ppp=192.168.1.1";
        string redirecturl = "https://www.example.com/xxxxx?campaign={camp}&adgroup={publisher_id}&install_callback=https%3A%2F%2Fpostback.example.com%3Ftransaction%3D{transaction}&session_callback=https%3A%2F%2Fpostback.example.com%3Ftransaction%3D{aff_sub1}&affsite={aff_site}&clickid={transaction}&adset_id={creative_id}&user_agent={ua}&ip={ip}&language={lang}&ieruiero{343454";
        int max_allocation = Math.Max(request.Length, redirecturl.Length) + Math.Abs(request.Length - redirecturl.Length);

        //get original url and take the longest one + domain


        return redirecturl.Replace(Placeholders.camp, Placeholders.getVal(Placeholders.camp))
            .Replace(Placeholders.publisher_id, Placeholders.getVal(Placeholders.publisher_id))
            .Replace(Placeholders.creative_id, Placeholders.getVal(Placeholders.creative_id))
            .Replace(Placeholders.ua, Placeholders.getVal(Placeholders.ua))
            .Replace(Placeholders.lang, Placeholders.getVal(Placeholders.lang))
            .Replace(Placeholders.ip, Placeholders.getVal(Placeholders.ip))
            .Replace(Placeholders.Transaction, Placeholders.getVal(Placeholders.Transaction))
            .Replace(Placeholders.AffSite, Placeholders.getVal(Placeholders.AffSite))
            .Replace(Placeholders.AdsetId, Placeholders.getVal(Placeholders.AdsetId))
            .Replace(Placeholders.AffSub1, Placeholders.getVal(Placeholders.AffSub1));

    }

1 MAX 分配

    int max_allocation = Math.Max(request.Length, redirecturl.Length) * 3;

我们可以计算出正确的字符串大小,但它的性能会更差。 对于这种情况,我们可以假设一个最大长度。

www.example.com?camp=1234567890123456789023456789012345678902345678 www.replace.com?{camp}{camp}{camp}{camp}{camp}{camp}{camp}

不会工作。

2 获取价值

   ReadOnlySpan<char> with = Placeholders.getVal(replace.ToString()).AsSpan();

如果占位符重复,我们可以缓存该值或在移动到下一个占位符之前搜索所有出现。

public static string getVal(string key) vs public static string getVal(ReadOnlySpan key)

使用字符串版本我们仍然有更好的性能。有什么改进的建议吗?

// * 总结 *

BenchmarkDotNet=v0.12.1,操作系统=Windows 10.0.19041.928 (2004/?/20H1) Intel Core i9-10900 CPU 2.80GHz,1 个 CPU,20 个逻辑内核和 10 个物理内核 .NET Core SDK=5.0.202 [主机]:.NET Core 3.1.14(CoreCLR 4.700.21.16201,CoreFX 4.700.21.16208),X64 RyuJIT .NET Core 3.1:.NET Core 3.1.14(CoreCLR 4.700.21.16201,CoreFX 4.700.21.16208),X64 RyuJIT

Job=.NET Core 3.1 工具链=.NET Core 3.1

Method URL find replace Mean Error StdDev Rank Gen 0 Gen 1 Gen 2 Allocated
FastTokenReplace [196] 518.8 ns 4.63 ns 3.61 ns 1 0.2470 0.0038 - 2.52 KB
FastTokenReplaceImproveMem [196] 584.4 ns 6.84 ns 5.71 ns 2 0.2050 0.0010 - 2.09 KB
StandardTokenReplace [196] 4,242.7 ns 84.82 ns 94.27 ns 3 0.6866 - - 7.06 KB

【问题讨论】:

  • 字符串的最大长度是多少?您也可以尝试使用字符数组。
  • 字符串都是营销url,一般不超过300,最大1000。根据占位符的个数,我们最多需要做15次替换
  • 优化String.Replace 可能不是要走的路。更高效的是优化上下文,因此首先不会调用 String.Replace。例如,最好不要通过获取原始字符串并替换来扩展字符串中的占位符,而是通过将其解析为占位符分隔符并在您进行时构建一个新字符串(StringBuilder 是最简单的,但这可能是如果需要,使用Spans 进一步优化——尽管只有在需要时,因为StringBuilder 已经很好地优化了)。
  • @Jeroen 这基本上就是我在函数中所做的。在 string.create 中使用 span 获取索引并重建字符串
  • 除了为不同的占位符连续执行 15 次比一次为所有占位符使用单个缓冲区效率低。值得注意的是,您希望尽可能避免(重新)分配。您目前正在针对单个占位符多次出现的场景进行优化,但这是正确的吗?通常,常见的情况是一次替换多个占位符。另一个不重要的考虑是:你真的需要一个新的字符串作为输出吗?例如,您可以直接流式传输结果吗?

标签: c# performance asp.net-core .net-core


【解决方案1】:

使用示例数据运行一些测试,String.Replace 似乎是高度最优的,StringBuilder.Replace 和我的IndexOfAny 的变体返回首先找到的匹配项(基于 CodeReview 的改进)都较慢。在我的测试中,使用元组数组进行替换是最快的:

var s = "https://www.example.com/xxxxx?campaign={camp}&adgroup={publisher_id}&install_callback=https%3A%2F%2Fpostback.example.com%3Ftransaction%3D{transaction}&session_callback=https%3A%2F%2Fpostback.example.com%3Ftransaction%3D{aff_sub1}&affsite={aff_site}&clickid={transaction}&adset_id={creative_id}&user_agent={ua}&ip={ip}&language={lang}";

var replacementsa = new[] {
        ("{camp}", "campiagn_it_banner_size_360"),
        ("{publisher_id}", "78983"),
        ("{transaction}", "c1032072-f815-413b-a57c-4a027f681e60"),
        ("{aff_sub1}", "78bea32a-6ead-4ea0-b9f2-9489ebc43d6a"),
        ("{aff_site}", "vbvsdgdavhdgdvjs_46_789-p90"),
        ("{creative_id}", "360x360"),
        ("{ua}", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36"),
        ("{ip}", "192.168.1.1"),
        ("{lang}", "en")
    };

public static string MultiReplace(this string s, (string match,string replace)[] replacements) {
    for (int replacementNum = 0; replacementNum < replacements.Length; ++replacementNum)
        s = s.Replace(replacements[replacementNum].match, replacements[replacementNum].replace);

    return s;
}

【讨论】:

  • 很少担心您的代码:1)var 类型会增加运行时的开销 2)为什么我们应该使用元组而不是字典? 3)s = s.Replace() 会导致大量分配
  • @Riccardo 1) 您对 var 类型的看法不正确 - 请参阅 C# documentation on var 2) 由于我们不是按键查找而是处理所有值,因此 Dictionary 只会增加开销和分配比较到ValueTuple。如果需要动态更改,请使用 List&lt;T&gt;。 3) 每次替换只有一次,但它比 StringBuilder 或 Span 的替代品更快,因为它是优化的库代码,所以内存分配和速度哪个更重要?
  • 1) 我的错,你是对的。 2)在真实环境中,您将有一个 request.query 应该是一个字典。我将比较开关、字典和您的解决方案。3)我试图在 CPU 和内存方面获得最佳解决方案。我将与我的解决方案进行测试比较。但是直到知道解析 span.indexof + slice + string.create 比多次替换好 10 倍
猜你喜欢
  • 2016-08-07
  • 2015-03-28
  • 2011-12-05
  • 2013-09-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-09
  • 1970-01-01
相关资源
最近更新 更多