编译器优化的锁正确性(C / gcc)

ss2ws0br  于 2023-05-16  发布在  其他
关注(0)|答案(3)|浏览(158)

在与我的教授讨论并发性时,他提到围绕锁的潜在编译器优化(重新排序指令,优化访问等),如pthread_mutex_lock()可能会导致问题。这不会发生的原因(根据他的说法)是编译器必须像黑盒一样对待函数调用,并且无法优化访问或围绕它重新排序,因为它不知道函数可能会对全局状态做什么。他说,如果你打开链接时优化,gcc -flto,你可能会发现你的锁突然停止工作。
他是一个相当有信誉的来源,一个开发了很多GNU核心实用程序的老派GNU家伙,甚至在gcc上工作过,但我想知道这怎么可能是真的,当编译器能够将memcpy这样的东西优化为单个指令时,有效地跨越模块边界而无需链接时优化。那么,它是否可以对pthread_mutex_lock()这样的东西做同样的事情,并优化它周围的访问?
如果这是真的,编译器可以查看pthread_mutex_lock()函数,并看到它没有改变某个变量,因此它优化了访问,这会影响正确性吗?如果是这样,这样的核心函数怎么会如此容易受到无法正常工作的影响?这是否意味着必须使用其他方法来告诉编译器不要优化对这些变量的访问,例如volatile结构?当考虑特定于模块的static锁时,这就变得更加棘手了,因为在不同模块中定义的函数不可能访问它。

fykwrbwg

fykwrbwg1#

