最近在看tokio源代码,在tokio/src/util/linked_list.rs之中看到了这样一段注释:
/// Previous / next pointers.
pub(crate) struct Pointers<T> {
inner: UnsafeCell<PointersInner<T>>,
}
/// We do not want the compiler to put the `noalias` attribute on mutable
/// references to this type, so the type has been made `!Unpin` with a
/// `PhantomPinned` field.
///
/// Additionally, we never access the `prev` or `next` fields directly, as any
/// such access would implicitly involve the creation of a reference to the
/// field, which we want to avoid since the fields are not `!Unpin`, and would
/// hence be given the `noalias` attribute if we were to do such an access. As
/// an alternative to accessing the fields directly, the `Pointers` type
/// provides getters and setters for the two fields, and those are implemented
/// using `ptr`-specific methods which avoids the creation of intermediate
/// references.
///
/// See this link for more information:
/// <https://github.com/rust-lang/rust/pull/82834>
struct PointersInner<T> {
/// The previous node in the list. null if there is no previous node.
prev: Option<NonNull<T>>,
/// The next node in the list. null if there is no previous node.
next: Option<NonNull<T>>,
/// This type is !Unpin due to the heuristic from:
/// <https://github.com/rust-lang/rust/pull/82834>
_pin: PhantomPinned,
}
这是一个双向链表的指针部分,但是这里的注释的含义我不太理解:好像加入PhantomPinned不是为了避免借出可变引用,改变链表元素在内存之中的位置一样,反而就是为了阻止Rust编译器生成noalias。
然后我就去网上查了一下这个noalias到底是什么,发现在21年在raddit上有很多关于这个的讨论(正好上面这段注释也是在2021年写的)。
大概意思就是说这个noalias可以让LLVM进行更加激进的优化,但是好像一直有bug,经常是修好一阵子就因为另外一些crate出现了编译问题就又关闭了。
所以,我的问题是:
- tokio在这里避免编译器生成noalias是因为21年这个特型还不稳定,防止因为Rustc的版本编译期爆炸。还是说从Rust语意上来说,使用PhantomPinned就是为了防止借出可变引用(后面使用Pin钉住),还是两者兼而有之?
- noalias 的作用具体是什么?我只找到了零散的描述,有没有什么博客我可以学习一下?
- 现在这个noalias Rust默认启用了吗?从讨论里面的描述上来看,应该是有一些性能提升的,如果还不能启用的话,原因是什么?
谢谢大家
1
共 8 条评论, 1 页
评论区
写评论stacked borrows: https://rust-unofficial.github.io/too-many-lists/fifth-stacked-borrows.html
这篇文档里面的小故事我感觉对理解aliasing非常有帮助!我直接帮大家翻译一下:
米歇尔有一天在整理自己的书架时,看到了一本自己不记得的书。他从书架上抽出那本书,仔细看了看封面。
“哦对,这可是我那本老版的《战争与和平》,一本我肯定读过的书。我很喜欢和平那部分的内容。”
突然,传来了敲门声。米歇尔将书放回书架,去开门——门外是他势不两立的死敌,汉姆斯劳。正当汉姆斯劳准备嘲讽米歇尔明显不如自己的代码技巧时,米歇尔捕捉到了一个反击的机会:
“嘿,汉姆斯劳,你读过《战争与和平》吗?”
“呸,根本没人真正读过《战争与和平》。”
“但我读过!看,它就在我的书架上,这显然证明我读过了。”
汉姆斯劳不敢相信。她一贯得意洋洋的表情瞬间变成了一副铁面怒容。她推开米歇尔,快步走向书架,以千名女武神的愤怒猛地抽出了那本书。她把这本古老的书翻来覆去地看,不知道是不是因为极度愤怒的原因,刚一看到封面,她就开始颤抖。
看到这一幕,米歇尔已经准备好炫耀自己无与伦比的聪明才智了,他刚准备开口:“哈……”,却被汉姆斯劳突然爆发的笑声打断了。
“这不是《战争与和平》,这是《战争与脚》!”
汉姆斯劳笑得眼泪都流了下来。显然,这是她人生中最乐的时刻。
“不——不可能!我刚刚看过了!”
米歇尔一把从汉姆斯劳手里抢过书,再次检查封面。果然,“和平”这个词被划掉了,取而代之的是“脚”。米歇尔羞愧难当。这显然是他人生中最急的时刻。
他跪倒在地,呆滞地盯着书架。怎么会这样?他明明才刚刚检查过封面!
就在这时,他看到书架里有点动静。是一个小人。一个带着米歇尔见过的最最最愤怒表情的小人。小人对米歇尔竖起中指,用嘴型清晰地说着“没人会相信你”,然后迅速消失在书架的缝隙间。
米歇尔的计划本来天衣无缝,但他忽略了一个可能性:一个拿着记号笔的小人,带着破坏的欲望和愤怒。他以为自己清楚书的封面上写的是什么,也以为没人可能改变它。可惜,他错了。
此时的汉姆斯劳已经开始制作一本小刊物,纪念她这次不可思议的胜利——米歇尔在本地Rust论坛的名声永远无法挽回了💔。
没有人想成为米歇尔,但也没有人想一直活在对那个“愤怒小人”的恐惧中。我们希望知道什么时候这个“愤怒小人”可能在捉弄我们。当他在捉弄我们时,我们会变得非常小心,甚至有些偏执,总是反复检查所有东西再去使用。但当“小小愤怒男人”离开时,我们希望能够“记住”事情。
这就是指针别名(pointer aliasing)问题的核心(非常简化后的版本):编译器什么时候可以假定“记住”(缓存)值是安全的,而不是每次都重新加载?为了做到这一点,编译器需要知道什么时候可能会有“愤怒小人”在背后偷偷修改内存。😉
旁白:编译器还利用这些信息来缓存写操作,这意味着如果编译器认为没有人会注意到,它可以避免将数据提交到内存中。在这种情况下,问题依然是这个“愤怒小人”,但这次他只需要读取内存就会导致问题出现。
谢谢你的补充!我最近也找到了一些关于alias优化的补充资料,希望也可以帮助卡在这个问题上的同学更好地理解这个概念:
这个讲的大概是Pointer aliasing实际上是Rust的借用检查器底层模型stacked borrows的Motivation
大概讲的是作者在实现一个名为left_right同步原语的时候因为aliasing踩的一些坑,以及如何绕过Rust编译机对自定义类型的aliasing标注
--
👇
RiversJin: Noalias 指的是一种优化约定. 指的是同一个内存区域, 只有一个指针指向它.
比如
这种情况就是违背了
Noalias
的约定. 通过这个限制, 编译器可以认为对应的内存区域只会被当前的变量修改, 这可以做出一些优化, 比如如果有两次读取行为, 第二次的读取就可以被省略掉, 因为没有别人修改嘛.至于为什么tokio的代码为什么使用
PhantomPinned
, 这个大家已经讲的很清楚我就不重复啦哦对, "noalias" 这个, 现在的rustc应该会默认启用的, 之前不启用主要是LLVM的实现在之前有一些bug, 会导致非预期的问题. 而C/C++的别名问题一直都很麻烦所以在Rust大规模使用"noalias"之前,没怎么引起重视所以修复速度也比较慢
不过现在应该已经基本修复了我记得.
Noalias 指的是一种优化约定. 指的是同一个内存区域, 只有一个指针指向它.
比如
这种情况就是违背了
Noalias
的约定. 通过这个限制, 编译器可以认为对应的内存区域只会被当前的变量修改, 这可以做出一些优化, 比如如果有两次读取行为, 第二次的读取就可以被省略掉, 因为没有别人修改嘛.至于为什么tokio的代码为什么使用
PhantomPinned
, 这个大家已经讲的很清楚我就不重复啦感谢!🙏
--
👇
az: 我也关注过这个话题,查过一些相关的内容。但LLVM我不懂,权当抛转引玉吧。
https://gist.github.com/Darksonn/1567538f56af1a8038ecc3c664a42462
↑这是Alice遇到帖子上的问题时写的一篇文章,基本把整个问题说清楚了。我按个人理解简单归纳下:通过侵入式链表保存waker可以避免大量的堆分配,但是它在Tokio的场景下会违反Rust的别名模型,而且Rust也没有暴露任何放松别名模型要求的原语,这种实现是unsound的。(其实unsafe链表不违反别名模型,Tokio这里的情况比较特殊)
因为Generator也有类似Tokio的问题,而且它们都和Pin相关,于是编译器给!Unpin的类型开了洞,关闭了别名模型对它们的要求(也就不对它们附加
noalias
声明了)。后来Ralf又提出了UnsafeAliased
(现在改成UnsafePinned
了)作为关闭别名要求的原语,它稳定后这块的实现会干净些。至于现状我也不清楚了。
我也关注过这个话题,查过一些相关的内容。但LLVM我不懂,权当抛转引玉吧。
https://gist.github.com/Darksonn/1567538f56af1a8038ecc3c664a42462
↑这是Alice遇到帖子上的问题时写的一篇文章,基本把整个问题说清楚了。我按个人理解简单归纳下:通过侵入式链表保存waker可以避免大量的堆分配,但是它在Tokio的场景下会违反Rust的别名模型,而且Rust也没有暴露任何放松别名模型要求的原语,这种实现是unsound的。(其实unsafe链表不违反别名模型,Tokio这里的情况比较特殊)
因为Generator也有类似Tokio的问题,而且它们都和Pin相关,于是编译器给!Unpin的类型开了洞,关闭了别名模型对它们的要求(也就不对它们附加
noalias
声明了)。后来Ralf又提出了UnsafeAliased
(现在改成UnsafePinned
了)作为关闭别名要求的原语,它稳定后这块的实现会干净些。至于现状我也不清楚了。
noalias是一个函数参数属性,专门为ptr类型设计的。 define void foo(ptr noalias %a)... 说明指针a不和其他指针指向同一内存区域。 llvm有个pass可以推断noalias信息,名字应该是funcattr什么的。
谷歌搜了一下 llvm noalias ,在这个 LLVM 文档页面上有相关的定义,这个 noalias(no alias,“没有别名”),我猜是来自“在编程语言中,我们用名字来称呼值”这种说法,所以说一个值“没有别名”意思可能就是说它只在一处被用到。
他们要在这里去掉 noalias 标记很可能与“双向链表中存放每一个节点的内存区域都会在两个地方被引用”有关。
至于为啥要加 PhantomPinned,应该是看到了这里那个“如果是 !Unpin 那我们就不加 noalias 了”。