何时在多线程中使用volatile?


130

如果有两个线程访问全局变量,那么许多教程都说要使变量可变,以防止编译器将变量缓存在寄存器中,从而导致无法正确更新。但是,两个都访问共享变量的线程需要通过互斥锁进行保护,不是吗?但是在那种情况下,在线程锁定和释放互斥锁之间,代码处于关键部分,其中只有一个线程可以访问该变量,在这种情况下,变量不需要是可变的?

因此,多线程程序中volatile的用途/目的是什么?


3
在某些情况下,您不需要/不需要互斥保护。
Stefan Mai,2010年

4
有时候有比赛条件是好的,有时则不是。您如何使用此变量?
大卫·赫弗南

3
@David:请举一个什么例子,当比赛“很好”的时候?
John Dibling 2010年

6
@John来了。假设您有一个工作线程正在处理许多任务。每当工作线程完成任务时,它都会增加一个计数器。主线程会定期读取此计数器,并用进度新闻向用户更新。只要计数器正确对齐以避免撕裂,就不需要同步访问。尽管有比赛,但它是良性的。
大卫·赫弗南

5
@John运行此代码的硬件保证对齐的变量不会遭受撕裂。如果工作人员在阅读器读取时将n更新为n + 1,则阅读器不在乎他们是n还是n + 1。由于仅用于进度报告,因此不会做出重要决定。
David Heffernan's

Answers:


167

简短的回答volatile对于平台无关的多线程应用程序编程,(几乎)没有用。它不提供任何同步,不创建内存隔离,也不能确保操作的执行顺序。它不会使操作原子化。它不会使您的代码神奇地线程安全。 volatile可能是所有C ++中最容易被误解的工具。有关更多信息,请参见此volatile

另一方面,volatile确实有一些用途可能不太明显。它的使用方式const与帮助编译器向您显示以非保护方式访问某些共享资源时可能出错的方式相同。Alexandrescu在本文中讨论了这种用法。但是,这基本上是在使用C ++类型系统,这种方式通常被视为一种发明,并且会引起未定义的行为。

volatile是专门用于与内存映射的硬件,信号处理程序和setjmp机器代码指令进行接口连接时使用的。这使得它volatile直接适用于系统级编程,而不是普通的应用程序级编程。

2003 C ++标准没有说volatile对变量应用任何类型的获取或发布语义。实际上,该标准在多线程的所有问题上都是完全沉默的。但是,特定的平台确实将Acquire和Release语义应用于volatile变量。

[C ++ 11的更新]

现在,C ++ 11标准确实在内存模型和内存中直接确认了多线程,并且它提供了以平台无关的方式处理它的库工具。但是,的语义volatile仍然没有改变。 volatile仍然不是同步机制。Bjarne Stroustrup在TCPPPL4E中说了很多:

volatile除非在直接处理硬件的低级代码中使用,否则请勿使用。

不要以为volatile在内存模型中有特殊含义。它不是。与某些后来的语言一样,它不是一种同步机制。为了得到同步,使用atomic,一个 mutex,或condition_variable

[/结束更新]

以上所有内容均适用2003标准(以及现在的2011标准)定义的C ++语言本身。但是,某些特定平台的确会增加其他功能或限制volatile。例如,至少在MSVC 2010中,“获取”和“发布”语义确实适用于对volatile变量的某些操作。 从MSDN

优化时,编译器必须保持对易失性对象的引用以及对其他全局对象的引用之间的顺序。特别是,

写入易失性对象(易失性写入)具有Release语义;对全局或静态对象的引用发生在指令序列中对易失性对象的写操作之前,将在已编译二进制文件中的易失性写操作之前进行。

易失性对象的读取(易失性读取)具有Acquire语义;在指令序列中读取易失性存储器之后对全局或静态对象的引用将在编译后的二进制文件中的易失性读取之后发生。

但是,您可能会注意到以下事实:如果您遵循上面的链接,则注释中会涉及到在这种情况下是否实际应用获取/释放语义的争论。


19
由于答案和第一条评论的居高临下,我的一部分希望对此表示反对。“易失性是无用的”类似于“手动内存分配是无用的”。如果没有volatile它就可以编写多线程程序,是因为您站在曾经volatile实现线程库的人们的肩膀上。
本杰克逊2010年

