Skip to content

Dart Core Optimization Series

Part 5: Saving Memory with Generators

Of course. This time, we'll talk about an advanced Dart feature that helps optimize memory usage when working with large sequences of data: Generators.


The Scenario 📝

  • System: An application needs to process a potentially very long sequence of numbers, for example, generating all even numbers from 0 to 100 million.
  • The Problem: The typical approach is to create a List, add all the numbers to it, and then return the List. For a large number of items, this will consume a huge amount of RAM and could crash the application.

The Problematic Code (Memory Intensive)

dart
// This approach will create a List containing 50 million integers in RAM.
List<int> getEvenNumbers(int max) {
  final result = <int>[];
  for (int i = 0; i < max; i++) {
    if (i % 2 == 0) {
      result.add(i);
    }
  }
  return result;
}

Analyzing the Bottleneck 🧐

The bottleneck here is upfront memory allocation. The entire list must be created and stored in memory before any of its elements can be processed.

The Solution: Use a Generator for "Lazy" Data Creation

  • The Logic: A generator function doesn't return a complete collection. Instead, it yields values one by one, only when they are requested. It never stores the entire sequence in memory.

  • The Syntax:

    • sync*: Marks a function as a synchronous generator, which returns an Iterable.
    • async*: Marks a function as an asynchronous generator, which returns a Stream.
    • yield: Returns a value from the generator and pauses the function.
  • The Optimized Code:

    dart
    // Using sync* and yield, the function returns an Iterable.
    Iterable<int> getEvenNumbers_Optimized(int max) sync* {
      for (int i = 0; i < max; i++) {
        if (i % 2 == 0) {
          // "yield" returns a value and pauses the function here.
          yield i;
        }
      }
    }
    
    void main() {
      // The getEvenNumbers_Optimized function runs until the first yield and returns 0.
      for (final number in getEvenNumbers_Optimized(100000000)) {
        print(number); // The function will resume to yield the next value.
        if (number > 10) {
          break; // The loop terminates, and the generator function also stops.
        }
      }
    }

Analyzing the Results

  1. Maximum Memory Savings: The generator function never creates a List containing 50 million numbers. The memory usage is constant and extremely low, no matter how large max is.
  2. Lazy Computation: The numbers are only calculated when the for loop requests the next element. In the main() example above, the generator function will only run long enough to produce the numbers 0, 2, 4, 6, 8, 10, 12 and then stop completely. It doesn't waste CPU calculating the other 50 million numbers.

Conclusion:

  • Generators are an extremely powerful memory optimization tool.
  • Use them when you need to create or process large sequences of data without needing to have the entire sequence in memory at once. This is especially useful for processing large files, data streams, or complex algorithms.