• C#开发者容易忽视的6个隐藏性能杀手
  • 发布于 1天前
  • 51 热度
    0 评论
  • 顾及谁
  • 24 粉丝 49 篇博客
  •   
这是一份给 C# 开发者和架构师 的实战清单,帮你揪出这些潜伏在代码里的“地雷”,提前排掉,别等上线才炸。
⚡ 1. LINQ:优雅背后的代价
LINQ 写起来像诗一样美,但用不好,它就是性能黑洞。
❌ 典型翻车现场
var data = Enumerable.Range(1, 1_000_000);
var evenCount = data.Where(x => x % 2 == 0).Count();
var oddCount  = data.Where(x => x % 2 != 0).Count();
看着没问题?其实这里 遍历了两次!因为 data 是一个可枚举对象,每次 .Where() 都会从头开始跑一遍。
结果:100 万条数据被扫了两遍,CPU 和内存双双拉满。
✅ 正确姿势
方案一:先转成 List,再操作
var data = Enumerable.Range(1, 1_000_000).ToList();
var evenCount = data.Count(x => x % 2 == 0);
var oddCount = data.Count(x => x % 2 != 0);
只遍历一次,性能直接起飞。

方案二:让数据库干活(推荐)
如果你的数据来自数据库,别把数据拉到内存再算,让 EF Core 把查询推到 SQL 层:
var evenCount = db.Users.Count(u => u.Age % 2 == 0);
一句话:能用 SQL 算的,绝不放内存里算。

⚡ 2. Async/Await:你以为异步了,其实卡住了
async/await 是 .NET 的标配,但很多人用错了,反而制造了“伪异步”。
❌ 致命写法:同步等待异步方法
var result = GetDataAsync().Result;
这行代码看着简单,实则危险!它会:
.阻塞当前线程
.可能引发线程池死锁(特别是在 ASP.NET 同步上下文中)
.高并发下直接拖垮服务
✅ 正确做法
保持异步链条到底:
var result = await GetDataAsync();
另外,对于那种调用频繁但逻辑简单的小方法,建议返回 ValueTask 而不是 Task,减少堆分配:
public async ValueTask<int> GetNumberAsync()
{
    return 42;
}

ValueTask 是值类型,在某些场景下能避免 GC 压力,特别适合高频调用的轻量接口。

⚡ 3. 日志:一条日志拖垮整个系统
日志本该是系统的“记录员”,但写法不对,它就成了“拖油瓶”。
❌ 常见错误:同步写日志
File.AppendAllText("log.txt", "Something happened\n");
每来一个请求就往文件里写一行,听着没啥问题?
可当并发上来后,成百上千个线程排队写磁盘,I/O 直接卡死。
✅ 正确姿势:异步 + 结构化日志
用 Serilog 这类现代日志库,并开启异步写入:
Log.Logger = new LoggerConfiguration()
    .WriteTo.Async(a => a.File("log.txt"))  // 异步写入
    .CreateLogger();

Log.Information("用户 {UserId} 登录了", userId);
好处:
.日志写入不阻塞主线程
.支持结构化输出(方便 ELK、Kibana 分析)
.生产环境可关闭 Debug 级别日志,减少噪音
📌 记住:日志不是越多越好,而是越准越好。

⚡ 4. 依赖注入:注册太多,启动变慢
依赖注入(DI)是好东西,但滥用 Scoped 服务,会让应用启动越来越慢,请求处理也越来越卡。
❌ 常见问题:全用 Scoped
services.AddScoped<IUserService, UserService>();
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<IBillingService, BillingService>();
// …… 还有几十个
每个请求都创建新实例,DI 容器负担重,GC 压力大。
✅ 优化建议
.能用 Singleton 就用 Singleton
.工具类、配置服务、无状态类,统统注册为单例。

按模块批量注册
别一个个 .AddScoped,太乱。改成模块化封装:
services.AddUserModule();      // 一键注册用户相关服务
services.AddOrderModule();     // 一键注册订单模块
这样代码清爽,性能也好。

