Skip to content

Provide slightly faster, lower memory use, safe constructors#16

Merged
oconnor0 merged 14 commits intomasterfrom
faster-ctor
Feb 20, 2026
Merged

Provide slightly faster, lower memory use, safe constructors#16
oconnor0 merged 14 commits intomasterfrom
faster-ctor

Conversation

@oconnor0
Copy link
Copy Markdown
Collaborator

Compile for .NET 10, prefer params ReadOnlySpan when possible, cleanup some method internals, use ref struct to allow creation of the backing collection for an ImmArray/Dict/Set without leaking any values, improves runtime perf slightly and memory pressure a little more, .NET 10 adds allows ref struct to Action so the additional delegate Fill types are not needed in .NET 10, but required before that.

Anyway, we can adjust the APIs however makes sense, but the structure should be solid.

image
[Benchmark]
public ImmArray<string> Array_ToImmArray() {
	var arr = new string[N];
	for (int i = 0; i < arr.Length; ++i) {
		arr[i] = Guid.NewGuid().ToString();
	}
	return arr.ToImmArray();
}

[Benchmark]
public ImmArray<string> List_ToImmArray() {
	var xs = new List<string>();
	for (int i = 0; i < N; ++i) {
		xs.Add(Guid.NewGuid().ToString());
	}
	return xs.ToImmArray();
}

[Benchmark]
public ImmArray<string> ImmArray_NewSized() {
	return ImmArray.NewSized<string>(N, xs => {
		for (int i = 0; i < xs.Length; ++i) {
			xs[i] = Guid.NewGuid().ToString();
		}
	});
}
image
[Benchmark]
public ImmSet<string> Set0_ToImmSet() {
	var set = new HashSet<string>();
	for (int i = 0; i < N; ++i) {
		set.Add(Guid.NewGuid().ToString());
	}
	return set.ToImmSet();
}

[Benchmark]
public ImmSet<string> SetN_ToImmSet() {
	var set = new HashSet<string>(N);
	for (int i = 0; i < N; ++i) {
		set.Add(Guid.NewGuid().ToString());
	}
	return set.ToImmSet();
}


[Benchmark]
public ImmSet<string> ImmSet_New0() {
	return ImmSet.New<string>(set => {
		for (int i = 0; i < N; ++i) {
			set.Add(Guid.NewGuid().ToString());
		}
	});
}

[Benchmark]
public ImmSet<string> ImmSet_NewN() {
	return ImmSet.New<string>(N, set => {
		for (int i = 0; i < N; ++i) {
			set.Add(Guid.NewGuid().ToString());
		}
	});
}
image
[Benchmark]
public ImmDict<string, int> Dict0_ToImmDict() {
	var dict = new Dictionary<string, int>();
	for (int i = 0; i < N; ++i) {
		dict.Add(Guid.NewGuid().ToString(), r.Next());
	}
	return dict.ToImmDict();
}

[Benchmark]
public ImmDict<string, int> DictN_ToImmDict() {
	var dict = new Dictionary<string, int>(N);
	for (int i = 0; i < N; ++i) {
		dict.Add(Guid.NewGuid().ToString(), r.Next());
	}
	return dict.ToImmDict();
}

[Benchmark]
public ImmDict<string, int> ImmDict_New0() {
	return ImmDict.New<string, int>(dict => {
		for (int i = 0; i < N; ++i) {
			dict.Add(Guid.NewGuid().ToString(), r.Next());
		}
	});
}

[Benchmark]
public ImmDict<string, int> ImmDict_NewN() {
	return ImmDict.New<string, int>(N, dict => {
		for (int i = 0; i < N; ++i) {
			dict.Add(Guid.NewGuid().ToString(), r.Next());
		}
	});
}

@oconnor0 oconnor0 requested a review from isaksky February 18, 2026 23:44
@oconnor0
Copy link
Copy Markdown
Collaborator Author

The performance differences between the various runtimes is enlightening:

image

@isaksky
Copy link
Copy Markdown
Member

isaksky commented Feb 19, 2026

Looks compelling. For the naming, it feels like we are trying to disagree with C#'s choice of having new as a keyword. Maybe there is a good reason though, I think we should discuss.

@oconnor0
Copy link
Copy Markdown
Collaborator Author

oconnor0 commented Feb 20, 2026

ImmArray benchmarks:

image image

ImmSet benchmarks:

image

I haven't had a chance to write the reflection/compiled tests for ImmSet/Dict. I can do those tommorrow if desired. I can run the ImmDict benchmark tomorrow as well.

It looks like, at least in these benchmarks, .NET 10 is faster than .NET 8. Tho I did see that creating an array/ImmArray of length 0 was slower in .NET 10 than in .NET 8.

It also looks like calling new ImmArray(T[]) allocates more than calling ImmArray.Build when it's done dynamically or via the expression API. It is not obvious to me why that might be.

@oconnor0
Copy link
Copy Markdown
Collaborator Author

ImmDict benchmark:

image

@oconnor0
Copy link
Copy Markdown
Collaborator Author

It turns out that my previous reflection test was using the wrong constructor so made Expression.New look way worse than it is. I've found & fixed that. It now appears there is no significant difference between using the no-copy new or using the Build method.

ImmArray reflection benchmark:

image

@oconnor0
Copy link
Copy Markdown
Collaborator Author

ImmSet reflection benchmark:

image image

@oconnor0
Copy link
Copy Markdown
Collaborator Author

ImmDict reflection benchmark:

image image

Copy link
Copy Markdown
Member

@isaksky isaksky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@oconnor0 oconnor0 merged commit 72771a7 into master Feb 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants