在 Nick Chapsas 最近制作的这段视频中,他在结尾评论说该Find
方法List<T>
“未优化”,因为代码不是在数组的 Span 上进行迭代,而是在数组本身上进行迭代:
public T? Find(Predicate<T> match) {
if (match == null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
}
for (int i = 0; i < _size; i++) {
if (match(_items[i])) {
return _items[i];
}
}
return default;
}
从Stephen Toub 在 Spans 上发表的(更新的?)博客文章的这一段来看,我认为对 Spans 进行迭代可以像对数组进行迭代一样快(但不会更快) :
汇编代码之所以如此相似,部分原因是消除了边界检查。但 JIT 将跨度索引器识别为内在函数,这意味着 JIT 会为索引器生成特殊代码,而不是将其实际的 IL 代码转换为汇编代码。
所有这些都是为了说明运行时可以对跨度应用 与数组相同类型的优化,从而使跨度成为访问数据的有效机制。
什么时候人们会不言而喻地认为,对数组的 Span 进行迭代会和对数组本身进行迭代一样快,甚至可能更快?哪个 .NET 版本引入了这些不能用于数组的 JIT Span 优化?
遍历跨度有两个优点:
如果不是因为
_size
也在起作用,那么可能可以修复代码;例如:只需将其提升
_arr
到本地即可允许 JIT 省略边界检查,因为无法重新分配数组:还要注意,
foreach (var x in _arr)
和都foreach (var x in arr)
可以,并且可以省略边界检查。但是,麻烦的_size
意味着边界检查不能省略,并且如果_size
不匹配_arr.Length
(缓冲区过大),我们就不能使用简单的foreach
。Span 间接解决了这个问题,因为跨度是有界的;创建长度的跨度_size
意味着一旦构造了跨度(在构造函数中使用边界验证),JIT 就可以信任它,并省略边界检查。