C# 12 中的 Span<T> 和 Memory<T>:高级开发人员的性能助推器

作者:微信公众号:【架构师老卢】
11-24 20:52
25

作为ASP.NET开发人员,我们一直在寻找能让我们的Web应用程序运行得更快、更高效的方法。Span<T>Memory<T>这两个强大的工具就能帮我们达成这一目标。它们于几年前被引入,如今已成为编写高性能C#代码必不可少的部分。

让我们通过实际示例以及针对2024年C# 12的一些技巧,来探讨如何在ASP.NET应用程序中有效地使用Span<T>Memory<T>

Span<T>Memory<T>是什么?

让我们先从基础知识讲起:

Span<T>是一种表示连续内存块的类型。它可用于处理数组、字符串或非托管内存,而且无需创建副本。

Memory<T>Span<T>类似,但它可用于异步方法中,并且能存储在字段里。

可以把Span<T>想象成能直接操作的内存视图,而Memory<T>则是对该内存的引用,能更自由地传递。

下面让我们来看一些在常见的ASP.NET场景中,Span<T>Memory<T>能发挥重大作用的情况:

Span<T>

1. 解析请求数据

设想你正在构建一个会接收大量JSON数据的API。你可以使用Span<T>更高效地解析数据,而不必为每条数据都分配新的字符串:

[HttpPost]
public IActionResult ProcessOrder([FromBody] string orderJson)
{
    ReadOnlySpan<char> jsonSpan = orderJson.AsSpan();
    
    // 在不分配新字符串的情况下查找 "totalAmount" 字段
    int startIndex = jsonSpan.IndexOf("\"totalAmount\":") + "\"totalAmount\":".Length;
    int endIndex = jsonSpan.Slice(startIndex).IndexOf(',');
    
    if (decimal.TryParse(jsonSpan.Slice(startIndex, endIndex), out decimal totalAmount))
    {
        // 处理订单...
        return Ok($"Order processed with total amount: {totalAmount}");
    }
    
    return BadRequest("Invalid order data");
}

这种方法减少了内存分配,提升了性能,尤其在处理大型有效载荷时效果显著。

2. 高效的响应编写

在发送响应(特别是大型响应)时,Span<T>有助于优化内存使用:

[HttpGet]
public IActionResult GetLargeData()
{
    const int bufferSize = 1024 * 1024; // 1 MB缓冲区
    byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
    
    try
    {
        int dataSize = GenerateLargeData(buffer.AsSpan());
        return File(buffer.AsMemory(0, dataSize).ToArray(), "application/octet-stream", "large-data.bin");
    }
    finally
    {
        ArrayPool<byte>.Shared.Return(buffer);
    }
}

private int GenerateLargeData(Span<byte> buffer)
{
    // 用数据填充缓冲区...
    return /* 实际数据大小 */;
}

这个示例结合使用ArrayPool<T>Span<T>Memory<T>,能在不过度分配内存的情况下高效地处理大型数据。

3. 中间件中的URL解析

自定义中间件通常需要检查URL。以下展示了如何高效地进行这项操作:

public class CustomUrlMiddleware
{
    private readonly RequestDelegate _next;

    public CustomUrlMiddleware(RequestDelegate next) => _next = next;

    public Task InvokeAsync(HttpContext context)
{
        ReadOnlySpan<char> path = context.Request.Path.Value.AsSpan();
        
        if (path.StartsWith("/api/".AsSpan()))
        {
            // 处理API请求
            context.Items["IsApiRequest"] = true;
        }
        
        return _next(context);
    }
}

这个中间件在检查URL时无需分配新的字符串,这对于高流量应用程序来说非常有用。

Memory<T>

在需要以下操作时可使用Memory<T>

  • 跨异步边界处理内存。
  • 将对内存段的引用作为类中的字段进行存储。
  • 将内存引用传递给期望接收Memory<T>的方法。

1. 异步文件上传处理

[HttpPost("upload")]
public async Task<IActionResult> UploadFile(IFormFile file)
{
    if (file.Length > 10_000_000) // 10 MB限制
        return BadRequest("File too large");

    var memory = new Memory<byte>(new byte[file.Length]);
    
    using (var stream = file.OpenReadStream())
    {
        await stream.ReadAsync(memory);
    }

    await ProcessUploadedFileAsync(memory);
    
    return Ok("File processed successfully");
}

private async Task ProcessUploadedFileAsync(Memory<byte> fileData)
{
    // 模拟一些异步处理
    await Task.Delay(100); // 实际处理的占位符
    
    // 示例:统计非零字节数
    int nonZeroBytes = 0;
    foreach (byte b in fileData.Span)
    {
        if (b!= 0) nonZeroBytes++;
    }
    
    Console.WriteLine($"Processed file with {nonZeroBytes} non-zero bytes");
}

这个示例展示了如何使用Memory<T>在异步方法间处理文件数据,而无需不必要的复制操作。

2. 缓存大型对象

在ASP.NET应用程序中缓存大型对象时,Memory<T>会很有用:

public class LargeObjectCache
{
    private Memory<byte> cachedData;

    public async Task<Memory<byte>> GetOrCreateAsync(Func<Task<byte[]>> createFunc)
    {
        if (cachedData.IsEmpty)
        {
            byte[] newData = await createFunc();
            cachedData = new Memory<byte>(newData);
        }
        return cachedData;
    }
}

// 在控制器中的用法
[HttpGet("large-data")]
public async Task<IActionResult> GetLargeData([FromServices] LargeObjectCache cache)
{
    var data = await cache.GetOrCreateAsync(async () =>
    {
        // 模拟获取大型数据
        await Task.Delay(1000);
        return new byte[1_000_000]; // 1 MB的数据
    });

    return File(data.ToArray(), "application/octet-stream");
}

3. 在后台任务中高效构建字符串

对于构建大型字符串的后台任务,Memory<T>可能比StringBuilder更高效:

public class ReportGenerator : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await GenerateReportAsync();
            await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
        }
    }

    private async Task GenerateReportAsync()
    {
        var writer = new ArrayBufferWriter<char>(initialCapacity: 1024 * 1024); // 初始容量为1 MB

        await WriteReportHeaderAsync(writer);
        await WriteReportBodyAsync(writer);
        await WriteReportFooterAsync(writer);

        string report = new string(writer.WrittenMemory.Span);
        await SaveReportAsync(report);
    }

    private async Task WriteReportHeaderAsync(IBufferWriter<char> writer)
{
        var memory = writer.GetMemory(1024);
        int written = System.Text.Encoding.UTF8.GetBytes("Report Header\n", memory.Span);
        writer.Advance(written);
        await Task.Delay(100); // 模拟一些异步工作
    }

    // 用于WriteReportBodyAsync和WriteReportFooterAsync的类似方法

    private async Task SaveReportAsync(string report)
{
        // 将报告保存到数据库或文件
        await File.WriteAllTextAsync("report.txt", report);
    }
}

这个示例展示了如何在后台任务中结合使用Memory<T>IBufferWriter<T>来高效构建字符串,这在ASP.NET应用程序中对于诸如生成报告或数据处理之类的任务很常见。

对于希望优化应用程序的ASP.NET开发人员来说,Span<T>Memory<T>是强大的工具。通过使用这些类型,你可以编写更高效的代码,这些代码占用更少的内存且运行速度更快。

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