别让”高性能“骗了你
博客园或是CSDN上总是会有一大堆诸如“.net X 引入全新方法 点燃性能革命”的标题党帖子。
千万不要被骗了,试试才知道!
为什么写这篇文章
某年某月某日作者我看到一篇盛赞System.Buffers.ArrayPool的教程。碰巧我在制作我的项目CsGrafeq,更巧的是这个绘图项目后端代码的核心之一是区间集合运算,换句话说是对两个数组的数据交叉分别运算,结果放入一个新数组
注:在以下的使用条件中 一般数组大小在4个元素以下 甚至大部分时候只有单个元素- //其中Range代表一个 (Inf,Sup) 元组结构体public static IntervalSet IntervalSetMethod(IntervalSet i1, IntervalSet i2,Func 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 Shared=ArrayPool.Shared;public static IntervalSet IntervalSetMethod(IntervalSet i1, IntervalSet i2,Func 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 _pool = default!; [GlobalSetup] public void Setup() { _pool = ArrayPool.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; }
复制代码 结果如下
MethodIterationsRequestedLengthClearOnReturnMeanErrorStdDevMedianRatioRatioSDGen0Gen1Gen2AllocatedAlloc RatioNewArray1000004False580.3 us11.85 us34.20 us573.2 us1.000.08637.2070--4000000 B1.00ArrayPool_RentReturn1000004False1,106.3 us39.18 us107.92 us1,126.0 us1.910.21----0.00NewArray1000004True545.7 us23.01 us65.64 us551.5 us1.030.25637.2070--4000000 B1.00ArrayPool_RentReturn1000004True1,451.9 us28.64 us62.86 us1,456.8 us2.730.60----0.00NewArray10000016False804.9 us41.15 us114.72 us828.8 us1.040.301402.3438--8800000 B1.00ArrayPool_RentReturn10000016False673.5 us26.67 us76.52 us639.5 us0.870.24----0.00NewArray10000016True428.8 us8.51 us18.86 us429.5 us1.000.061402.8320--8800000 B1.00ArrayPool_RentReturn10000016True756.1 us10.84 us9.05 us755.3 us1.770.08----0.00NewArray10000065536False725,208.8 us14,294.53 us40,551.21 us721,440.9 us1.0030.088333000.00008333000.00008333000.000026219426512 B1.00ArrayPool_RentReturn10000065536False628.4 us12.38 us17.36 us625.4 us0.0010.00----0.00NewArray10000065536True679,028.3 us13,495.60 us29,338.33 us680,642.6 us1.000.068333000.00008333000.00008333000.000026219525632 B1.00ArrayPool_RentReturn10000065536True368,874.7 us1,215.74 us1,137.20 us368,559.3 us0.540.02----0.00分析结果可以得到,对于ArrayPool的Rent操作,不论数组大小基本可以保持在600μs左右,出于未知的原因,当数组长度为4时,竟达到了1000μs
同时Return操作(即ZeroMemory)所需时间随数组大小基本线性增长,在65536长度下,总时长也基本在Array时长的一半。
总体而言,ArrayPool并没有表现出想象中那么牛逼的效果。在极小数组情况下,不论是否清除数据,效率都不及Array,而在大数组下,的确有显著效率优势。
当然,这项测试存在一些问题,比如没有考虑ArrayPool的Return操作带来的GC压力减轻等。不过从我踩坑的血泪教训可以看出,在小数组下,ArrayPool绝非一个好的选择。
就这样我因为盲目追求高性能,浪费了一个下午的宝贵时光,把所有的数组改成了ArrayPool,然后git回档。。。
All in all 搞清楚真实使用场景,以此作适当的测试,再使用!!!
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |