高并发下string.Format易成瓶颈,因每次调用均分配新字符串、解析格式项、装箱值类型,加剧GC压力;StringBuilder需预设容量防扩容开销;插值字符串在.NET 6+中简单场景编译为string.Concat,含格式符则退化为string.Format;最优方案是绕过字符串分配,如用Utf8Formatter、Pipelines或结构化日志。

高并发下 string.Format 为什么容易成为瓶颈
它每次调用都会分配新字符串、解析格式项、做类型检查和装箱(对值类型),在高并发场景下会快速堆积大量短生命周期对象,加剧 GC 压力。尤其当格式字符串含多个参数(如 "User {0} logged in at {1:O}")时,内部还要走反射式参数提取逻辑。
- 频繁触发 Gen0 GC,间接拖慢吞吐量
- 格式字符串未缓存时,每次都要重新编译正则匹配(.NET Framework 更明显)
- 不支持 span-based 操作,无法避免堆分配
StringBuilder 在循环拼接中的真实开销
它适合「多次追加」场景,但不是万能加速器。默认构造的 StringBuilder() 初始容量仅 16,高并发下若预估不准长度,会反复扩容(涉及数组复制 + 新内存分配),反而比一次性插值更慢。
- 务必显式指定初始容量:
new StringBuilder(256)(根据典型日志长度估算) - 避免在锁外反复创建实例——可配合
ArrayPool<char>.Shared.Rent()+Span<char>手动构造(.NET 6+) - 调用
.ToString()仍会分配新字符串;若后续只用于写入流,优先用.CopyTo()或AsSpan()
插值字符串($"")在 .NET 6+ 的实际表现
编译器对简单插值(无复杂表达式、无文化敏感格式)会直接生成 string.Concat 调用,零装箱、无格式解析开销。但一旦含格式项(如 $"Time: {DateTime.Now:O}")或条件表达式($"{x > 0 ? "ok" : "fail"}"),就会退化为 string.Format 调用。
- 纯变量拼接(
$"ID={id}, Name={name}")≈string.Concat,最快 - 含
:G、:C等格式符 → 触发FormatHelper,性能回落至string.Format水平 - 跨线程共享插值字符串字面量无问题,但运行时拼接结果仍是不可变字符串,逃不过分配
var msg = $"Order {orderId} status: {status}"; // ✅ 高效
var msg = $"Created: {DateTime.UtcNow:O}"; // ❌ 实际调用 string.Format
真正适合高并发日志/响应拼接的方案
绕过字符串分配才是关键。不要执着于“哪个拼接快”,而要看“能不能不拼”。例如:
- 用
Utf8Formatter.TryFormat直接写入Span<byte>(需手动管理缓冲区) - 使用
System.IO.Pipelines的WritableBuffer配合ReadOnlySequence<char> - 日志场景优先选
Microsoft.Extensions.Logging的结构化日志(logger.LogInformation("User {UserId} logged in", userId)),底层用ValueTuple避免提前格式化 - ASP.NET Core 中返回 JSON?直接用
System.Text.Json.JsonSerializer.Serialize流式写入HttpResponse.Body,跳过中间字符串
插值和 StringBuilder 的差异在微秒级,而 GC 暂停和内存带宽争用才是毫秒级杀手——别优化错地方。










