在.net中开发高性能应用程序代码的技术和示例

科技   2024-11-12 05:24   上海  


受 Steve Gordon 的 NDC Oslo 2024 演讲的启发,本文通过实际示例探讨了编写高性能 .NET 代码的基本技术。在这里,我们将介绍关键的优化,从节省内存的数据处理到改进的 JSON 序列化。

1. 用于节省内存的字符串处理Span<T>

使用字符串时,避免不必要的内存分配至关重要。 提供了一种无需创建新字符串即可执行 subString 操作的方法。Span<T>

public class StringProcessor
{
public static string TraditionalSubstring(string input, int start, int length)
{
return input.Substring(start, length); // Allocates new string
}

public static ReadOnlySpan<char> OptimizedSubstring(ReadOnlySpan<char> input, int start, int length)
{
return input.Slice(start, length); // No allocation
}

public static bool ContainsOptimized(string haystack, string needle)
{
ReadOnlySpan<char> haystackSpan = haystack.AsSpan();
ReadOnlySpan<char> needleSpan = needle.AsSpan();
return haystackSpan.Contains(needleSpan, StringComparison.Ordinal);
}
}

使用允许字符串切片和搜索操作,而无需额外分配内存,从而提高性能,尤其是在高频字符串操作中。ReadOnlySpan<char>

2. 用于临时数组优化ArrayPool

使用临时数组时,可以通过重用数组来减轻内存压力,尤其适用于 IO 操作中的大型缓冲区。ArrayPool

public class BufferProcessor
{
private static readonly ArrayPool<byte> _arrayPool = ArrayPool<byte>.Shared;

public async Task ProcessLargeData(Stream stream)
{
byte[] buffer = _arrayPool.Rent(81920); // Rent 80KB buffer
try
{
while (await stream.ReadAsync(buffer) is int bytesRead && bytesRead > 0)
{
ProcessBuffer(buffer.AsSpan(0, bytesRead));
}
}
finally
{
_arrayPool.Return(buffer);
}
}

private void ProcessBuffer(ReadOnlySpan<byte> buffer)
{
// Process buffer data
}
}

使用 ,您可以租用一个数组用于临时使用并返回它,从而减少高吞吐量应用程序中的垃圾收集开销。ArrayPool

3. 利用高性能数据流System.IO.Pipelines

System.IO.Pipelines提供强大的 API,用于以最少的内存分配处理高性能数据处理,在网络流或文件处理等场景中特别有用。

public class PipelineReader
{
private readonly PipeReader _reader;

public PipelineReader(Stream stream)
{
_reader = PipeReader.Create(stream);
}

public async Task ProcessDataAsync()
{
while (true)
{
ReadResult result = await _reader.ReadAsync();
ReadOnlySequence<byte> buffer = result.Buffer;

try
{
while (TryReadLine(ref buffer, out ReadOnlySequence<byte> line))
{
await ProcessLineAsync(line);
}

if (result.IsCompleted)
break;
}
finally
{
_reader.AdvanceTo(buffer.Start, buffer.End);
}
}
}

private bool TryReadLine(ref ReadOnlySequence<byte> buffer, out ReadOnlySequence<byte> line)
{
SequencePosition? position = buffer.PositionOf((byte)'\n');

if (position == null)
{
line = default;
return false;
}

line = buffer.Slice(0, position.Value);
buffer = buffer.Slice(buffer.GetPosition(1, position.Value));
return true;
}
}

System.IO.Pipelines通过使用 无需重新分配即可处理数据,帮助有效地管理大量数据流。ReadOnlySequence

4. 优化 JSON 处理System.Text.Json

System.Text.Json提供高效的 JSON 序列化和反序列化选项,包括源生成的序列化,以获得额外的性能优势。

