嘿,我目前正在尝试使用非常底层的C#进行实验,主要是为了学习并将我的技能提升到下一个水平,以应对性能要求较高的项目。
我试图创建一个类似于NativeList的东西,即一个没有任何自动垃圾回收的列表。
我想使用结构体和手动指针分配来创建一个内存和缓存行对齐的列表实现。这对Unity开发者可能很熟悉。
我目前面临的问题是,在使用BenchmarkDotnet进行基准测试后,我得到了一些意外的结果。 读取操作的速度如预期地更快一些,主要是因为我直接使用指针(我猜测),而且在C#中数组访问速度本来就很快。 写入操作则快了很多,实际上快了约70%,这令人困惑,但可能是由于内存对齐引起的。 然而,添加操作却慢了约5%,我无法弄清楚原因。
请注意,基准测试中没有涉及任何分配,因为两个列表都保持其内部缓冲区的活动状态。剩下的部分只有与List内部非常相似的if条件。
您有什么建议可以使这个过程更快,或者可以告诉我为什么我的列表添加操作较慢吗?
基准测试结果如下:
我目前面临的问题是,在使用BenchmarkDotnet进行基准测试后,我得到了一些意外的结果。 读取操作的速度如预期地更快一些,主要是因为我直接使用指针(我猜测),而且在C#中数组访问速度本来就很快。 写入操作则快了很多,实际上快了约70%,这令人困惑,但可能是由于内存对齐引起的。 然而,添加操作却慢了约5%,我无法弄清楚原因。
请注意,基准测试中没有涉及任何分配,因为两个列表都保持其内部缓冲区的活动状态。剩下的部分只有与List内部非常相似的if条件。
您有什么建议可以使这个过程更快,或者可以告诉我为什么我的列表添加操作较慢吗?
基准测试结果如下:
Method | Size | Mean | Error | StdDev | Ratio | RatioSD | CacheMisses/Op |
---|---|---|---|---|---|---|---|
Native_Add | 100 | 934.59 ns | 5.828 ns | 4.866 ns | 1.04 | 0.01 | 2 |
List_Add | 100 | 894.44 ns | 7.063 ns | 6.607 ns | 1.00 | 0.00 | 1 |
Native_Add | 1000 | 9,463.38 ns | 183.577 ns | 162.736 ns | 1.03 | 0.03 | 16 |
List_Add | 1000 | 9,196.11 ns | 87.191 ns | 81.558 ns | 1.00 | 0.00 | 17 |
Native_Add | 10000 | 93,361.52 ns | 876.444 ns | 776.945 ns | 1.05 | 0.01 | 112 |
List_Add | 10000 | 88,501.43 ns | 362.486 ns | 302.692 ns | 1.00 | 0.00 | 53 |
Native_Get | 100 | 41.38 ns | 0.371 ns | 0.347 ns | 0.94 | 0.02 | 0 |
List_Get | 100 | 43.93 ns | 0.849 ns | 0.794 ns | 1.00 | 0.00 | 0 |
Native_Get | 1000 | 366.97 ns | 4.445 ns | 3.940 ns | 0.98 | 0.02 | 0 |
List_Get | 1000 | 374.97 ns | 3.053 ns | 2.550 ns | 1.00 | 0.00 | 0 |
Native_Get | 10000 | 3,674.04 ns | 60.222 ns | 56.331 ns | 0.98 | 0.02 | 5 |
List_Get | 10000 | 3,733.41 ns | 35.220 ns | 32.945 ns | 1.00 | 0.00 | 4 |
Native_Set | 100 | 56.57 ns | 0.357 ns | 0.279 ns | 0.34 | 0.00 | 0 |
List_Set | 100 | 164.29 ns | 0.936 ns | 0.875 ns | 1.00 | 0.00 | 0 |
Native_Set | 1000 | 493.52 ns | 8.668 ns | 8.108 ns | 0.30 | 0.00 | 1 |
List_Set | 1000 | 1,665.81 ns | 10.073 ns | 7.865 ns | 1.00 | 0.00 | 2 |
Native_Set | 10000 | 4,984.44 ns | 94.866 ns | 88.737 ns | 0.30 | 0.01 | 10 |
List_Set | 10000 | 16,699.32 ns | 59.226 ns | 49.456 ns | 1.00 | 0.00 | 18 |
结构:
public unsafe struct NativeList<T> : IDisposable where T : unmanaged {
private static readonly int MinCapacity = UnsafeUtils.CacheLineSize / sizeof(T);
private T* _buffer;
private int _capacity;
private int _count;
public int Capacity => _capacity;
public int Count => _count;
public T this[int index] {
get {
if((uint)index >= (uint)_count) throw new ArgumentOutOfRangeException(nameof(index));
return _buffer[index];
}
set {
if((uint)index >= (uint)_count) throw new ArgumentOutOfRangeException(nameof(index));
_buffer[index] = value;
}
}
public NativeList() {
}
public NativeList(int capacity) {
if(capacity != 0) Resize(capacity);
}
public void Add(T value) {
EnsureCapacity(1);
_buffer[_count] = value;
_count++;
}
public void AddRange(T* values, int count) {
if(count == 0) return;
EnsureCapacity(count);
UnsafeUtils.Copy<T>(values, 0, _buffer, _count, count);
_count += count;
}
public bool Remove(T value) {
var index = IndexOf(value);
if(index < 0) return false;
RemoveAt(index);
return true;
}
public void RemoveAt(int index) {
if(index >= _count) throw new ArgumentOutOfRangeException(nameof(index));
_count--;
if(_count != index) {
UnsafeUtils.Copy<T>(_buffer, index + 1, _buffer, index, _count - index);
}
}
public void RemoveRangeAt(int index, int count) {
if(index < 0 || index >= _count) throw new ArgumentOutOfRangeException(nameof(index));
if(count < 0 || index + count > _count) throw new ArgumentOutOfRangeException(nameof(count));
if(count == 0) return;
_count -= count;
if(_count != index) {
UnsafeUtils.Copy<T>(_buffer, index + count, _buffer, index, _count - index);
}
_count -= count;
}
public bool Contains(T value) {
return IndexOf(value, EqualityComparer<T>.Default) >= 0;
}
public bool Contains(T value, IEqualityComparer<T> equalityComparer) {
return IndexOf(value, equalityComparer) >= 0;
}
public int IndexOf(T value) {
return IndexOf(value, EqualityComparer<T>.Default);
}
public int IndexOf(T value, IEqualityComparer<T> equalityComparer) {
for(var i = 0; i < _count; i++) {
if(equalityComparer.Equals(((T*)_buffer)[i], value)) return i;
}
return -1;
}
public void Clear() {
_count = 0;
}
public Span<T> AsSpan() {
return UnsafeUtils.CreateSpan<T>(_buffer, _count);
}
public Enumerator GetEnumerator() {
return new Enumerator(_buffer, _count);
}
private void EnsureCapacity(int count) {
var minCapacity = _count + count;
if(minCapacity > _capacity) Resize(minCapacity);
}
private void Resize(int capacity) {
capacity = Math.Max(MinCapacity, (int)BitOperations.RoundUpToPowerOf2((uint)capacity));
_buffer = (T*)UnsafeUtils.AlignedRealloc<T>(_buffer, capacity);
_capacity = capacity;
}
public void Dispose() {
if(_buffer is null) return;
UnsafeUtils.AlignedFree(_buffer);
_capacity = 0;
_buffer = null;
}
public struct Enumerator : IEnumerator<T> {
private readonly void* _buffer;
private readonly int _count;
private int _index;
public T Current { get; private set; }
object IEnumerator.Current => Current;
public Enumerator(void* buffer, int count) {
_buffer = buffer;
_count = count;
_index = -1;
}
public bool MoveNext() {
if(++_index >= _count) return false;
Current = ((T*)_buffer)[_index];
return true;
}
public void Reset() {
_index = -1;
}
public void Dispose() {}
}
}
基准:
[SimpleJob]
[MemoryDiagnoser]
[HardwareCounters(HardwareCounter.CacheMisses)]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory, BenchmarkLogicalGroupRule.ByParams)]
public class NativeListBenchmark {
private NativeList<int> _fullNativeList = new();
private List<int> _fullList = new();
private Random _random;
private NativeList<int> _nativeList = new();
private List<int> _list = new();
[Params(100, 1000, 10_000)]
public int Size { get; set; }
[GlobalSetup]
public void Setup() {
_random = new Random(1337);
for(var i = 0; i < Size; i++) {
_fullNativeList.Add(i);
}
for(var i = 0; i < Size; i++) {
_fullList.Add(i);
}
}
[GlobalCleanup]
public void Cleanup() {
_nativeList.Dispose();
_fullNativeList.Dispose();
}
[Benchmark]
[BenchmarkCategory("Add")]
public void Native_Add() {
_nativeList.Clear();
for(var i = 0; i < Size; i++) {
_nativeList.Add(_random.Next());
}
}
[Benchmark]
[BenchmarkCategory("Get")]
public void Native_Get() {
for(var i = 0; i < Size; i++) {
_ = _fullNativeList[i];
}
}
[Benchmark]
[BenchmarkCategory("Set")]
public void Native_Set() {
for(var i = 0; i < Size; i++) {
_fullNativeList[i] = i;
}
}
[Benchmark(Baseline = true)]
[BenchmarkCategory("Add")]
public void List_Add() {
_list.Clear();
for(var i = 0; i < Size; i++) {
_list.Add(_random.Next());
}
}
[Benchmark(Baseline = true)]
[BenchmarkCategory("Get")]
public void List_Get() {
for(var i = 0; i < Size; i++) {
_ = _fullList[i];
}
}
[Benchmark(Baseline = true)]
[BenchmarkCategory("Set")]
public void List_Set() {
for(var i = 0; i < Size; i++) {
_fullList[i] = i;
}
}
}
List<>
中也没有自动垃圾回收。 - BlindyList<>
在没有自动垃圾回收的情况下,也不会进行任何垃圾回收,只要您的类型是值类型。 - Blindy