19
@Ben只是因为一些挑战你的信仰并不能使它居高临下
大卫赫弗南

38
@Ben:不,继续阅读C ++的volatile实际功能。@约翰所说的是正确的,故事的结尾。就此而言,它与应用程序代码与库代码无关,或者与“普通”与“神似的无所不能的程序员”无关。volatile对于线程之间的同步是不必要且无用的。线程库不能用volatile; 来实现;无论如何,它都必须依赖于特定于平台的细节,而当您依靠这些细节时,就不再需要volatile
jalf

6
@jalf:“ volatile对于线程之间的同步是不必要的且无用的”(这就是您所说的)与“ volatile对于多线程编程是无用的”(这就是约翰在回答中所说的)不同。您是100%正确的,但我不同意John的观点(部分)

4
@GMan:有用的一切仅在某些要求或条件下才有用。在严格的条件下,Volatile对多线程编程很有用(在某些情况下,它甚至比替代方法更好(就某种定义而言更好))。您说“忽略那个……”,但是当volatile对多线程有用时,它不会忽略任何东西。你编造了我从未要求过的东西。是的,volatile的用途是有限的,但它确实存在-但我们都可以同意它对同步没有用。

31

(编者注:在C ++ 11 volatile中不是适合此工作的工具,并且仍然具有数据争用UB。std::atomic<bool>std::memory_order_relaxed加载/存储一起使用可在没有UB的情况下执行此操作。在实际实现中,它将编译为与.asm相同的asm volatile答案与更多的细节,也解决了误解在评论认为弱有序存储可能是该用例的一个问题:所有现实世界的CPU具有连贯共享内存中,因此volatile将努力为这对真正的C ++实现,但还是不要。不做。

评论中的某些讨论似乎是在谈论其他用例,在这些用例中,您需要比松弛原子更强大的功能。该答案已经指出,volatile您无法订购。)


由于以下原因,挥发性有时会有用:此代码:

/* global */ bool flag = false;

while (!flag) {}

由gcc优化以:

if (!flag) { while (true) {} }

如果该标志是由另一个线程写入的,那显然是不正确的。请注意,如果没有这种优化,则同步机制可能会起作用(取决于其他代码,可能需要一些内存障碍)-在1个生产者-1个使用者场景中不需要互斥体。

否则,volatile关键字太怪异而无法使用-它不提供任何易失性和非易失性访问的内存顺序保证,并且不提供任何原子操作-即,除了禁用的寄存器缓存外,使用volatile关键字的编译器没有任何帮助。


4
如果我还记得,C ++ 0x原子意味着要正确地做很多人认为(错误地)由volatile完成的事情。
大卫·赫弗南

13
volatile不会阻止对内存访问进行重新排序。volatile访问不会相对于彼此重新排序,但是它们不能保证对非volatile对象的重新排序,因此,它们基本上也无用。
jalf

13
@本:我想你已经颠倒了。“易失性是无用的”人群依赖于一个简单的事实,即易失性不能防止重新排序,这意味着它对于同步完全是无用的。其他方法可能同样无用(如您所述,链接时代码优化可能允许编译器查看您认为编译器将其视为黑匣子的代码),但这不能解决的不足volatile
jalf

15
@jalf:参见Arch Robinson的文章(在此页面的其他位置链接),第十条评论(“ Spud”)。基本上,重新排序不会更改代码的逻辑。发布的代码使用该标志来取消任务(而不是表示任务已完成),因此,在代码之前还是之后取消任务都没有关系(例如:while (work_left) { do_piece_of_work(); if (cancel) break;}如果在循环中对取消进行了重新排序,我有一段类似的代码:如果主线程想要终止,它将设置其他线程的标志,但是不会...

15
...如果其他线程在终止之前进行了几次工作循环迭代,则前提是只要设置了标记后不久就发生了。当然,这是我能想到的唯一用途,并且它的利基性很强(并且可能无法在写volatile变量并不能使其他线程看到更改的平台上工作,尽管至少在x86和x86-64上这样)作品)。我当然不建议任何人没有很好的理由就这样做,我只是说一个笼统的声明,如“ volatile在多线程代码中永远没有用”并不是100%正确的。

15

在C ++ 11中,通常从不volatile用于线程,仅用于MMIO

但是TL:DR,mo_relaxed在具有相干缓存(即所有内容)的硬件上,确实可以像原子一样“工作” ;停止编译器将var保存在寄存器中就足够了。 atomic不需要内存障碍来创建原子性或线程间可见性,只需使当前线程在操作之前/之后等待,以创建该线程对不同变量的访问之间的排序。 mo_relaxed永远不需要任何障碍,只需加载,存储或RMW。

对于滚你自己的原子能公司volatile(和内联汇编的障碍)在坏日子C ++ 11之前std::atomicvolatile是得到一些东西的工作唯一的好办法。但这取决于有关实现方式工作的许多假设,并且从来没有任何标准可以保证。

例如,Linux内核仍使用带有的自己的原子原子volatile,但仅支持一些特定的C实现(GNU C,clang以及ICC)。部分原因是由于GNU C扩展以及内联asm语法和语义,还因为它取决于有关编译器如何工作的一些假设。

对于新项目而言,几乎总是错误的选择。您可以使用std::atomic(与std::memory_order_relaxed)来使编译器发出与相同的高效机器代码volatilestd::atomic带有mo_relaxed过时volatile的线程。(除非可以解决atomic<double>某些编译器上的未优化错误)。

std::atomic在主流编译器(例如gcc和clang)上的内部实现不仅仅volatile内部使用;而是在内部使用。编译器直接公开原子加载,存储和RMW内置函数。(例如,对“普通”对象进行操作的GNU C __atomic内置函数。)


挥发性在实践中可用(但不要这样做)

就是说,由于CPU的工作方式(一致的缓存)以及关于应该如何工作的共同假设,因此volatile实际上可用于exit_now所有(?)现有C ++实现上的标志之类的事情volatile。但是没有太多其他内容,因此建议使用。 这个答案的目的是解释现有的CPU和C ++实现实际上是如何工作的。如果您对此不关心,则只需知道std::atomic使用mo_relaxed过时volatile线程即可。

(ISO C ++标准对此含糊不清,只是说volatile访问应严格按照C ++抽象机的规则进行评估,而不是进行优化。鉴于实际的实现使用该机器的内存地址空间来建模C ++地址空间,这意味着volatile读取和分配必须进行编译以加载/存储指令以访问内存中的对象表示形式。)


另一个答案指出,exit_now标志是线程间通信的一种简单情况,不需要任何同步:它不发布数组内容已准备就绪或类似的东西。只是由于另一个线程中的未优化负载而立即引起注意的商店。

    // global
    bool exit_now = false;

    // in one thread
    while (!exit_now) { do_stuff; }

    // in another thread, or signal handler in this thread
    exit_now = true;

如果没有volatile或atomic,则as-if规则和无数据争用UB的假定使编译器可以将其优化为只进入标志一次的asm,然后进入(或不进入)无限循环。这正是真实编译器在现实生活中发生的情况。(并且通常会优化大部分内容,do_stuff因为循环永远不会退出,因此,如果我们进入循环,则可能无法获得使用该结果的任何后续代码)。

 // Optimizing compilers transform the loop into asm like this
    if (!exit_now) {        // check once before entering loop
        while(1) do_stuff;  // infinite loop
    }

多线程程序停留在优化模式下,但通常在-O0运行,这是一个示例(说明了GCC的asm输出),它说明了x86-64上的GCC会如何发生。此外,MCU编程-C ++ O2优化在电子设备上循环中断.SE显示了另一个示例。

我们通常希望对CSE和提升机进行循环之外的积极优化,包括全局变量。

在C ++ 11之前,这volatile bool exit_now使这项工作按预期进行的一种方法(在常规C ++实现上)。但是在C ++ 11中,数据争用UB仍然适用,volatile因此,即使假设具有硬件一致性缓存,ISO标准也不能保证它在任何地方都可以工作。

请注意,对于较宽的类型,volatile不能保证不会撕裂。我在这里忽略了这种区别,bool因为它在正常实现中不是问题。但这也是为什么volatile仍然要接受数据争用UB而不是等同于宽松原子的一部分。

请注意,“按预期”并不意味着正在执行的线程exit_now会等待另一个线程实际退出。甚至等待该易失性exit_now=true存储在全局范围内可见,然后再继续执行此线程中的后续操作。(atomic<bool>默认情况下,mo_seq_cst它将至少等到以后再加载seq_cst。在许多ISA上,存储后您只会遇到障碍。)

