高性能 .NET 代码技术和示例

作者:微信公众号:【架构师老卢】
11-10 19:44
87

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 应用程序提供了一种基本方法。每种技术都有其用例,必须通过测量和测试选择最合适的优化策略。

相关留言评论
昵称:
邮箱:
阅读排行