The simplest approach for OSA is Scalar Replacement of Aggregates (or struct promotion), which works by deconstructing the constituent fields of an object into local variables:
class ImmutableArray<T>.Builder {
T[] data;
int index = 0;
Builder(int capacity) => data = new T[capacity];
void Add(T value) => data[index++] = value;
ImmutableArray<T> MoveToImmutable()
=> index == data.Length ? new(data) : throw Something();
}
ImmutableArray<int> Method1() {
var builder = ImmutableArray.CreateBuilder<int>(1);
builder.Add(123);
return builder.MoveToImmutable();
}
Assuming we could inline all calls where the object is passed to, the code could be rewritten into something like this:
ImmutableArray<int> Method1_SROA() {
var builder_data = new int[1];
int builder_index = 0;
builder_data[list_index++] = 123;
return builder_index == builder_data.Length ? new(builder_data) : throw Something();
}
This transform would preferably be done before SsaTransform, so that we can just replace fields with local load/stores without having to build SSA again.
Proper OSA is significantly more complicated but possible to some extent at CIL level, by replacing objects with struct refs when passing to methods where it doesn't escape.
This approach will obviously destroy the API surface (and thus reflection access), but it allows for much more impactful opts such as rewriting arrays/generics to the payload definition (in addition to a IsNull field if the compiler can't prove that there won't be null objects):
class Data { int x, y; }
void Method1(Data data) => data.x += data.y;
void Method2(Data[] arr) {
if (arr[0] != null) Method1(arr[0]);
}
//Output
class Data {
Payload__ p__;
struct Payload__ { int x, y, z; bool IsNull__; }
}
void Method1(ref Data.Payload__ data) => data.x += data.y;
void Method2(Data.Payload__[] arr) {
if (!arr[0].p__.IsNull__) Method1(ref arr[0].p__);
//null check must be added before getting ref to p__ to preserve behavior
}
The simplest approach for OSA is Scalar Replacement of Aggregates (or struct promotion), which works by deconstructing the constituent fields of an object into local variables:
Assuming we could inline all calls where the object is passed to, the code could be rewritten into something like this:
This transform would preferably be done before SsaTransform, so that we can just replace fields with local load/stores without having to build SSA again.
Proper OSA is significantly more complicated but possible to some extent at CIL level, by replacing objects with struct refs when passing to methods where it doesn't escape.
This approach will obviously destroy the API surface (and thus reflection access), but it allows for much more impactful opts such as rewriting arrays/generics to the payload definition (in addition to a IsNull field if the compiler can't prove that there won't be null objects):