VL.Audio FFT output type

There is this question that is puzzling me since a very long time, and I am super sure I am not alone with this - why is the output of the FFT node an IReadOnlyList and not a Spread?

I already heard the explanation somewhere, that a Spread would fill up the RAM, but I tried it today… I let a patch run for hours with a FFT output converted to a spread, and it did not do any harm. Also it was not slower with the maximum buffer size than the IReadOnlyList version.

It would really make a difference to understand this and to be able to explain. I think 95% of all patchers make a conversion with an empty ForEach or a FromSequence node anyways… at least this is what is taught in these courses:

I told AI to explain it, as if it were me:

This is a very good question. Understanding this concept is a key step to getting better performance out of your patches.

The type hierarchy

First, it is important to understand that a Spread<T> is an IReadOnlyList<T>. It implements that interface. You can think of IReadOnlyList<T> as a general contract or a job description that says “I am a list that you can only read from.” Since Spread<T> fulfills this contract perfectly, it can be connected to such an input without any explicit conversion. The other way around is not true.

The reason for IReadOnlyList<T>: Avoiding GC Pressure

The reason the FFT node and other modern nodes output IReadOnlyList is to avoid a performance killer known as Garbage Collector (GC) Pressure.

Every time you create a new object, like a Spread, you are allocating new memory. When the Spread from the last frame is no longer needed, it becomes garbage. The GC then has to kick in to clean up this memory. This cleanup process is not free; it consumes CPU time.

Let’s use a factory analogy:

  • An efficient node like FFT is a worker with a reusable tray. For each frame, it just refills the same tray with new data. It’s fast and creates zero waste. This is the IReadOnlyList output.
  • When you convert this to a Spread, you are forcing that worker to grab a brand-new cardboard box for every single frame, copy the data into it, and throw the old box away.

This cleanup of all the discarded boxes is the job of the Garbage Collector. It’s a cumulative effect. A single node is no problem, so your test, of course, has no issues at all, but if many nodes in your patch do this, the GC has to work constantly in the background. This extra CPU load competes with your patch’s main logic. When your application has to meet a strict 60fps deadline, this can be the cause of a missed frame, which you perceive as a stutter.

The FFT node’s IReadOnlyList output is a zero-allocation strategy. It gives you a fast, read-only “window” to its internal, reusable buffer. By converting it back to a Spread right away, you undo this benefit and create the very problem the node was designed to prevent.

The real-world problem: Teaching, Legacy Nodes, and Beginner Confusion

So, if IReadOnlyList is better, why do so many workshops show an immediate conversion to Spread?

From a teaching perspective, this is the main hurdle. Students are taught that Spread is the fundamental, friendly, go-to tool. Then, they encounter a high-performance node that outputs this 'scary-looking IReadOnlyList<T>. Their immediate impulse is to convert it back to a Spread just to get back to familiar ground.

This is not the student’s nor the teacher’s fault. It’s a natural consequence of encountering a mix of modern, performant nodes alongside older, unoptimized ones that have Spread<T> as an input type. This outdated design in legacy nodes forces the user into the allocation, creating a pattern that seems normal but is inefficient.

The key lesson to teach is that the conversion is often unnecessary. You can (and should) use ForEach, GetSlice, and many other nodes directly on the IReadOnlyList. It’s just as powerful for reading data, but more performant.

The best approach for node design is:

  • Inputs should be liberal: Use Sequence<T>, IReadOnlyList<T>, etc., to accept Spreads, Arrays, etc., without forcing allocations.
  • Outputs should be conservative: For high-frequency data, output IReadOnlyList<T> for performance and safety. For slower-changing or constant data, Spread is great.

How to fix it?

The fix for this situation is often easy: open the definition of the legacy nodes and change the type of the input pin from Spread<T> to IReadOnlyList<T>. Much is gained by this simple change.

This is a good opportunity for anyone in the community to contribute. If you find such nodes, consider updating them and sharing the fix. Many current and future students will thank you.

Cheers,
tonfilm

1 Like

Thank you!

One question though still… in the Gray Book it says:

The base type for collections in VL is the Sequence. It corresponds to what is known as IEnumerable in .NET world. We just gave it a more human-readable name.

Would it make sense, if a read-only collection also had a human-readable, less intimidating and VL-specific name?

1 Like

And also: Your explanation now reads, as if using Spreads in general was totally legacy, unoptimized and outdated, which is not the case, right? Just for the sake of completeness, would your AI self also be able to clarify when a Spread approach is in fact useful?

There was this one small sentence about it. Spreads are great for just holding data or updating it now and then, e.g., settings, config data changed by the user, a UI state.

The issue isn’t dramatic for smaller spreads. There was also a recent vvvv build with an optimized GC that is faster and should clean up such smaller spreads quite fast and efficiently.

But yeah, in general, Spread isn’t such a great collection type if you need performance when all data changes every frame. Check out the endeavors of @TremensS on how to optimize his LED and laser controller patches.

Yes, absolutely, very good point.

This should be part of the documentation

1 Like