⚡ 5. JSON 序列化:大对象一序列化,内存爆炸
JSON 是前后端通信的桥梁,但处理不当,桥没走通,内存先炸了。
❌ 危险操作:一次性序列化大集合
var json = JsonSerializer.Serialize(bigList);
如果 bigList 有几十万条数据,这一行就会:
.占用大量内存
.触发 GC 频繁回收
.接口响应时间飙升
✅ 正确做法
方案一:流式序列化(推荐)
await JsonSerializer.SerializeAsync(stream, bigList, options);
数据一边生成一边写入流,内存占用低,适合大文件导出、API 分页流式返回。

方案二:分页处理
var page = bigList.Skip(0).Take(1000);
别一次性返回所有数据,按需加载才是王道。
⚡ 6. Entity Framework:懒加载引发的“N+1 查询”灾难
EF Core 很方便,但一个不小心,就会触发“N+1 查询”—— 性能杀手中的战斗机。
❌ 经典反例
var users = db.Users.ToList();
foreach (var u in users)
{
    Console.WriteLine(u.Orders.Count);  // 每次访问 Orders 都查一次库!
}
结果:查了 1 次 Users,又查了 N 次 Orders(N 是用户数),总共 N+1 次查询!
1000 个用户?就是 1001 次数据库访问!数据库直接跪了。

✅ 正确做法:预加载(Include)
var users = db.Users.Include(u => u.Orders).ToList();
一次 JOIN 查询搞定,效率提升几十倍。
🔍 小技巧:开启 EF 日志,随时监控 SQL
optionsBuilder.LogTo(Console.WriteLine);
这样你就能看到 EF 到底生成了啥 SQL,有没有偷偷发起额外查询。

📊 实测对比:用数据说话
光讲道理不够劲,咱们用 BenchmarkDotNet 实测两组数据,看看差距有多大。
实验一:LINQ vs for 循环
[MemoryDiagnoser]
publicclassLinqVsForLoop
{
    privateint[] data;

    [GlobalSetup]
    public void Setup()
    {
        data = Enumerable.Range(1, 1_000_000).ToArray();
    }

    [Benchmark]
    public int LinqWhereCount()
    {
        return data.Where(x => x % 2 == 0).Count();
    }

    [Benchmark]
    public int ForLoopCount()
    {
        var count = 0;
        for (int i = 0; i < data.Length; i++)
        {
            if (data[i] % 2 == 0) count++;
        }
        return count;
    }
}
实测结果(.NET 8/9)

结论:for 循环更快,且零内存分配。LINQ 虽然优雅,但有迭代器开销。

实验二:同步日志 vs 异步日志
[MemoryDiagnoser]
publicclassLoggingBenchmarks
{
    private ILogger syncLogger;
    private ILogger asyncLogger;

    [GlobalSetup]
    public void Setup()
    {
        syncLogger = new LoggerConfiguration()
            .WriteTo.File("sync.log")
            .CreateLogger();

        asyncLogger = new LoggerConfiguration()
            .WriteTo.Async(a => a.File("async.log"))
            .CreateLogger();
    }

    [Benchmark]
    public void SyncLogging()
    {
        syncLogger.Information("Hello {Now}", DateTime.UtcNow);
    }

    [Benchmark]
    public void AsyncLogging()
    {
        asyncLogger.Information("Hello {Now}", DateTime.UtcNow);
    }
}
实测结果

方法 吞吐量 是否阻塞
SyncLogging 是,线程被卡住
AsyncLogging 否,日志进队列异步写
结论:高并发下,异步日志性能碾压同步日志。

🛡️ 最后总结:别被“优雅”蒙蔽了双眼
这些“性能杀手”都有一个共同点:
平时看不出问题,一到高并发就原形毕露。
记住这几点:
✅ 信任抽象,但要验证代价
别以为“框架封装=高性能”,很多便利背后都有成本。

✅ 性能优化靠数据,不是靠感觉
多用 BenchmarkDotNet 和 dotnet-trace,别猜,要测。

✅ 重点关注“四大护栏”
查询(LINQ / EF)
日志
异步编程
序列化
这些地方最容易出问题,也最容易优化。

🎯 一句话收尾:
优秀的代码不仅要“能跑”,更要“跑得稳”。
把这些隐藏的性能杀手揪出来,你的系统才能真正扛得住流量洪峰。
用户评论