> For the complete documentation index, see [llms.txt](https://postica.gitbook.io/binding-system-3/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://postica.gitbook.io/binding-system-3/project-tools/performance/phased-bindings.md).

# Phased Bindings

Only re-run the parts of the pipeline whose input changed.

On by default once you answer the [welcome screen](/binding-system-3/getting-started/welcome-screen.md) question.

## The idea

A pipeline with a converter and four modifiers is six calls per update. But most of those stages are **pure**: given the same input, they produce the same output. If the input has not changed, running them again is wasted work.

A phased binding splits the pipeline into blocks and only re-runs a block when its input actually changed.

```
Source → [ clamp → remap → convert ]  →  [ tween ]  →  Field
             pure, cached                dynamic, every frame
```

The split point is decided by which components declare themselves **dynamic**.

<table><thead><tr><th width="230">Component</th><th>What it means</th></tr></thead><tbody><tr><td><strong>Static</strong></td><td>Output depends only on its input. <strong>Clamp Value</strong>, <strong>Remap Range</strong>, <strong>Swizzle Color</strong>, a plain format.</td></tr><tr><td><strong>Dynamic</strong></td><td>Output can change even when the input has not: the modifier depends on time, or on a bindable parameter that something else is driving. <strong>Tween Animation</strong>, <strong>Value Smoother</strong>, <strong>Value Delayer</strong>, and any modifier whose parameters are themselves bound.</td></tr></tbody></table>

Everything up to the first dynamic component is one cacheable block. Everything from there on runs every time. On the write path the same split applies in mirror order.

When **nothing** in the chain is dynamic, which is the common case, there is no second block at all: the whole pipeline is cached, and an unchanged source returns the previous result outright.

## What it is worth

The saving is not a single number, and it follows from what the update actually does.

A read costs two things: the **source read** through the accessor, and the **pipeline** that follows it. A phased read is:

```
value = read the source          ← always paid
if value == the value last seen:
    return the cached result     ← the whole cached block is skipped
otherwise:
    run the block, cache it, return it
```

So the source read is **never** skipped: the binding has to read the source to find out whether it changed. That is what sets the ceiling. Everything after it can go, and on a fully static chain all of it does.

### Measured

`PhasedPipelinePerformanceTests` in the package's [performance suite](/binding-system-3/project-tools/performance/benchmarks.md#running-them-yourself) reads the same field through the same modifier chain twice, once as a `Bind<float>` and once as a `PhasedBind<float>`. Medians in nanoseconds per read, from an IL2CPP build and a Mono build of the same project on the same machine, on 3.0.2:

<table><thead><tr><th width="210">Static stages after the source</th><th width="110">Fixed</th><th width="110">Phased</th><th width="130">Saved</th><th width="110">Fixed, Mono</th><th width="110">Phased, Mono</th><th>Saved, Mono</th></tr></thead><tbody><tr><td>None</td><td>8 ns</td><td>7 ns</td><td>nothing to pay</td><td>10 ns</td><td>9 ns</td><td>nothing to pay</td></tr><tr><td>One</td><td>10 ns</td><td>10 ns</td><td>a wash</td><td>22 ns</td><td>18 ns</td><td>18%</td></tr><tr><td>Two</td><td>20 ns</td><td>12 ns</td><td><strong>40%</strong></td><td>62 ns</td><td>20 ns</td><td><strong>67%</strong></td></tr><tr><td>Four</td><td>31 ns</td><td>12 ns</td><td><strong>62%</strong></td><td>112 ns</td><td>20 ns</td><td><strong>82%</strong></td></tr></tbody></table>

The shape is the one the sketch above predicts. The phased column is **flat**: about 12 ns on IL2CPP and 20 ns on Mono whether there is one stage behind the cache or four, because on a hit none of them run. The fixed column grows with every stage, by 5 to 6 ns on IL2CPP and about 25 ns on Mono. So the saving is the length of the chain times the cost of a stage, less the price of the compare, and the first row says what that price is: with nothing to cache a phased bind reads in the same time as a fixed one.

That is also why the two backends break even in different places. On Mono a stage is dear and the cache pays for itself at one; on IL2CPP a stage is cheap and the cache needs two. A converter counts as a stage, so a binding with a conversion and one modifier is the two-stage row on either backend.

### When the source changes

The table above is the case the mechanism exists for. The suite also measures its opposite, a source that changes on **every** read, so the cache never hits and every stage runs every time:

<table><thead><tr><th width="210">Static stages, source changing</th><th width="110">Fixed</th><th width="110">Phased</th><th width="130">Cost</th><th width="110">Fixed, Mono</th><th width="110">Phased, Mono</th><th>Cost, Mono</th></tr></thead><tbody><tr><td>None</td><td>6.6 ns</td><td>7.1 ns</td><td>the compare</td><td>16 ns</td><td>14 ns</td><td>none</td></tr><tr><td>Four</td><td>28 ns</td><td>37 ns</td><td>1.35x</td><td>112 ns</td><td>140 ns</td><td>1.25x</td></tr><tr><td>Dynamic modifier first, two static after it</td><td>16.5 ns</td><td>16.4 ns</td><td>none</td><td>74 ns</td><td>85 ns</td><td>1.15x</td></tr></tbody></table>

A miss costs the fixed read plus the compare, the cache write and one extra hop into the combined modifier: about 10 ns on a four-stage chain, on either backend, and half a nanosecond with nothing behind the cache. A chain led by a dynamic modifier has nothing to cache, so the package builds a plain continuous reader for it, and that reads in the same time as the fixed bind.

Two earlier runs of this table read very differently, and the difference is worth knowing about if you measure the package yourself on IL2CPP. The phased bind runs its cached block as one combined modifier, `MultiModifier<T>`. It called each stage through its interface, and IL2CPP resolves an interface call by searching the type's interface table, which the numeric modifiers make long; and it was reached only through `MakeGenericType`, and a generic instantiation that appears nowhere in the compiled code has no specialised body on IL2CPP and runs through **full generic sharing**, where every `T`-typed operation is a runtime lookup. Together those made a four-stage miss cost 126 ns against the fixed bind's 36. 3.0.2 replaced the interface calls with bound delegates (27 ns a stage to 17) and names the type in the phased bind's own code, so the build carries a specialised body (17 to 7.5). Mono specialises everything as it goes and showed only a sliver of either.

So the setting is a bet on how often your sources hold still. A slider the user is not touching, a health value, a colour set once a level, any value that is stable for most frames: phased wins, and the longer the chain the more it wins. A transform being followed, a timer, an input axis, anything that is different every frame: phased pays about a third more for nothing, on a chain long enough to have been worth caching. Most bindings in most projects are the first kind, which is why it is on by default; a project made largely of the second kind should measure with it off. The frame is the arbiter, and the [Bindings Monitor](/binding-system-3/project-tools/diagnostics/bindings-monitor.md) reports it either way.

{% hint style="warning" %}
**An end-to-end measurement will always read lower than these tables.** The figures are the pipeline alone. Around it sits per-update cost that phased bindings do not remove: the updater walking its list, the is-alive check, the interval check and the delegate dispatch into the binding. That overhead is charged whether or not the pipeline runs, so the fraction of a *frame* it saves is smaller than the fraction of a *pipeline* it saves. If you are comparing against a profiler capture rather than against the pipeline, expect a smaller number.
{% endhint %}

Three things bound the gain:

* **The source has to be unchanged.** On an update where it changed, the phased read costs the fixed read plus the compare and the cache write: about 10 ns on a four-stage chain, per the table above.
* **Only the stages before the first dynamic one are cached.** A [tween](/binding-system-3/pipeline/tweening.md) at the head of the chain leaves nothing cacheable, and the continuous reader the package builds for that case runs the whole chain on every read, as a fixed bind would.
* **A binding with no pipeline has nothing to split** and sees no difference: the first row of the table is the measurement.

## Turning it on and off

<table><thead><tr><th width="330">Where</th><th>What it is</th></tr></thead><tbody><tr><td><strong>Project Settings ▸ Binding System ▸ Optimization ▸ Phased Bindings</strong></td><td>The setting.</td></tr><tr><td><a href="/binding-system-3/getting-started/welcome-screen.md">Welcome screen</a></td><td>Asked once, on upgrade.</td></tr><tr><td><a href="/binding-system-3/project-tools/diagnostics/bindings-monitor.md">Bindings Monitor</a></td><td>Reports whether phased bindings are on, in the footer. It does not switch them.</td></tr></tbody></table>

The setting applies to bindings the engine drives, which in practice means [proxy bindings](/binding-system-3/overview/proxy-bindings.md). For a `Bind<T>` field of your own, the equivalent is to declare it as `PhasedBind<T>`:

```csharp
public PhasedBind<float> speed;   // instead of Bind<float>
```

`PhasedBind<T>` is a drop-in replacement: same Inspector, same API, same implicit conversion.

{% hint style="warning" %}
**The setting is read when a binding builds its pipeline, not on every update.** Changing it in play mode therefore affects bindings that initialise *after* the change, not the ones already running. To compare the two settings honestly, change it and re-enter play mode.
{% endhint %}

## The trade-offs

<table><thead><tr><th width="230">Cost</th><th>What it means</th></tr></thead><tbody><tr><td>Build size</td><td>Slightly larger. The phase machinery is more generated code.</td></tr><tr><td>Memory</td><td>A little per binding, for the cached intermediate values.</td></tr><tr><td>Complexity</td><td>Nothing visible. The Inspector and the API are identical.</td></tr></tbody></table>

{% hint style="info" %}
If something misbehaves with phased bindings on, switching back to fixed bindings is one toggle, and it is worth reporting what broke. The welcome screen says as much, and it means it.
{% endhint %}

## Phased versus Optimized Update

Neighbouring ideas, different targets, and they compose.

<table><thead><tr><th width="230"></th><th>Phased bindings</th><th>Optimized Update</th></tr></thead><tbody><tr><td>Skips</td><td>Pipeline stages whose input did not change</td><td>The whole update when the source did not change</td></tr><tr><td>Granularity</td><td>Per stage</td><td>Per binding</td></tr><tr><td>Set</td><td>Project wide, or per bind type</td><td>Per binding</td></tr><tr><td>Best for</td><td>Long modifier chains</td><td>Values that are stable most frames</td></tr></tbody></table>

Use both. They are not alternatives.

{% hint style="warning" %}
Do **not** put Optimized Update on a binding that uses a [tweening](/binding-system-3/pipeline/tweening.md) modifier. "The value did not change" is exactly when a tween still has work to do. Phased bindings handle that correctly on their own, because the tween declares itself dynamic.
{% endhint %}

## Related pages

* [Optimized Accessors](/binding-system-3/project-tools/performance/optimized-accessors.md): removing the reflection instead of re-running less of it.
* [Bindings Monitor](/binding-system-3/project-tools/diagnostics/bindings-monitor.md): measuring the difference.
* [Benchmarks](/binding-system-3/project-tools/performance/benchmarks.md): running the numbers yourself.
