C#
simple moving average
algorithm optimization
programming
software development

How to calculate simple moving average faster in C?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

A simple moving average is easy to write, but the obvious implementation does unnecessary work. If you sum the last n values again for every position, the cost grows quickly on large inputs or live streams.

The standard optimization is a sliding window. Instead of recomputing the full sum, keep the current window total, subtract the value that leaves the window, and add the new value that enters it.

Why the Naive Version Is Slow

Suppose you have one million samples and a window size of 100. A naive loop performs roughly 100 additions for each output point. That gives you time complexity of O(count * period).

For a moving average, consecutive windows overlap almost completely. The window for index 10 and the window for index 11 differ by only two values:

  • one value drops out on the left
  • one value is added on the right

That means the new sum can be derived from the old sum in constant time. With that change, the whole algorithm becomes O(count).

Sliding Window Implementation

The example below uses C# because the existing article tags target that runtime. It returns one average for each full window and throws on invalid input instead of silently producing bad data.

csharp
1using System;
2
3public static class MovingAverage
4{
5    public static double[] Calculate(double[] values, int period)
6    {
7        if (values == null) throw new ArgumentNullException(nameof(values));
8        if (period <= 0) throw new ArgumentOutOfRangeException(nameof(period));
9        if (period > values.Length) return Array.Empty<double>();
10
11        double[] result = new double[values.Length - period + 1];
12        double sum = 0;
13
14        for (int i = 0; i < period; i++)
15        {
16            sum += values[i];
17        }
18
19        result[0] = sum / period;
20
21        for (int i = period; i < values.Length; i++)
22        {
23            sum += values[i];
24            sum -= values[i - period];
25            result[i - period + 1] = sum / period;
26        }
27
28        return result;
29    }
30
31    public static void Main()
32    {
33        double[] samples = { 10, 12, 14, 20, 22, 18, 16 };
34        double[] averages = Calculate(samples, 3);
35
36        Console.WriteLine(string.Join(", ", averages));
37    }
38}

For the sample input, the output is 12, 15.333333333333334, 18.666666666666668, 20, 18.666666666666668. Each number is the average of one consecutive block of three values.

Handling Streaming Data

If values arrive one at a time, you usually do not want to rebuild an array on every update. A queue plus a running sum works well for that case.

csharp
1using System;
2using System.Collections.Generic;
3
4public sealed class StreamingMovingAverage
5{
6    private readonly int _period;
7    private readonly Queue<double> _window = new();
8    private double _sum;
9
10    public StreamingMovingAverage(int period)
11    {
12        if (period <= 0) throw new ArgumentOutOfRangeException(nameof(period));
13        _period = period;
14    }
15
16    public double? Add(double value)
17    {
18        _window.Enqueue(value);
19        _sum += value;
20
21        if (_window.Count > _period)
22        {
23            _sum -= _window.Dequeue();
24        }
25
26        if (_window.Count < _period)
27        {
28            return null;
29        }
30
31        return _sum / _period;
32    }
33}

This pattern is useful for telemetry, market data, sensor readings, or rolling dashboards. Memory stays bounded by the window size, and each update is still constant time.

Choosing the Right Numeric Type

For most workloads, double is the practical choice. It is fast and usually accurate enough for measurements, statistics, and monitoring. If you are handling money, decimal may be safer because it avoids many binary floating-point rounding surprises, but it is slower.

You should also decide what to return before the first full window exists. Common choices are:

  • return an empty result until enough samples arrive
  • return nullable values for early positions
  • use a shorter warm-up average

The best option depends on whether your consumer expects fixed-length output or only complete windows.

Common Pitfalls

  • Recomputing the full sum inside a nested loop. That throws away the main optimization and becomes slow on large datasets.
  • Forgetting to subtract the outgoing value. The result will drift upward because the sum keeps growing.
  • Using integer division when you want fractional output. In C#, int / int truncates.
  • Ignoring invalid periods such as 0 or a value larger than the input length.
  • Choosing float for convenience when accuracy matters. Repeated additions can accumulate noticeable error.

Summary

  • A naive moving average costs O(count * period).
  • A sliding window reduces the work to O(count).
  • Keep a running sum, add the incoming value, and subtract the outgoing one.
  • Use a queue-based version for real-time streams.
  • Validate the period and choose double or decimal based on your precision needs.

Course illustration
Course illustration

All Rights Reserved.