这不会发生的原因(根据他的说法)是编译器必须像黑盒一样对待函数调用,并且无法优化访问或围绕它重新排序,
这是一个原因,当然。
另一个可能不会专门在GNU工具链中发生的原因是,GNU的链接时优化仅适用于使用-flto选项编译的函数,因此它们带有优化所依赖的附加信息。要防止GCC的链接时优化扰乱pthread_mutex_lock(),所需要的只是拥有一个不携带LTO信息的函数版本。(这通常是系统的责任,而不是应用程序开发人员的责任。
如果打开链接时优化(gcc-flto),您可能会发现锁突然停止工作。
有可能。如果你确实看到了,那么它就构成了你的C实现中的一个缺陷,无论你把它归因于编译器、链接器、库,还是以上所有。这并不是说你应该低估这种可能性,而是说,这是使用这些工具的人要避免和/或修复的事情,所以遇到这种问题的可能性会随着时间的推移而下降。
我想知道这怎么可能是真的,当编译器能够将memcpy这样的东西优化为单个指令时,有效地跨越模块边界而无需链接时优化。那么,它是否可以对pthread_mutex_lock()这样的东西做同样的事情,并优化它周围的访问?
你现在谈论的是优化 * 特定函数 *。例如,编译器特别了解memcpy(),基于它的规范,在某些情况下,基于它是同一个集成C实现的一部分。它可以在编译时优化(比如说)一些memcpy调用,因为它知道该函数应该做什么,并且它可以识别使用习惯,而无需实际查看库。
原则上,编译器可以对pthread_mutex_lock()做同样的事情,但这不会成为问题,因为这种特定函数优化知道(并依赖于)所涉及的函数的语义。没有理由认为pthread_mutex_lock()的这种优化会无法保留该函数的有良好文档记录的内存顺序语义。
如果这是真的,并且编译器可以查看pthread_mutex_lock()函数并看到它没有改变某个变量,因此它优化了访问,那么这会影响正确性吗?
编译器会有bug吗?是的,他们可以,而且确实如此。
当前版本的C编译器是否有这些特定的bug?我不知道。
如果是这样的话,这样一个核心功能怎么会如此容易受到无法正常工作的影响呢?
没有人会让函数对这样的失败敞开大门。在这种不正确的优化存在的机会的程度上,“人们”感兴趣并有动力修复他们的编译器,链接器和库以关闭这些漏洞。
还要理解,新的优化通常要经过长时间的仔细测试--有时是几年和多个编译器版本--然后才能安全地用于生产。如果有的话。
这是否意味着必须使用其他方法来告诉编译器不要优化对这些变量的访问,例如volatile结构?
一般来说,你应该依赖函数和语言结构来根据它们的文档来工作。特别是C语言本身定义的函数。平台的核心规范定义的函数也是如此,比如符合POSIX的系统上的POSIX。
此外,一般来说,您应该谨慎和勤奋地处理编译器选项。有些通过设计产生不符合的行为。有些是附带警告的。如果我看到一个选项附带了像GCC的-flto这样冗长的文档,我通常会认为这是一个Maven特性,除非我投入时间和精力使自己成为Maven,否则我不应该乱用。

xxhby3vn

xxhby3vn2#

为了给予你的教授休息一下,他们说的在过去的70多年里都是正确的(C是50年)。它还会是真的吗?坚韧回答。
C语言不再是一种轻量级的系统编程语言。多亏了一系列克伦族委员会的不懈努力;它现在包括不相关的库的精确规范,以及比通用系统编程语言更喜欢微优化编译器的行为。如果一个人是不友善的,他们正在通过破坏“C”来消除他们首选语言的失败(Pascal,Modula-2,欧几里得,即将生 rust );但这是个次要问题
编译器可以识别memcpy,因为它是由stdc定义的。Pthread_mutex_mumble不是由stdc定义的,所以它不能。(他的成长)。
一般来说,编译器不能超越它的编译单元(=源文件)来获得关于正在发生什么的提示。1991年,当Plan 9引入“链接优化编译器”时,情况发生了相当大的变化。当然,Plan/9的人只是对更好的技术感兴趣,而不是赢得微基准测试。
快进22年,它都是基准婴儿;编译器最近赶上了链接优化;所以要知道,仅仅隐藏在外部函数调用后面是不够的。
幸运的是,对于大多数实现,pthread互斥锁看起来像这样:

id = get_my_thread_id();
if  (cswap(mutex, 0, id) != 0) {
    _syscall_mutex_wait(mutex);
}
return 0;

这对一个链接时间优化者来说是无法理解的,所以你的教授的最初陈述成立。
作为final;因为pthread_mutex_lock至少强制使用获取-释放语义;并且在系统调用的情况下可能是完全丢弃;它保证在其“锁定”期间进行的各种更新在另一个锁定器可以获取它之前实现全局可见性。这是非常重要的。

hfwmuf9z

hfwmuf9z3#

是的,在很久以前,多线程必须依赖于像不透明函数边界这样的东西来充当临时的memory barrier。这是真的,LTO将打破这一点。幸运的是,这是在LTO广泛实施之前。
在现代,编译器支持显式内存屏障。特别是对于GCC,有几种可能的方法:

  • 在最基本的层次上,你可以(ab)使用gcc的扩展asm:asm("" : : : "memory");将强制编译器假设内存中的任何对象都可以被读取或写入,这样编译器就不能围绕它重新排序加载和存储指令。这可以与内联asm屏障指令结合使用,其防止CPU本身重新排序加载和存储的可见性。
  • 后来,gcc推出了一系列synchronization builtins。因此,您可以将__sync_synchronize()插入到代码中,这同样可以防止编译器重新排序并根据需要插入屏障指令。更有可能的情况是,您已经在使用其他读-修改-写原语之一,它们也插入了一个完整的屏障。
  • 从C11开始,该语言有一个正式的内存模型,本质上提供了关于允许什么样的重新排序的保证。这包括像atomic_thread_fence这样的标准函数,编译器必须专门处理这些函数,同样会产生一个适当类型的屏障,以阻止不必要的重新排序。

因此,使用这些方法中的至少一种,pthread_mutex_lock的实现将包括内存屏障。然后,无论是在编译时还是在链接时,都可以尽可能多地内联它,并且仍然会遵守屏障,防止任何违反其语义的重新排序。

相关问题