C ++ 11提供了一种非UB方式来进行编译

“保持运行”或“立即退出”标志应std::atomic<bool> flagmo_relaxed

使用

  • flag.store(true, std::memory_order_relaxed)
  • while( !flag.load(std::memory_order_relaxed) ) { ... }

会为您提供与完全相同的asm(没有昂贵的障碍说明)volatile flag

除了不撕裂之外,atomic还使您能够在没有UB的情况下将其存储在一个线程中并在另一个线程中进行加载,因此编译器无法将负载提升到循环之外。(没有数据争用UB的假设可以使我们对非原子非易失性对象进行积极的优化。)的此功能与纯负载和纯存储的功能atomic<T>几乎相同volatile

atomic<T>还会将+=诸如此类的事情变成原子RMW操作(比将原子加载到临时存储器,操作和一个单独的原子存储器要昂贵得多。如果您不希望使用原子RMW,请使用本地临时存储器编写代码)。

使用seq_cst您从中获得的默认订购while(!flag),它还会添加订购保证wrt。非原子访问,以及其他原子访问。

(理论上,ISO C ++标准不排除原子的编译时优化。但是实际上,编译器并不这样做,因为没有办法控制何时不可行。在某些情况下,甚至volatile atomic<T>可能无法超过原子能如果编译器做了优化的优化足够的控制,所以现在编译器不会见你为什么不编译器合并冗余的std ::原子写? 需要注意的是WG21 / p0062建议不要使用volatile atomic在当前代码防范的优化原子。)


volatile 确实可以在真正的CPU上工作(但是仍然不使用它)

即使使用弱排序的内存模型(非x86)。但实际上并不使用它,使用atomic<T>mo_relaxed替代!本节的重点是要解决有关实际CPU工作方式的误解,而不是要证明其合理性volatile。如果您正在编写无锁代码,那么您可能会关心性能。通常,了解缓存和线程间通信的成本对于获得良好的性能很重要。

实际的CPU具有一致的缓存/共享内存:一个核心的存储全局可见后,其他任何核心都无法加载陈旧的值。 (另请参见神话程序员相信CPU缓存,其中讨论了有关Java volatile的某些知识,等效于atomic<T>具有seq_cst内存顺序的C ++ 。)

当我说load时,我的意思是访问内存的asm指令。这就是一个volatile访问确保,并且是一样的东西左值到右值非原子/非易失性C ++变量的转换。(例如local_tmp = flagwhile(!flag))。

您唯一需要克服的就是编译时优化,它在第一次检查后根本不会重新加载。每次迭代中的任何load + check都足够,无需任何排序。如果该线程与主线程之间没有同步,那么谈论确切的存储时间或加载顺序就没有意义。循环中的其他操作。仅当此线程可见时才重要。当您看到设置了exit_now标志时,您将退出。在典型的x86 Xeon上,不同物理内核之间的内核间延迟可能约为40ns


理论上:C ++线程在硬件上没有一致性缓存

我看不到有什么办法可以实现远程高效,仅使用纯ISO C ++而不要求程序员在源代码中进行显式刷新。

从理论上讲,您可以在不是这样的机器上实现C ++实现,这需要编译器生成的显式刷新来使事物对其他内核上的其他线程可见。(或者使读取不使用过时的副本)。C ++标准并没有做到这一点,但是C ++的内存模型是围绕在相干共享内存计算机上高效而设计的。例如,C ++标准甚至谈到“读-读一致性”,“写-读一致性”等。该标准中的一个注释甚至指出了与硬件的连接:

http://eel.is/c++draft/intro.races#19

[注意:前面的四个一致性要求有效地禁止了编译器对单个对象的原子操作重新排序,即使两个操作都是宽松的负载也是如此。这有效地使大多数硬件提供的缓存一致性可用于C ++原子操作。—尾注]

没有一种机制可以让release存储仅刷新自身并选择一些地址范围:它必须同步所有内容,因为如果它们的获取负载看到了这个发布存储,它将不知道其他线程可能想要读取什么内容(形成一个释放序列可以在线程之间建立事前发生的关系,从而保证写入线程完成的较早的非原子操作现在可以安全读取。除非在释放存储之后对其进行了进一步的写操作...)否则编译器将拥有是真正聪明的证明,只有少数的高速缓存行需要冲洗。

相关:我对mov + mfence在NUMA上安全吗?详细介绍了没有连贯的共享内存的x86系统的不存在。还相关:在ARM上进行加载和存储重新排序,以获取有关到相同位置的加载/存储的更多信息。

这里我想用非相干共享存储集群,但他们不是单一系统映像机器。每个一致性域都运行一个单独的内核,因此您不能跨该内核运行单个C ++程序的线程。而是运行程序的单独实例(每个实例都有其自己的地址空间:一个实例中的指针在另一个实例中无效)。

为了使它们通过显式刷新相互通信,通常需要使用MPI或其他消息传递API来使程序指定需要刷新的地址范围。


真正的硬件不会std::thread跨缓存一致性边界运行:

存在一些非对称ARM芯片,具有共享的物理地址空间,但不具有内部共享的缓存域。所以不连贯。(例如,注释线程为A8核和TI Sitara AM335x之类的Cortex-M3)。

但是不同的内核将在这些内核上运行,而不是一个可以跨两个内核运行线程的系统映像。我不知道有任何C ++实现std::thread在没有一致性缓存的情况下跨CPU内核运行线程。

专门针对ARM,GCC和clang生成代码,并假设所有线程都在同一内部可共享域中运行。实际上,ARMv7 ISA手册说

编写此体系结构(ARMv7)时,希望所有使用相同操作系统或虚拟机管理程序的处理器都位于同一内部共享域中

因此,对于不同内核下不同进程之间的通信,单独域之间的非一致性共享内存仅是明确使用共享内存区域的系统特定用途。

另请参阅有关在该编译器中使用(内部可共享屏障)与(系统)内存屏障进行代码生成的CoreCLR讨论。dmb ishdmb sy

我断言,其他任何ISA的C ++实现都不会std::thread在具有非一致性缓存的内核之间运行。 我没有证据表明不存在这样的实现,但似乎极不可能。除非您以这种方式工作的特定硬件为目标,否则您对性能的思考应假定所有线程之间都具有类似于MESI的缓存一致性。(不过,最好atomic<T>以保证正确性的方式使用!)


一致的缓存使其变得简单

但是,在具有一致缓存的多核系统上,实现发布存储仅意味着为该线程的存储排序到缓存中的提交,而不进行任何显式刷新。(https://preshing.com/20120913/acquire-and-release-semantics/https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/)。(获取负载意味着命令访问另一个核心中的缓存)。

内存屏障指令仅阻塞当前线程的加载和/或存储,直到存储缓冲区耗尽为止。总是尽可能快地自行发生。内存屏障是否可确保高速缓存一致性已完成?解决了这个误解)。因此,如果您不需要订购,则只需在其他线程中快速查看即可mo_relaxed。(,也是如此volatile,但不要那样做。)

另请参见C / C ++ 11到处理器的映射

有趣的事实:在x86上,每个asm存储都是一个发布存储,因为x86内存模型基本上是seq-cst加一个存储缓冲区(带有存储转发)。


半相关的re:存储缓冲区,全局可见性和一致性:C ++ 11几乎不能保证。大多数真正的ISA(PowerPC除外)的确保证所有线程都能通过其他两个线程就两个存储出现的顺序达成一致。(在正式的计算机体系结构内存模型术语中,它们是“多副本原子”)。

另一个误解是需要的存储栅栏汇编指令刷新存储缓冲区其他处理器看到我们的店在所有。实际上,存储缓冲区总是试图尽快耗尽自身(提交到L1d缓存),否则它将填满并暂停执行。完整的屏障/栅栏所做的是使当前线程停止运行,直到耗尽存储缓冲区为止,因此,我们的较晚负载将在较早的存储之后以全局顺序显示。

(x86的有序asm内存模型意味着volatilex86可能最终使您更接近mo_acq_rel,除了使用非原子变量的编译时重排序仍然可能发生。但是大多数非x86的有弱有序的内存模型,volatile并且relaxed大约弱点mo_relaxed。)


评论不作进一步讨论;此对话已转移至聊天
塞缪尔·

2
伟大的写作。这正是我要寻找的(给出所有事实),而不是一条笼统的声明,即“对于单个全局共享布尔标志使用原子而不是volatile”。
伯尼

