1. ⏱️ Stopwatch

1.1 测量执行时间

var stopwatch = Stopwatch.StartNew();

Run();

stopwatch.Stop();
Console.WriteLine(stopwatch.Elapsed);
  • 测量前先预热待测代码。
  • 使用相同输入执行多轮,比较平均值或中位数。
  • 日志使用 ElapsedMilliseconds,短耗时使用 Elapsed.TotalMilliseconds

1.2 记录时间戳

long start = Stopwatch.GetTimestamp();

Run();

TimeSpan elapsed = Stopwatch.GetElapsedTime(start);

需要比较分配量、GC 和稳定吞吐量时,改用 BenchmarkDotNet。

2. 📚 stackalloc

2.1 创建小型临时缓冲区

Span<int> buffer = stackalloc int[128];

Fill(buffer);
Consume(buffer);

2.2 动态长度设置回退

const int StackLimit = 256;

Span<byte> buffer = length <= StackLimit
    ? stackalloc byte[length]
    : new byte[length];

大型且频繁使用的缓冲区改用 ArrayPool<T>

2.3 使用限制

  • 只分配容量明确的小缓冲区。
  • 不要在循环中反复执行较大的 stackalloc
  • 内存读取前必须完整写入,敏感数据使用后主动清理。
  • 由它创建的 Span<T> 不能逃离当前方法,也不能跨越 awaityield 使用。

3. 🧩 使用 static Lambda 避免捕获

3.1 无需外部状态时添加 static

var enabledUsers = users.Where(
    static user => user.IsEnabled);

3.2 需要外部值时保留捕获

int minimumScore = GetMinimumScore();

var passed = students.Where(
    student => student.Score >= minimumScore);
  • 不需要外部状态时使用 static Lambda。
  • 需要动态外部值时,保留捕获或使用支持状态参数的 API。
  • 热路径中的捕获可以改为显式循环或独立方法。
  • static Lambda 不能捕获局部变量、方法参数或 this,但可以访问常量和静态成员。
  • 非捕获 Lambda 可能已被编译器缓存,是否有收益仍需测量。

4. ⏲️ 使用 PeriodicTimer 执行周期任务

4.1 复用定时器执行异步任务

using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));

while (await timer.WaitForNextTickAsync(stoppingToken))
{
    await RefreshAsync(stoppingToken);
}

长期运行的周期任务复用同一个 PeriodicTimer,避免在循环中反复创建 Task.Delay

4.2 与 Task.Delay 的区别

写法间隔语义适用场景
PeriodicTimer固定频率,实际间隔约为 max(周期, 任务耗时)心跳、指标采集、缓存刷新
Task.Delay固定延迟,实际间隔约为 任务耗时 + Delay轮询、冷却、重试间隔

需要在任务完成后再等待完整间隔时,直接使用 Task.Delay

while (!stoppingToken.IsCancellationRequested)
{
    await RefreshAsync(stoppingToken);
    await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
  • 希望按固定节奏执行,并且允许跳过来不及处理的 Tick,使用 PeriodicTimer
  • 希望每次任务完成后休息固定时间,使用 Task.Delay
  • PeriodicTimer 会等待第一个 Tick;上面的 Task.Delay 写法会先执行一次任务。

4.3 使用限制

  • 同一个 PeriodicTimer 同时只能有一个等待者。
  • 取消令牌只取消当前等待,停止定时器需要调用 Dispose() 或离开 using 作用域。
  • 处理速度慢于周期时,多个 Tick 会合并,不适合要求每次触发都必须执行的任务。
  • 循环中的任务按顺序执行,不会产生定时器回调重入。
  • 不适合实时系统或需要高精度调度的任务。
  • 低频任务中的性能差异通常很小,是否替换仍需测量。