- 在VisualStudio中部署GDAL库的C++版本(包括SQLite、PROJ等依赖)
- Android开机流程介绍
- STM32CubeMX教程31USB_DEVICE-HID外设_模拟键盘或鼠标
- 深入浅出Java多线程(五):线程间通信
1 using System; 2 using System.Buffers; 3
4 class ArrayPoolExample 5 { 6 static void Main() 7 { 8 // 创建数组池实例
9 ArrayPool<int> arrayPool = ArrayPool<int>.Shared; 10
11 // 请求租借一个大小为 5 的数组
12 int[] rentedArray = arrayPool.Rent(5); 13
14 try
15 { 16 // 使用租借的数组进行操作
17 for (int i = 0; i < rentedArray.Length; i++) 18 { 19 rentedArray[i] = i * 2; 20 } 21 } 22 finally
23 { 24 // 使用完毕后归还数组到数组池
25 arrayPool.Return(rentedArray); 26 } 27 } 28 }
以上的样例比较简单,主要包含:创建数组池实例、租借一个大小为 5 的数组、使用租借的数组进行操作、使用完毕后归还数组到数组池。在实际的项目中,我们可以对ArrayPool进行包装,创建我们需要的不同对象池的管理,这可以根据我们实际的项目需求进行开发.
1 private static readonly SharedArrayPool<T> s_shared = new SharedArrayPool<T>(); 2
3 public static ArrayPool<T> Shared => s_shared; 4
5 public static ArrayPool<T> Create() => new ConfigurableArrayPool<T>();
从以上ArrayPool的初始化代码可以发现,其数组对象池的创建是由ConfigurableArrayPool类完成的,那么我们继续看一下对应的初始化逻辑。部分代码已经做过删减,我们只关注核心的实现逻辑,需要看全部的实现代码的同学,可以自行前往GitHub上查看.
1 private const int DefaultMaxArrayLength = 1024 * 1024; 2 private const int DefaultMaxNumberOfArraysPerBucket = 50; 3 private readonly Bucket[] _buckets; 4 internal ConfigurableArrayPool() : this(DefaultMaxArrayLength, DefaultMaxNumberOfArraysPerBucket){ } 5 internal ConfigurableArrayPool(int maxArrayLength, int maxArraysPerBucket) 6 { 7 ... 8
9 int maxBuckets = Utilities.SelectBucketIndex(maxArrayLength); 10 var buckets = new Bucket[maxBuckets + 1]; 11 for (int i = 0; i < buckets.Length; i++) 12 { 13 buckets[i] = new Bucket(Utilities.GetMaxSizeForBucket(i), maxArraysPerBucket, poolId); 14 } 15 _buckets = buckets; 16 }
我们从源码中可以看出几个比较重要的实现逻辑,ConfigurableArrayPool在初始化时,设置了默认的两个参数DefaultMaxArrayLength和DefaultMaxNumberOfArraysPerBucket,分别用于设置默认的池中每个数组的默认最大长度(2^20)和设置每个桶默认可出租的最大数组数。根据传入的参数,对其调用Utilities.SelectBucketIndex(maxArrayLength)进行计算,根据最大数组长度计算出桶的数量 maxBuckets,然后创建一个数组 buckets.
1 internal static int SelectBucketIndex(int bufferSize) 2 { 3 return BitOperations.Log2((uint)bufferSize - 1 | 15) - 3; 4 }
SelectBucketIndex使用位操作和数学运算来确定给定缓冲区大小应分配到哪个桶。该方法的目的是为了根据缓冲区的大小,有效地将缓冲区分配到适当大小的桶中。在bufferSize大小介于 2^(n-1) + 1 和 2^n 之间时,分配大小为 2^n 的缓冲区。使用了BitOperations.Log2 方法,计算 (bufferSize - 1) | 15 的二进制对数(以 2 为底)。由于要处理1到16字节之间的缓冲区,使用了|15 来确保范围内的所有值都会变成15。最后,通过-3进行调整,以满足桶索引的需求。针对零大小的缓冲区,将其分配给最高的桶索引,以确保零长度的缓冲区不会由池保留。对于这些情况,池将返回 Array.Empty 单例.
1 internal static int GetMaxSizeForBucket(int binIndex) 2 { 3 int maxSize = 16 << binIndex; 4 return maxSize; 5 }
GetMaxSizeForBucket将数字 16 左移 binIndex 位。因为左移是指数增长的,所以这样的计算方式确保了每个桶的大小是前一个桶大小的两倍。初始桶的索引(binIndex 为 0)对应的最大大小为 16。这种是比较通用的内存管理的策略,按照一系列固定的大小划分内存空间,这样可以减少分配的次数。接下来我们看一下Bucket对象的初始化代码.
1 internal readonly int _bufferLength; 2 private readonly T[]?[] _buffers; 3 private readonly int _poolId; 4 private SpinLock _lock; 5 internal Bucket(int bufferLength, int numberOfBuffers, int poolId) 6 { 7 _lock = new SpinLock(Debugger.IsAttached); 8 _buffers = new T[numberOfBuffers][]; 9 _bufferLength = bufferLength; 10 _poolId = poolId; 11 }
SpinLock只有在附加调试器时才启用线程跟踪;它为Enter/Exit增加了不小的开销;numberOfBuffers表示可以租借的次数,只初始化定义个二维的泛型数组,未分配内存空间;bufferLength每个缓冲区的大小。以上的逻辑大家可能不是很直观,我们用一个简单的图给大家展示一下.
1 ArrayPool 2 |
3 +-- Bucket[0] (Buffer Size: 16) 4 | +-- Buffer 1 (Size: 16) 5 | +-- Buffer 2 (Size: 16) 6 | +-- ... 7 |
8 +-- Bucket[1] (Buffer Size: 32) 9 | +-- Buffer 1 (Size: 32) 10 | +-- Buffer 2 (Size: 32) 11 | +-- ... 12 |
13 ... 14 默认会创建50个Buffer
如果对C#的字典的结构比较了解的同学,可能很好理解,ArrayPool是由一个一维数组和一个二维泛型数组进行构建。无论是.NET 还是JAVA中,很多的复杂的数据结构都是由多种简单结构进行组合,这样不仅一定程度上保证数据的取的效率,又可以考虑插入、删除的性能,也兼顾内存的占用情况。这里用一个简单的图来说明一下二维数组的初始化时占用的内存的结构。(_buffers = new T[numberOfBuffers][]) 。
1 +-----------+
2 | arrayInt |
3 +-----------+
4 | [0] | --> [ ] (Possibly null or an actual array) 5 +-----------+
6 | [1] | --> null
7 +-----------+
8 | [2] | --> null
9 +-----------+
10
11 +----------+
12 | arrInt1 |
13 +----------+
14 | | --> [ ] (Possibly null or an actual array) 15 +----------+
1 public override T[] Rent(int minimumLength) 2 { 3 if (minimumLength == 0){ return Array.Empty<T>(); } 4 T[]? buffer; 5 int index = Utilities.SelectBucketIndex(minimumLength); 6 if (index < _buckets.Length) 7 { 8 const int MaxBucketsToTry = 2; 9 int i = index; 10 do
11 { 12 buffer = _buckets[i].Rent(); 13 if (buffer != null) { return buffer; } 14 } 15 while (++i < _buckets.Length && i != index + MaxBucketsToTry); 16 buffer = new T[_buckets[index]._bufferLength]; 17 } 18 else
19 { 20 buffer = new T[minimumLength]; 21 } 22 return buffer; 23 }
从源码中我们可以看到,如果请求的数组长度为零,直接返回一个空数组。允许请求零长度数组,因为它是一个有效的长度数组。因为在这种情况下,池的大小没有限制,不需要进行事件记录,并且不会对池的状态产生影响。根据传入的minimumLength确定数组长度对应的池的桶的索引,在选定的桶中尝试租用数组,如果找到可用的数组,记录相应的事件并返回该数组。如果未找到可用的数组,会尝试在相邻的几个桶中查找(MaxBucketsToTry=2)。buffer = newT[_buckets[index]._bufferLength]表示如果池已耗尽,则分配一个具有相应大小的新缓冲区到合适的桶。buffer = new T[minimumLength]请求的大小对于池来说太大了,分配一个完全符合所请求长度的数组。 当它返回到池中时,我们将直接扔掉它.
1 internal T[]? Rent() 2 { 3 T[]?[] buffers = _buffers; 4 T[]? buffer = null; 5 bool lockTaken = false, allocateBuffer = false; 6 try
7 { 8 _lock.Enter(ref lockTaken); 9 if (_index < buffers.Length) 10 { 11 buffer = buffers[_index]; 12 buffers[_index++] = null; 13 allocateBuffer = buffer == null; 14 } 15 } 16 finally
17 { 18 if (lockTaken) _lock.Exit(false); 19 } 20 if (allocateBuffer) 21 { 22 buffer = new T[_bufferLength]; 23 } 24 return buffer; 25 }
我们来具体看一下这个方法的核心逻辑。T[]?[] buffers = _buffers通过获取 _buffers 字段的引用,获取桶中缓冲区数组的引用,并初始化一个用于保存租用的缓冲区的变量 buffer。使用 SpinLock 进入临界区,在临界区中,检查 _index 是否小于缓冲区数组的长度buffers.Length。来判断桶是否还有缓冲区可以使用。我们从if(allocateBuffer)可以看出,如果allocateBuffer==null时,则需要生成一个对应大小的缓冲区。可以明显的看到,具体的缓冲区对象都是在第一次使用的时候生成的,未使用时并不初始化,不占据内存空间.
1 public override void Return(T[] array, bool clearArray = false) 2 { 3 if (array.Length == 0) { return; } 4 int bucket = Utilities.SelectBucketIndex(array.Length); 5 bool haveBucket = bucket < _buckets.Length; 6 if (haveBucket) 7 { 8 if (clearArray) { Array.Clear(array); } 9 _buckets[bucket].Return(array); 10 } 11 }
首先是对归还的数组对象进行长度的判断,如果传入的数组长度为零,表示是一个空数组,直接返回,不进行任何处理。在池中,对于长度为零的数组,通常不会真正从池中取出,而是返回一个单例,以提高效率。然后根据数组的长度计算确定传入数组的长度对应的桶的索引。bucket < _buckets.Lengt判断是否存在与传入数组长度对应的桶,如果存在,表示该数组的长度在池的有效范围内。如果存在对应的桶,根据用户传入的 clearArray 参数,选择是否清空数组内容,然后将数组返回给对应的桶。_buckets[bucket].Return(array)将缓冲区返回到它的bucket。将来,我们可能会考虑让Return返回false不掉一个桶,在这种情况下,我们可以尝试返回到一个较小大小的桶,就像在Rent中,我们允许从更大的桶中租用.
1 internal void Return(T[] array) 2 { 3 if (array.Length != _bufferLength) 4 { 5 throw new ArgumentException(SR.ArgumentException_BufferNotFromPool, nameof(array)); 6 } 7 bool returned; 8 bool lockTaken = false; 9 try
10 { 11 _lock.Enter(ref lockTaken); 12 returned = _index != 0; 13 if (returned) { _buffers[--_index] = array; } 14 } 15 finally
16 { 17 if (lockTaken) _lock.Exit(false); 18 } 19 }
这一部分的实现逻辑相对较简单,首先判断归还的数组对象长度是否符合要求,在将缓冲区返回到桶之前,首先检查传入的缓冲区的长度是否与桶的期望长度相匹配。 如果长度不匹配,抛出 ArgumentException,表示传入的缓冲区不是从该池中租用的。使用 SpinLock 进入临界区。在临界区中,检查是否有可用的空槽,如果有,则将传入的缓冲区放入下一个可用槽,并将 _index 减小。如果没有可用槽,则不存储缓冲区。使用 try/finally 语句确保在退出临界区时正确释放锁,以处理可能的线程中止.
最后此篇关于深度解析C#数组对象池ArrayPool底层原理的文章就讲到这里了,如果你想了解更多关于深度解析C#数组对象池ArrayPool底层原理的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我对这个错误很困惑: Cannot implicitly convert type 'System.Func [c:\Program Files (x86)\Reference Assemblies\
考虑这段代码: pub trait Hello { fn hello(&self); } impl Hello for Any { fn hello(&self) {
问题很简单。是否可以构造这样一个类型 T,对于它下面的两个变量声明会产生不同的结果? T t1 = {}; T t2{}; 我已经研究 cppreference 和标准一个多小时了,我了解以下内容:
Intellij idea 给我这个错误:“Compare (T, T) in Comparator cannot be applied to (T, T)” 对于以下代码: public class
任何人都可以告诉我 : n\t\t\t\t\n\t\t\t 在以下来自和 dwr 服务的响应中的含义和用途是什么. \r\n\t\t\t \r\n\t\t\t
让 T 成为一个 C++ 类。 下面三个指令在行为上有什么区别吗? T a; T a(); T a = T(); T 为不带参数的构造函数提供了显式定义这一事实是否对问题有任何改变? 后续问题:如果
Rust中的智能指针是什么 智能指针(smart pointers)是一类数据结构,是拥有数据所有权和额外功能的指针。是指针的进一步发展 指针(pointer)是一个包含内存地
比如我有一个 vector vector > v={{true,1},{true,2},{false,3},{false,4},{false,5},{true,6},{false,7},{true,8
我有一个来自 .xls 电子表格的数据框,我打印了 print(df.columns.values) 列,输出包含一个名为:Poll Responses\n\t\t\t\t\t。 我查看了 Excel
This question already has answers here: What are good reasons for choosing invariance in an API like
指针类型作为类型前缀与在类型前加斜杠作为后缀有什么区别。斜线到底是什么意思? 最佳答案 语法 T/~ 和 T/& 基本上已被弃用(我什至不确定编译器是否仍然接受它)。在向新向量方案过渡的初始阶段,[T
我正在尝试找到一种方法来获取模板参数的基类。 考虑以下类: template class Foo { public: Foo(){}; ~Foo(){};
这是一个让我感到困惑的小问题。我不知道如何描述它,所以只看下面的代码: struct B { B() {} B(B&) { std::cout ::value #include
为什么有 T::T(T&) 而 T::T(const T&) 更适合 copy ? (大概是用来实现move语义的???) 原始描述(被melpomene证明是错误的): 在C++11中,支持了一种新
在 Java 7 中使用 eclipse 4.2 并尝试实现 List 接口(interface)的以下方法时,我收到了警告。 public T[] toArray(T[] a) { ret
假设有三个函数: def foo[T](a:T, b:T): T = a def test1 = foo(1, "2") def test2 = foo(List(), ListBuffer()) 虽
我对柯里化(Currying)和非柯里化(Currying)泛型函数之间类型检查的差异有点困惑: scala> def x[T](a: T, b: T) = (a == b) x: [T](a: T,
考虑一个类A,我如何编写一个具有与相同行为的模板 A& pretty(A& x) { /* make x pretty */ return x; } A pretty(A&& x) {
Eclipse 表示由于泛型类型橡皮擦,类型参数不允许使用 instanceof 操作。 我同意在运行时不会保留任何类型信息。但是请考虑以下类的通用声明: class SomeClass{ T
在 C++14 中: 对于任何整数或枚举类型 T 以及对于任何表达式 expr: 有没有区别: struct S { T t { expr }; }; 和 struct S { T t = { exp
我是一名优秀的程序员,十分优秀!