You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何优化指数移动平均线(EMA)计算性能?IEnumerable与List等对比

EMA计算类的性能优化方案

我实现了一个计算指数移动平均线(EMA)的C#类,相关测试用例均已通过,但在处理大规模数据(比如嵌套计算EMA的EMA)时,计算速度明显变慢。我注意到代码中使用了inputs.ElementAt(index),同时也在考虑foreach与for循环、List与Enumerable的选择差异,希望能找到性能优化的方法。

原始EMA类代码

public class ExponentialMovingAverage {

  public Int32 Period { get; set; }

  public ExponentialMovingAverage(Int32 period = 20) {

    ArgumentOutOfRangeException.ThrowIfNegativeOrZero(period);

    Period = period;
    
  } 

  public IEnumerable<(DateTimeOffset Stamp, Decimal? ExponentialMovingAverage)> Compute(IEnumerable<(DateTimeOffset Stamp, Decimal? Value)> inputs) {
    
    ArgumentNullException.ThrowIfNull(inputs);

    inputs = inputs.OrderBy(x => x.Stamp);

    Decimal? previous = null;

    Decimal factor = (Decimal)(2d / (Period + 1));

    Decimal? sum = 0;

    Int32 notNulls = 0;

    for (Int32 index = 0; index < inputs.Count(); index++) {

      (DateTimeOffset stamp, Decimal? value) = inputs.ElementAt(index);

      if (value == null) {
        notNulls++;
        yield return (stamp, null);   
        continue;  
      }

      if (index < notNulls + Period - 1) {
        sum += value;
        yield return (stamp, null);   
        continue;
      }
        
      if (index == notNulls + Period - 1) {
        sum += value;
        Decimal? sma = sum / Period;
        previous = sma;
        yield return (stamp, sma);  
        continue;  
      }

      Decimal? ema = previous + (factor * (value - previous));
      previous = ema;
      yield return (stamp, ema);

    } 
    
  } 

}

测试用例

[Fact]
public void Test_AllNonNullInputs() {
    var ema = new ExponentialMovingAverage(3);

    var inputs = new List<(DateTimeOffset Stamp, decimal? Value)> {
        (new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero), 10),
        (new DateTimeOffset(2024, 1, 2, 0, 0, 0, TimeSpan.Zero), 15),
        (new DateTimeOffset(2024, 1, 3, 0, 0, 0, TimeSpan.Zero), 20),
        (new DateTimeOffset(2024, 1, 4, 0, 0, 0, TimeSpan.Zero), 25),
        (new DateTimeOffset(2024, 1, 5, 0, 0, 0, TimeSpan.Zero), 30),
        (new DateTimeOffset(2024, 1, 6, 0, 0, 0, TimeSpan.Zero), 35)
    };
    var output = ema.Compute(inputs).ToList();

    Assert.Equal(6, output.Count);
    Assert.Null(output[0].ExponentialMovingAverage);
    Assert.Null(output[1].ExponentialMovingAverage);
    Assert.Equal(15m, output[2].ExponentialMovingAverage);
    Assert.Equal(20m, output[3].ExponentialMovingAverage);
    Assert.Equal(25m, output[4].ExponentialMovingAverage);
    Assert.Equal(30m, output[5].ExponentialMovingAverage);
}

[Fact]
public void Test_FirstTwoInputsAreNull() {
    var ema = new ExponentialMovingAverage(3);

    var inputs = new List<(DateTimeOffset Stamp, decimal? Value)> {
        (new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero), null),
        (new DateTimeOffset(2024, 1, 2, 0, 0, 0, TimeSpan.Zero), null),
        (new DateTimeOffset(2024, 1, 3, 0, 0, 0, TimeSpan.Zero), 20),
        (new DateTimeOffset(2024, 1, 4, 0, 0, 0, TimeSpan.Zero), 25),
        (new DateTimeOffset(2024, 1, 5, 0, 0, 0, TimeSpan.Zero), 30),
        (new DateTimeOffset(2024, 1, 6, 0, 0, 0, TimeSpan.Zero), 35)
    };
    var output = ema.Compute(inputs).ToList();

    Assert.Equal(6, output.Count);
    Assert.Null(output[0].ExponentialMovingAverage);
    Assert.Null(output[1].ExponentialMovingAverage);
    Assert.Null(output[2].ExponentialMovingAverage);
    Assert.Null(output[3].ExponentialMovingAverage);
    Assert.Equal(25m, output[4].ExponentialMovingAverage);
    Assert.Equal(30m, output[5].ExponentialMovingAverage);
}

性能瓶颈分析

  • ElementAt(index)的低效调用:对于普通IEnumerable<T>,ElementAt需要从头遍历到指定索引,每次调用都是O(n)时间。在for循环中执行n次该操作,整体时间复杂度会变为O(n²),数据量越大性能下降越明显。
  • 重复枚举集合:inputs.Count()每次调用都会重新枚举整个集合,带来O(n)额外开销;排序后的inputs仍是延迟枚举的IEnumerable,每次访问都会重新执行排序逻辑。
  • 嵌套计算的重复执行:当用Compute的输出作为另一个Compute的输入时,延迟枚举特性会导致第一个Compute的逻辑被重复执行多次,进一步放大性能问题。

优化方案

  1. 提前转为List:排序后立即把IEnumerable转为List<T>,让Count和随机访问变为O(1)操作,同时避免重复排序和枚举。
  2. 用foreach替代for+ElementAt:直接遍历List,避免索引访问的开销,代码更简洁高效。
  3. 调整有效数据跟踪逻辑:遍历过程中直接统计非null数据的数量,计算SMA时只累积有效数据。
  4. 避免重复计算:如果需要多次使用Compute的结果,提前调用ToList()转为List,避免重复执行计算逻辑。

优化后的代码

public class ExponentialMovingAverage {

  public Int32 Period { get; set; }

  public ExponentialMovingAverage(Int32 period = 20) {
    ArgumentOutOfRangeException.ThrowIfNegativeOrZero(period);
    Period = period;
  } 

  public IEnumerable<(DateTimeOffset Stamp, Decimal? ExponentialMovingAverage)> Compute(IEnumerable<(DateTimeOffset Stamp, Decimal? Value)> inputs) {
    ArgumentNullException.ThrowIfNull(inputs);

    // 提前排序并转为List,避免重复枚举和排序
    var sortedInputs = inputs.OrderBy(x => x.Stamp).ToList();
    Decimal? previous = null;
    Decimal factor = (Decimal)(2d / (Period + 1));
    Decimal? sum = 0;
    int validCount = 0; // 统计有效(非null)数据的数量

    foreach (var item in sortedInputs) {
      var (stamp, value) = item;

      if (value == null) {
        yield return (stamp, null);
        continue;
      }

      validCount++;
      // 还没收集够Period个有效数据,继续累积
      if (validCount < Period) {
        sum += value;
        yield return (stamp, null);
        continue;
      }

      // 收集够Period个有效数据,计算初始SMA
      if (validCount == Period) {
        sum += value;
        var sma = sum / Period;
        previous = sma;
        yield return (stamp, sma);
        continue;
      }

      // 计算EMA
      var ema = previous + (factor * (value - previous));
      previous = ema;
      yield return (stamp, ema);
    } 
  } 

}

优化后的使用示例

var ema1 = new ExponentialMovingAverage(3);
var outputs1 = ema1.Compute(inputs).ToList(); // 提前转为List,避免重复计算
var ema2 = new ExponentialMovingAverage(3);
var outputs2 = ema2.Compute(outputs1);

内容的提问来源于stack exchange,提问作者Miguel Moura

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 04:08:14