public class JsonOptimizer
{
private static readonly JsonSerializerOptions _options = new()
{
PropertyNameCaseInsensitive = true,
AllowTrailingCommas = true,
WriteIndented = false
};

public static string SerializeOptimized<T>(T value)
{
return JsonSerializer.Serialize(value, _options);
}

public static T? DeserializeOptimized<T>(ReadOnlySpan<byte> utf8Json)
{
return JsonSerializer.Deserialize<T>(utf8Json, _options);
}

[JsonSourceGenerationOptions(
WriteIndented = false,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)
]
[JsonSerializable(typeof(WeatherForecast))]
internal partial class JsonContext : JsonSerializerContext
{
}
}

使用 source generation () 可以减少开销,并允许以更好的性能处理复杂的序列化场景。JsonSerializerContextSystem.Text.Json

5. 使用 BenchmarkDotNet 测量性能

BenchmarkDotNet 是用于测量 .NET 性能的宝贵工具。下面是用于比较和分配的示例基准。Span<T>string

[MemoryDiagnoser]
public class StringOperationsBenchmark
{
private const string Sample = "Hello, World! This is a test string for benchmarking.";

[Benchmark(Baseline = true)]
public string Traditional()
{
return Sample.Substring(7, 5);
}

[Benchmark]
public ReadOnlySpan<char> Optimized()
{
return Sample.AsSpan(7, 5);
}
}

BenchmarkDotNet 提供有关内存使用情况和运行时性能的详细报告,帮助您做出明智的优化决策。

6. 使用 StringBuilder 池化最小化内存分配

池化可重用对象(如 )可在频繁构建和丢弃大型字符串的情况下减少内存流失。StringBuilder

public class StringBuilderPool
{
private static readonly ObjectPool<StringBuilder> _pool =
new DefaultObjectPool<StringBuilder>(new StringBuilderPooledObjectPolicy());

public static string BuildString(Action<StringBuilder> action)
{
var sb = _pool.Get();
try
{
action(sb);
return sb.ToString();
}
finally
{
sb.Clear();
_pool.Return(sb);
}
}
}

public class StringBuilderPooledObjectPolicy : PooledObjectPolicy<StringBuilder>
{
public override StringBuilder Create()
{
return new StringBuilder(1024);
}

public override bool Return(StringBuilder obj)
{
if (obj.Capacity > 2048)
return false;

obj.Clear();
return true;
}
}

使用可防止过度分配,使其适用于需要高性能字符串操作的应用程序。ObjectPool<StringBuilder>

7. 优化 HTTP 客户端使用

与配置一起重复使用有助于减少 TCP 连接的开销,并提高高流量应用程序中的请求效率。HttpClientSocketsHttpHandler

public class OptimizedHttpClient
{
private static readonly HttpClient _client = new(new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(2),
MaxConnectionsPerServer = 20,
EnableMultipleHttp2Connections = true
});

public static async Task<string> GetDataAsync(string url, CancellationToken token = default)
{
using var response = await _client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
response.EnsureSuccessStatusCode();

return await response.Content.ReadAsStringAsync(token);
}
}

设置像 和 这样的选项允许通过重用连接来有效地处理多个请求。PooledConnectionLifetimeMaxConnectionsPerServerHttpClient

  • 优化前测量:使用 BenchmarkDotNet 等工具确保性能提升。

  • 安全使用:在使用 和 时避免不安全的代码做法。

  • Span<T> Span<T>Memory<T>

  • 利用池化:明智地使用 和 对象池来控制内存使用。ArrayPool

  • Leverage :用于 JSON 操作,以受益于其速度和内存效率。System.Text.JsonSystem.Text.Json

  • 最小化内存分配:优先考虑可读代码,但旨在减少不必要的分配。

  • 谨慎使用高性能 API:仅在需要时应用高性能优化。

这些示例和最佳实践为优化现代 .NET 应用程序提供了一种基本方法。每种技术都有其用例,必须通过测量和测试选择最合适的优化策略。

如果你喜欢我的文章,请给我一个赞!谢谢

架构师老卢
资深软件架构师, 分享编程、软件设计经验, 教授前沿技术, 分享技术资源(每天发布电子书),每天进步一点点...
 最新文章