¿Existen estructuras de datos para .NET (las versiones recientes) que tengan un modo inicial "pequeño" que no asigne memoria? Algo que usaría inicialmente la memoria en la pila (stackalloc), luego, si todo eso está agotado, cambie a usar el montón y solo luego ejerza presión sobre GC. Una SmallList<T> con una interfaz similar a List<T> sería la más útil cuando sabe que, en la mayoría de los casos, solo se agregan algunos elementos.
En C++ hay al menos una buena implementación de esta idea utilizada en LLVM, SmallVector y algunos tipos pequeños similares para conjuntos/mapas. ¿Cuál es la diferencia entre std::vector y llvm::SmallVector? ¿Cuál usar cuando?
También este video: CppCon 2016: Chandler Carruth "Código de alto rendimiento 201: Estructuras de datos híbridas"
He estado buscando en Google un poco y no pude encontrar nada sobre una implementación existente, y si es posible hacerlo. Tendría que limitarse a los tipos de valor con más certeza, pero incluso entonces no estoy seguro de cómo escribiría algún valor T en la matriz stackalloc y lo volvería a leer, por ejemplo. si T es un int, ¿hay ahora una API para "reinterpret_cast" un rango de bytes de un intervalo a un int directamente, no combinando los 4 bytes uno por uno?
La estructura de datos más cercana a esta idea que pude encontrar es SStringBuilder de Towel lib: https://github.com/ZacharyPatten/Towel/blob/main/Sources/Towel/SStringBuilder.cs
// SStringBuilder is a small helper for initializing strings. // It will append to the span until the capacity is reached // and then it will revert to a StringBuilder if necessary Editar
Encontré esta API, puede ser lo que se necesita https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.memorymarshal.read
Editar 2
Hice una pequeña lista pirateada que solo funciona con una pequeña cantidad de valores, lanzando una excepción si se alcanza la capacidad en lugar de cambiar a una lista. Intenté algunas formas de hacer esto:
Hasta cerca de 32 valores, las versiones Span son mucho más rápidas, a continuación se muestran algunos resultados de BenchmarkDotNet: el banco solo agrega N estructuras pequeñas (2 campos int) a la lista y luego obtiene cada valor y suma los campos. N era 1,2,4... y algunos más hasta 128, era curioso en qué punto desaparece la ventaja, en la práctica la parte de la pila debería ser para < 10 valores.
Results for 2 values: | SpanListBench | 2 | 6.126 ns | 0.0188 ns | 0.0147 ns | | TypedSpanlListBench | 2 | 5.403 ns | 0.0147 ns | 0.0123 ns | | PoolListBench | 2 | 24.182 ns | 0.0375 ns | 0.0351 ns | | ListBench | 2 | 18.160 ns | 0.1737 ns | 0.1540 ns | | ListBenchPrealloc | 2 | 13.754 ns | 0.0978 ns | 0.0867 ns | Results for 8 values: | SpanListBench | 8 | 18.045 ns | 0.0494 ns | 0.0438 ns | | TypedSpanListBench | 8 | 16.467 ns | 0.0565 ns | 0.0472 ns | | PoolListBench | 8 | 31.558 ns | 0.0735 ns | 0.0651 ns | | ListBench | 8 | 44.527 ns | 0.1977 ns | 0.1849 ns | | ListBenchPrealloc | 8 | 29.989 ns | 0.2079 ns | 0.1736 ns | ref struct TypedSpanList<T> where T:struct { private Span<T> buffer_; private int count_; public TypedSpanList(Span<T> buffer) { buffer_ = buffer; count_ = 0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(T value) { if (count_ >= buffer_.Length) { throw new InvalidOperationException(); } buffer_[count_] = value; count_++; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public T Get(int index) { if (index >= buffer_.Length) { throw new InvalidOperationException(); } return buffer_[index]; } } [Benchmark] public int TypedListBench() { int count = 0; Span<Test> s = stackalloc Test[Number]; var sl = new TypedSpanList<Test>(s); for (int i = 0; i < Number; i++) { sl.Add(new Test() { a = i, b = i + 1 }); } for (int i = 0; i < Number; i++) { var t = sl.Get(i); count += ta + tb; } return count; }Hasta donde yo sé, no hay una solución lista para usar de .NET.
La pregunta más importante es: ¿qué necesito? List tiene un mecanismo de control de versiones interno que verifica si se actualizó durante la iteración. ¿Necesitas eso para tu versión pequeña? La iteración es un tema desafiante en sí mismo: consulte la lista de go on de .NET .
Si usa una estructura (por referencia no le permite implementar ninguna interfaz además de IDisposable ), parte de su ganancia sin asignación podría perderse cuando habla a través de la interfaz (debido al boxeo).
Entonces, ¿qué es lo más importante? ¿Mutar sus elementos? pasando los elementos a lo largo? ¿Iterar a través de todos/algunos de los elementos?
Es obvio que podría ser una optimización prematura, así que asumo que ya lo tuvo en cuenta.
Y hasta donde yo sé, la inserción agresiva no funciona si lanzas excepciones en esos métodos.