别让”高性能“骗了你
博客园或是CSDN上总是会有一大堆诸如“.net X 引入全新方法 点燃性能革命”的标题党帖子。
千万不要被骗了,试试才知道!
为什么写这篇文章
某年某月某日作者我看到一篇盛赞System.Buffers.ArrayPool的教程。碰巧我在制作我的项目CsGrafeq,更巧的是这个绘图项目后端代码的核心之一是区间集合运算,换句话说是对两个数组的数据交叉分别运算,结果放入一个新数组
注:在以下的使用条件中 一般数组大小在4个元素以下 甚至大部分时候只有单个元素
//其中Range代表一个 (Inf,Sup) 元组结构体
public static IntervalSet IntervalSetMethod(IntervalSet i1, IntervalSet i2,Func<Range, Range, Range> handler)
{
var Ranges = new Range[i1.Intervals.Length * i2.Intervals.Length];
var loc = 0;
foreach (var i in i1.Intervals)
foreach (var j in i2.Intervals)
Ranges[loc++] = handler(i, j);
return IntervalSet.Create(FormatRanges(Ranges), i1._Def & i2._Def);
}
于是乎我想都没想,把代码改成了这样
private readonly ArrayPool<Range> Shared=ArrayPool<Range>.Shared;
public static IntervalSet IntervalSetMethod(IntervalSet i1, IntervalSet i2,Func<Range, Range, Range> handler)
{
var Ranges = Shared.Rend(i1.Intervals.Length * i2.Intervals.Length);
var loc = 0;
foreach (var i in i1.Intervals)
foreach (var j in i2.Intervals)
Ranges[loc++] = handler(i, j);
Shared.Return(i1.Intervals);
Shared.Return(i2.Intervals);
return IntervalSet.Create(FormatRanges(Ranges), i1._Def & i2._Def);
}
WoW 这完美符合了
System.Buffers 命名空间下提供了一个可对 array 进行复用的高性能池化类 ArrayPool
,在经常使用 array 的场景下可使用 ArrayPool 来减少内存占用,提升效率
然而令我大跌眼镜的是,看上去高端大气上档次的ArrayPool实战拉跨到了极致。运行效率经过大约估计,慢了不止十倍
我去!怎么会这样???
实际测试
本着Talk is cheap, show me the code的箴言,写一个基准测试
[MemoryDiagnoser]
[SimpleJob]
public class ArrayAllocVsPoolBenchmarks
{
// 每次迭代要“分配/租用”的数组个数
[Params(100_000)]
public int Iterations { get; set; }
//超小 小 大 数组
[Params(4,16, 65_536)]
public int RequestedLength { get; set; }
// 是否在归还时清零
[Params(false, true)]
public bool ClearOnReturn { get; set; }
private ArrayPool<int> _pool = default!;
[GlobalSetup]
public void Setup()
{
_pool = ArrayPool<int>.Shared;
}
[Benchmark(Baseline = true)]
public int NewArray()
{
int checksum = 0;
for (int i = 0; i < Iterations; i++)
{
var arr = new int[RequestedLength];
// 写入少量元素,避免 JIT 把分配当成“无用”而过度优化
// 同时不让工作量随数组大小线性暴涨,聚焦“分配/回收”的差异
arr[0] = i;
arr[^1] = i ^ 12345;
checksum += arr[0];
checksum += arr[^1];
}
return checksum;
}
[Benchmark]
public int ArrayPool_RentReturn()
{
int checksum = 0;
for (int i = 0; i < Iterations; i++)
{
var arr = _pool.Rent(RequestedLength);
try
{
arr[0] = i;
arr[RequestedLength - 1] = i ^ 12345;
checksum += arr[0];
checksum += arr[RequestedLength - 1];
}
finally
{
_pool.Return(arr, clearArray: ClearOnReturn);
}
}
return checksum;
}
结果如下
| Method | Iterations | RequestedLength | ClearOnReturn | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| NewArray | 100000 | 4 | False | 580.3 us | 11.85 us | 34.20 us | 573.2 us | 1.00 | 0.08 | 637.2070 | - | - | 4000000 B | 1.00 |
| ArrayPool_RentReturn | 100000 | 4 | False | 1,106.3 us | 39.18 us | 107.92 us | 1,126.0 us | 1.91 | 0.21 | - | - | - | - | 0.00 |
| NewArray | 100000 | 4 | True | 545.7 us | 23.01 us | 65.64 us | 551.5 us | 1.03 | 0.25 | 637.2070 | - | - | 4000000 B | 1.00 |
| ArrayPool_RentReturn | 100000 | 4 | True | 1,451.9 us | 28.64 us | 62.86 us | 1,456.8 us | 2.73 | 0.60 | - | - | - | - | 0.00 |
| NewArray | 100000 | 16 | False | 804.9 us | 41.15 us | 114.72 us | 828.8 us | 1.04 | 0.30 | 1402.3438 | - | - | 8800000 B | 1.00 |
| ArrayPool_RentReturn | 100000 | 16 | False | 673.5 us | 26.67 us | 76.52 us | 639.5 us | 0.87 | 0.24 | - | - | - | - | 0.00 |
| NewArray | 100000 | 16 | True | 428.8 us | 8.51 us | 18.86 us | 429.5 us | 1.00 | 0.06 | 1402.8320 | - | - | 8800000 B | 1.00 |
| ArrayPool_RentReturn | 100000 | 16 | True | 756.1 us | 10.84 us | 9.05 us | 755.3 us | 1.77 | 0.08 | - | - | - | - | 0.00 |
| NewArray | 100000 | 65536 | False | 725,208.8 us | 14,294.53 us | 40,551.21 us | 721,440.9 us | 1.003 | 0.08 | 8333000.0000 | 8333000.0000 | 8333000.0000 | 26219426512 B | 1.00 |
| ArrayPool_RentReturn | 100000 | 65536 | False | 628.4 us | 12.38 us | 17.36 us | 625.4 us | 0.001 | 0.00 | - | - | - | - | 0.00 |
| NewArray | 100000 | 65536 | True | 679,028.3 us | 13,495.60 us | 29,338.33 us | 680,642.6 us | 1.00 | 0.06 | 8333000.0000 | 8333000.0000 | 8333000.0000 | 26219525632 B | 1.00 |
| ArrayPool_RentReturn | 100000 | 65536 | True | 368,874.7 us | 1,215.74 us | 1,137.20 us | 368,559.3 us | 0.54 | 0.02 | - | - | - | - | 0.00 |
分析结果可以得到,对于ArrayPool的Rent操作,不论数组大小基本可以保持在600μs左右,出于未知的原因,当数组长度为4时,竟达到了1000μs
同时Return操作(即ZeroMemory)所需时间随数组大小基本线性增长,在65536长度下,总时长也基本在Array时长的一半。
总体而言,ArrayPool并没有表现出想象中那么牛逼的效果。在极小数组情况下,不论是否清除数据,效率都不及Array,而在大数组下,的确有显著效率优势。
当然,这项测试存在一些问题,比如没有考虑ArrayPool的Return操作带来的GC压力减轻等。不过从我踩坑的血泪教训可以看出,在小数组下,ArrayPool绝非一个好的选择。
就这样我因为盲目追求高性能,浪费了一个下午的宝贵时光,把所有的数组改成了ArrayPool,然后git回档。。。