这与为什么GCC不能为两个int32s的结构生成最优运算符==有关?。我在godbolt.org玩弄那个问题的代码,注意到了这个奇怪的行为。
struct Point {
int x, y;
};
bool nonzero_ptr(Point const* a) {
return a->x || a->y;
}
bool nonzero_ref(Point const& a) {
return a.x || a.y;
}
https://godbolt.org/z/e49h6d
对于非零ptr,clang-O3(所有版本)生成此代码或类似代码:
mov al, 1
cmp dword ptr [rdi], 0
je .LBB0_1
ret
.LBB0_1:
cmp dword ptr [rdi + 4], 0
setne al
ret
这严格实现了C函数的短路行为,仅当x字段为零时加载y字段。
对于非零参考,clang 3.6和更早版本生成与非零ptr相同的代码,但clang 3.7到11.0.1生成
mov eax, dword ptr [rdi + 4]
or eax, dword ptr [rdi]
setne al
ret
无条件加载y
。当参数是指针时,没有版本的clang愿意这样做。为什么?
我能想到的(在x64平台上)分支代码行为明显不同的唯一情况是当[rdi 4]
处没有内存映射时,但我仍然不确定为什么clang会认为这种情况对指针而不是引用很重要。我最好的猜测是有一些语言法律论据认为引用必须是“完整对象”,而指针不必是:
char* p = alloc_4k_page_surrounded_by_guard_pages();
int* pi = reinterpret_cast<int*>(p + 4096 - sizeof(int));
Point* ppt = reinterpret_cast<Point*>(pi); // ok???
ppt->x = 42; // ok???
Point& rpt = *ppt; // UB???
但是,如果规范暗示了这一点,我不知道该怎么做。
我相信,从标准C的角度来看,编译器可以为两者发出相同的代码,因为标准中没有像您构建的那样的“部分对象”的规定。事实上,这可能只是一个错过的优化。
人们可以比较像a-这样的代码
或者,clang作为扩展,可能试图生成在您使用非标准功能创建只能访问某些成员的对象时仍然有效的代码。这适用于指针而不适用于引用的事实可能是该扩展的错误或限制,但我不认为这是任何形式的一致性违规。
这是一个错过的优化;无分支代码对两个C源代码版本都是安全的。
为什么gcc可以从结构中推测加载?GCC实际上是通过指针推测性地加载两个结构成员,即使C源代码只引用其中一个。所以,至少GCC开发人员已经决定,在他们对C和C标准的解释中,这种优化是100%安全的(我认为这是故意的,不是一个bug)。Clang生成一个0或1索引来选择要加载的int,因此Clang仍然像您的情况一样不愿意发明加载。(C与C:相同的asm,带或不带xc,源代码的版本移植为:https://godbolt.org/z/6oPKKd)
asm中的明显区别在于指针版本避免访问-
但是ISO C不允许部分对象。您示例中的页面边界设置是我很确定未定义的行为。在读取a的执行路径中-
这当然不是
int*p;
和p[0]||p[1]
的情况,因为在页面的最后4个字节中拥有一个恰好为1个元素长的隐式长度0终止数组是完全有效的。
正如@Nate在评论中所建议的,也许clang在优化时根本没有利用ISO C这一事实;在考虑这种“如果转换”类型的优化(分支到无分支)时,它可能会在内部转换为更像数组的东西。或者LLVM只是不让自己通过指针发明负载。
它始终可以为引用参数执行此操作,因为引用保证为非NULL。调用方执行非零ref(*ppt)
“甚至更多”,就像在部分对象示例中一样,因为在C术语中,我们取消了对整个对象的指针的引用。
bool nonzero_ptr_full_deref(Point const* pa) {
Point a = *pa;
return a.x || a.y;
}
https://godbolt.org/z/ejrn9h-无分支编译,与非零参考编译相同。不确定这能告诉我们什么/多少。这就是我所期望的,因为它可以访问-
脚注1:像所有主流的ISA一样,x86-64不进行硬件竞争检测,所以加载另一个线程可能正在编写的东西的可能性只对性能有影响,并且只有当完整的结构跨缓存线边界拆分时,因为我们已经在读取一个成员。如果对象不跨越缓存线,任何错误共享性能效果都已经产生了。
像这样制作ASM不会“引入数据竞争UB”,因为x86 ASM与ISO C不同,对这种可能性有明确定义的行为。ASM适用于从[rdi 4]
加载的任何可能值,因此它正确地实现了C源代码的语义学。与写入不同,发明读取是线程安全的,并且是允许的,因为它不是易失性
,因此访问不是可见的副作用。唯一的问题是指针是否必须指向完整有效的Point
对象。
数据竞争的一部分(在非原子
对象上)是未定义的行为,允许在硬件上使用竞争检测进行C实现。另一个是允许编译器假设重新加载他们访问过一次的东西是安全的,并且期望相同的值,除非两点之间有一个获取或seq_cst加载。甚至制作如果第二次加载与第一次不同会崩溃的代码。在这种情况下,这是无关紧要的,因为我们不是在谈论将1访问变成2(而是将0变成1,其值可能无关紧要),而是为什么滚动你自己的原子(例如在Linux内核中)需要为ACCESS_ONCE
使用强制转换(https://lwn.net/Articles/793253/#Invented加载)。
我承认这个问题的答案可能是“一些非常具体的魔法”,但我对我在这里观察到的有点震惊。我想知道是否有人了解这些类型的优化是如何工作的。我发现编译器设计非常有趣,我真的无法想象这是如何工作的。我肯定答案在clang源代码的某个地方,但我甚至不知道我会去哪里看。 我是大学一堂课的助教,最近我被要求帮助解决一个简单的家庭作业问题。这让我走上了一条有趣的道路...... 问题很简单:在x86_64程序集中,编
问题1. 为什么将原生指针放到智能指针里后,再通过 get()取出来的地址和原生指针地址不同呢? 比如例子中打印的base0和 base2不同。 问题2. 为什么将 nullptr 放到智能指针里后,通过 get()取出来的地址不是 nullptr 呢? 那如果判断智能指针管理的原生指针是否为 nullptr呢? 输出: base0=0x156704080 base1=0x0 base2=0x15
我正在通过Sonarqube在代码上运行findbugs,我得到一个空指针解引用错误: 有一个语句分支,如果执行,保证空值将被取消引用。 故障代码很简单: 我想知道这怎么可能。NPE唯一可能的地方是调用x.compareTo(y)时,但如果x=null,Java将永远不会分析该分支,对吗? 这是一个bug,还是我遗漏了Java分析这条语句的方法? 更新 谢谢你的意见。最后我建议他们改成: 我发现这
我有java代码,我改成kotlin了,我的代码是用pdf-viewer库显示pdf的,我不明白为什么我的代码是错误的,下面是错误: 指定为non-null的是null参数:方法kotlin.jvm.internal.intrinsics.CheckParameterIsNotNull,inputStream参数 这是我的密码
问题内容: 这个来自json.Unmarshal docs的示例(为便于使用而不是进行了稍微修改)有效,没有错误: 工作示例的游乐场链接 但是这个经过稍微修改的示例却没有: 非工作示例的游乐场链接 它显示了这个实际上并没有帮助的模糊错误(看起来更像是一个函数调用,而不是错误的IMO): json:Unmarshal(nil * main.Animal) 这似乎是因为它是未初始化的指针。但是文档说(
问题内容: 如果我删除in : github.com/creating_web_app_go/main.go:8:不能在http.HandleFunc的参数中使用func文字(类型func(http.ResponseWriter,http.Request))作为func(http.ResponseWriter,* http.Request)类型。 我对Go和指针都很陌生。 所以问题是,为什么必须是