> 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/il2cpp-specializations.md).

# IL2CPP Specializations

Tell IL2CPP ahead of time which generic types the bindings will need.

Off by default. Turn it on in **Project Settings ▸ Binding System ▸ Optimization ▸ IL2CPP Specializations**. It applies to IL2CPP builds only; on Mono it changes nothing, and the build step says so and generates nothing.

## The problem it solves

IL2CPP compiles ahead of time. For a generic type it can see in your sources, `List<Vector3>` say, it emits a body specialised for that exact type argument, as fast as hand-written code. For a generic type it **cannot** see, because the instantiation only ever happens at runtime through `MakeGenericType`, it has nothing to emit, and it falls back to a **shared** body where every operation on the type argument is a runtime lookup. For reference types that is the normal, cheap kind of sharing. For value types it means every call through such a body goes through a runtime adapter.

The binding pipeline is built almost entirely that second way. When a binding first runs, it creates the accessor for its path, the chain that joins several members, the combined modifier for its stages and the phased reader for its source, all as generic types instantiated over the types it finds in the scene. None of those instantiations appear in any source file, so on IL2CPP all of them run shared. The [benchmarks](/binding-system-3/project-tools/performance/benchmarks.md#several-hops) show what that costs: a chain of two struct properties reads in **59 ns on IL2CPP against 7 ns on Mono**, on the same machine, running the same code, and a chain of four modifiers on a miss used to cost **three times** what the plain bind paid per stage until the modifier's own instantiation was named in the runtime. Turning this setting on takes the first of those from 59 ns to 28, and a path through Unity's own types from 75 ns to 22, which is Mono's own figure. The [measurements](#what-it-is-worth) are below.

Mono never has this problem, because its JIT compiles each instantiation for the exact types the first time it is called. It is also why editor measurements say nothing about it.

## What it does

At build time, the package writes one generated source file into your project, next to the [optimized accessors](/binding-system-3/project-tools/performance/optimized-accessors.md) file. It contains a single method that is never called. Its body is a list of calls into the runtime's `BindAotAnchors`, each with concrete type arguments:

```csharp
BindAotAnchors.ClassToStruct<object, Vector3>();
BindAotAnchors.ChainStruct<object, float, Vector3>();
BindAotAnchors.Value<Color>();
BindAotAnchors.Conversion<float, int>();
```

Each anchor names, for those arguments, every generic type the pipeline would build for them: the field and property accessors, the compound accessor and its chain class, the proxy pair and phased bind, the combined modifier, the readers and writers, the delegates between them. Because the calls exist in compiled code, IL2CPP sees the instantiations and compiles them specialised. When the game later builds the same types by reflection, it finds the specialised bodies already there.

A class is always written as `object`. IL2CPP shares one body across every reference type, so naming `object` covers `Transform`, `Light`, your own components, and everything else at once; only the value types have to be named exactly.

The file is removed after the build unless **Keep Generated File** is on, the same as the optimized accessors.

## What it is worth

Measured, and only where it applies. The package's [performance suite](/binding-system-3/project-tools/performance/benchmarks.md#running-them-yourself) run twice against the same IL2CPP build of the same project on the same machine, once with the setting at **None** and once at **All**. Medians, nanoseconds per call:

<table><thead><tr><th width="250">Path</th><th width="120">None</th><th width="120">All</th><th width="110">Gain</th><th>Mono, for scale</th></tr></thead><tbody><tr><td><code>Transform.localScale.x</code>, read<br><em>class, struct property, field</em></td><td>75 ns</td><td><strong>22 ns</strong></td><td><strong>3.3x</strong></td><td>22 ns</td></tr><tr><td><code>SubStruct.vec2.x</code>, read<br><em>struct property, struct property, field</em></td><td>59 ns</td><td><strong>28 ns</strong></td><td>2.1x</td><td>7 ns</td></tr><tr><td><code>SubStruct.vec2.x</code>, written</td><td>58 ns</td><td><strong>36 ns</strong></td><td>1.6x</td><td>17 ns</td></tr><tr><td>A single <strong>field</strong>, read</td><td>3.7 ns</td><td>3.5 ns</td><td>none</td><td>4.0 ns</td></tr><tr><td>A single <strong>property</strong>, read</td><td>7.0 ns</td><td>7.0 ns</td><td>none</td><td>8.3 ns</td></tr></tbody></table>

Three things to take from it.

**The first row is the whole argument.** `Transform.localScale.x` is a path through Unity's own types, so the standard set names every hop of it exactly. At 22 ns against Mono's 22, the backend gap on that path is gone: the IL2CPP build now reads it as fast as the Mono one, which is what the shared-code diagnosis predicted and what no other setting achieves.

**The middle rows close part of the way**, because `SubStruct` is a type of this project's own and only the hops whose types the build names get specialised. That is the difference between **Standard** and **All** in practice, and the reason All exists.

**The single-member rows do not move**, in either direction. `Bind<float>` and the accessor for one field are written out in ordinary code already, so IL2CPP was never sharing them. If your bindings are all one member deep, this setting has nothing to give you, and the build size it costs buys nothing.

Accessor throughput across the whole mixed path set moved about **15% for reads and 25% for writes**, which is those two effects averaged over a realistic mixture.

## The three levels

<table><thead><tr><th width="180">Level</th><th>What is named</th><th width="230">What it costs</th></tr></thead><tbody><tr><td><strong>None</strong></td><td>Nothing. Every generic type the pipeline builds for your scenes runs through IL2CPP's shared code. This is the default.</td><td>No extra code.</td></tr><tr><td><strong>Standard types</strong></td><td>A fixed set: the numeric primitives, <code>Vector2/3/4</code>, <code>Vector2Int/3Int</code>, <code>Quaternion</code>, <code>Color</code>, <code>Color32</code>, <code>Rect</code>, <code>RectInt</code>, <code>Bounds</code>; every hop into the public members of those structs (<code>position.x</code>, <code>rotation.eulerAngles.y</code>, <code>rect.center.x</code>), with a class before them or not; and the conversions between them, numeric to numeric, the vector widenings, <code>Color</code> to <code>Color32</code>, and each of them to and from a class.</td><td>About <strong>220</strong> anchors, the same in every project. Each one expands to a set of specialised bodies in the player, which is where the size is spent; <strong>Preview</strong> shows the file itself, and your build report shows what it came to.</td></tr><tr><td><strong>All types</strong></td><td>The standard set, plus every type the bindings in the build's scenes and resources actually reach. The build step scans them, resolves each path to its members, and names each hop, each chain and each end type exactly.</td><td>The standard set plus whatever your project adds, and a scan of the build's text assets when the build starts.</td></tr></tbody></table>

{% hint style="warning" %}
**More code in the build, more memory at runtime.** Every instantiation named is compiled once more and stays loaded for the life of the player. That is the whole trade: in exchange, a path through value types runs 1.6x to 3.3x faster than the shared code IL2CPP would otherwise use, on every read and every write, and a path of one member runs exactly as it did. It is opt in for that reason, and the setting says so beside the choice. **Preview** writes the exact file a build would generate into a temp folder, so you can see what you are adding before you add it.
{% endhint %}

## Choosing

* Your bindings are mostly onto floats, vectors, colors and the usual Unity structs, and you build with IL2CPP: **Standard types**. It costs the same in every project and covers the bulk of what a scene binds, including the `Transform.localScale.x` row above.
* You bind into structs of your own, or deep into Unity structs the standard set does not walk: **All types**. It names exactly what your build uses and nothing more; anything the standard set already covers is not repeated. The middle rows above are what this adds over Standard.
* Every binding you care about is **one member deep**, or you build with Mono, or the binding cost on IL2CPP is not on your profiler: **None**. The single-member rows above are the reason: there is nothing there to win.

Two things the All level cannot name, and counts instead of guessing: a struct that is not public, since generated code in your project cannot spell its name, and a path through an indexer or a method, which goes through accessors of its own. The build log gives both counts.

## Against the other two build-time options

|         | IL2CPP Specializations                                    | [Optimized Accessors](/binding-system-3/project-tools/performance/optimized-accessors.md) | [Phased Bindings](/binding-system-3/project-tools/performance/phased-bindings.md) |
| ------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| Changes | How the existing pipeline is compiled                     | What runs instead of the pipeline for a path                                              | How often the pipeline runs                                                       |
| Reaches | Every binding over a named type, whatever its source mode | Deterministic paths on a direct source                                                    | Every binding with stages                                                         |
| Costs   | Code size and memory                                      | Code size, one method per path                                                            | A compare per read                                                                |
| Backend | IL2CPP only                                               | Both                                                                                      | Both                                                                              |

They compose. A path the optimized accessors replace never reaches the pipeline and does not care how it was compiled; every other path does, and this is what compiles it well.

## Related pages

* [Benchmarks](/binding-system-3/project-tools/performance/benchmarks.md#several-hops): the numbers that made this necessary.
* [Build-Time Optimized Accessors](/binding-system-3/project-tools/performance/optimized-accessors.md): the other generated file, and the folder both share.
* [Settings](/binding-system-3/reference/settings.md#optimization): the setting itself.
