【发布时间】: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] | afd | 370.4 ns | 9.37 ns | 27.33 ns | 360.7 ns | 1 | 0.0319 | - | - | 336 B | |
| StringReplaceWithCreate | http(...)ogle [196] | 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