2
@bernie:我对重复声明感到沮丧,因为不使用它atomic可能导致不同的线程对cache中的同一变量具有不同的值,因此我写了这篇文章。/ facepalm。在高速缓存中,否,在CPU 寄存器中,是(带有非原子变量);CPU使用一致的缓存。我希望关于SO的其他问题没有引起人们对atomicCPU工作原理的误解的充分解释。(因为出于性能原因,这是一件有用的事情,并且还有助于解释为什么按原样编写ISO C ++原子规则。)
Peter Cordes

-1
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;

bool checkValue = false;

int main()
{
    std::thread writer([&](){
            sleep(2);
            checkValue = true;
            std::cout << "Value of checkValue set to " << checkValue << std::endl;
        });

    std::thread reader([&](){
            while(!checkValue);
        });

    writer.join();
    reader.join();
}

曾经也认为volatile是无用的访问者曾与我争辩说,Optimization不会引起任何问题,并且指的是具有单独的缓存行以及所有其他内容的不同内核(并不能真正理解他所指的是什么)。但是,这段代码在g ++(g ++ -O3 thread.cpp -lpthread)上用-O3编译时,显示了未定义的行为。基本上,如果在while检查之前设置该值,则它可以正常工作;否则,它会进入循环而不会费心去获取该值(该值实际上已由另一个线程更改)。基本上,我相信checkValue的值只会被提取一次到寄存器中,而不会在最高级别的优化下再次被检查。如果在获取之前将其设置为true,则它将正常工作,否则将进入循环。如果有错,请纠正我。


4
这和什么有关系volatile?是的,此代码是UB,但也包含UB volatile
David Schwartz '18

-2

您需要易失性并且可能需要锁定。

volatile告诉优化器,该值可以异步更改,因此

volatile bool flag = false;

while (!flag) {
    /*do something*/
}

每次循环都会读取标志。

如果关闭优化或使每个变量都可变,则程序的行为将相同,但速度较慢。volatile只是意味着“我知道您可能已经读了它,并且知道它说了什么,但是如果我说了,请先阅读。

锁定是程序的一部分。因此,顺便说一句,如果要实现信号量,那么它们必须是易变的。(不要尝试,这很难,可能会需要一些汇编程序或新的原子材料,并且它已经完成了。)


1
但这不是吗?另一个响应中的相同示例正忙于等待,因此应该避免某些事情?如果这是人为的例子,那么有没有现实生活中的人为例子?
戴维·普雷斯顿

7
@克里斯:繁忙的等待有时是一个很好的解决方案。特别是,如果您只希望等待几个时钟周期,那么它所带来的开销要比重量级得多的挂起线程的方法少得多。当然,正如我在其他评论中提到的那样,诸如此类的示例是有缺陷的,因为它们假定对标志的读/写不会相对于它所保护的代码进行重新排序,并且没有给出这种保证,因此,volatile即使在这种情况下也不是很有用。但是繁忙的等待是一种偶尔有用的技术。
jalf

3
@richard是的,不是。前半部分是正确的。但这仅意味着CPU和编译器不允许相对于可变变量重新排序。如果我读取了一个易失性变量A,然后又读取了一个易失性变量B,则编译器必须发出保证(即使使用CPU重新排序)才能在B之前读取A的代码。但是,它不能保证所有非易失性变量的访问。可以根据易失性读/写对它们进行重新排序。所以,除非你让每一个变量在你的程序挥发,它不会给你你感兴趣的保证
jalf

2
@ ctrl-alt-delor:那不是volatile“不重新排序”的意思。您希望它意味着这些存储将按程序顺序在全局(对其他线程)可见。这就是atomic<T>memory_order_releaseseq_cst给你。但是,这volatile 只能保证您没有编译时重新排序:每次访问都将按程序顺序显示在asm中。对于设备驱动程序很有用。对于与当前内核/线程上的中断处理程序,调试器或信号处理程序进行交互很有用,但对于与其他内核进行交互则无效。
彼得·科德斯

1
volatile在实践中,足以keep_running像您在此处那样检查标志:真正的CPU始终具有不需要手动刷新的一致缓存。但是,没有任何理由建议volatileatomic<T>mo_relaxed; 您将获得相同的asm。
彼得·科德斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.