Hardware Memory Models 硬件内存模型

原文链接:research!rsc: Hardware Memory Models (Memory Models, Part 1)

该博客为依托本人理解翻译。

简介:一个童话的终结

很久之前,当所有人都在写单线程程序时,为了使程序运行的更快,最有效的方法之一是啥也不干地上一躺:)。在那个时代,更新迭代的硬件以及编译器的优化并不会改变程序运行的方式,仅仅是使程序运行的更快。在这个童话时期,有一种很简单的测试优化是否有效的方式:如果一个程序员说不出除了运行速度之外的其他区别,那么说明这个优化是有效的。在那个时代,有效的优化不会改变程序的运行方式。

N年前的一个悲伤的日子里,让单处理器运行的越来越快的魔法失效了(回来吧我滴摩尔:))。作为回应,硬件魔法师们找到了一个新的法术,他们使得计算机有了越来越多的处理器,并且操作系统将这种硬件上的并行以一个抽象:线程 的方式暴露给程序员。多处理器以操作系统线程的方式提供对于硬件工程师来说很友好,但却为编程语言设计者、编译器作者以及程序员造成了很大的困扰。

许多硬件以及编译器优化在单线程程序中是不可见无感知的(就像第一段阐述的那样,因此是有效的),但这些优化会产生在多线程程序中产生可见的变化。如果有效的优化不改变有效程序的行为,那么在多线程编程环境下,必须声明这些优化或现有程序无效。谁无效?我们怎么做选择?(这段有点抽象,下文有例子

这里给一个C风格的简单例子,在这个程序和我们将要考虑的所有程序中,所有变量的初始值都是0。

// Thread 1           // Thread 2
x = 1;                while(done == 0) { /* loop */ }
done = 1;             print(x);

如果线程1和线程2都在自己的专用处理器上运行,并且都能跑完,那么这个程序最后会输出0吗?

这取决于硬件以及编译器,一行行转汇编,在x86多核处理器上会输出1,但在ARM或者POWER多核处理器上会输出0。并且不管底层硬件是什么,标准编译器优化会使得程序输出0或者进入一个死循环。

“It depends”不是一个happy ending。程序员需要一个明确知道程序是否能在不同的硬件以及编译器下运行。并且硬件设计者以及编译器开发者需要明确的知道编译后的代码在硬件上的执行方式(指令的执行顺序等)。因为主要的点在于对存储在内存中的数据更改的可见性和一致性,该契约被称为内存一致性模型或仅称为内存模型

起初,内存模型提出的目的是定义硬件提供的对于程序员编写汇编代码的保障。在那个设定中,编译器是没有涉及其中的。25年前,人们开始尝试改写内存模型定义,定义 Java 或 C++ 等高级编程语言向用该语言编写代码的程序员提供的保证。在内存模型中包含编译器会使定义一个合理内存模型的工作变得更加复杂。

这里原博主给了一个链接:research!rsc: Memory Models ,里面包含了硬件内存模型以及编程语言内存模型。写这些推文的目的是在Go语言的内存模型中进行一些潜在更改来奠定基础。但要了解 Go 的现状以及未来的发展方向,首先我们必须了解其他硬件内存模型和语言内存模型目前的状况以及它们实现这一目标所采取的“不稳定”路径。

再次重申,这个博文是关于硬件的。让我们假设我们在位多处理器计算机写汇编代码。程序员需要从计算机硬件那里获得怎样的保证从而写出正确的程序?计算机科学家为了寻找这一问题的答案已经追寻40载了。

顺序一致性

Leslie Lamport (分布式の神)在1979的论文How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Programs介绍了顺序一致性的概念:

为此类计算机设计和证明多进程算法的正确性的常规方法假设满足以下条件:任何执行的结果与如果所有处理器的操作按顺序执行的结果相同,并且每个处理器的操作按照其程序指定的顺序出现在这个序列中。满足这个条件的多处理器将被称为顺序一致。

ps. 这个概念我理解就是可以说清楚某个结果是通过怎么样的线程交错处理得到的,就叫顺序一直,下文会有个例子。

今天,我们不仅讨论计算机硬件,还讨论保证顺序一致性的编程语言,此时程序的执行顺序对应于线程操作顺序执行的某种交错。顺序一致性通常被认为是某种理想的模型,对于程序员来说是最自然的。这使你可以假设程序就和显示的一样的顺序执行,并且单个线程的执行只是以某种顺序交错,而不是以其他方式重新排列。

有些人可能会质疑顺序一致性是否是“理想的模型”,但这个讨论超过了这个博文的范围。我只想指出,考虑所有可能的线程交错情况(这种判断方式)无论是今天还是1979年都一样,“设计和证明多进程算法正确性的惯用方法”。在过去的四十年中,没有什么能取代它。

在本文的前文中,我问了这个问题,以下代码能否打印0:

// Thread 1           // Thread 2
x = 1;                while(done == 0) { /* loop */ }
done = 1;             print(x);

为了使上述程序更容易分析,让我们去掉循环和打印,分析一下从共享变量可能读取到结果:
 

Litmus Test: Message Passing
Can this program see r1 = 1, r2 = 0?

// Thread 1           // Thread 2
x = 1                 r1 = y
y = 1                 r2 = x

我们假设每个例子开始运行时所有共享变量初始值均为0。由于我们正在尝试确定允许硬件执行的操作,我们假设每个线程都在其专用处理器上执行,并且没有编译器重新排列线程中发生的事情:上述列表中的指令就是处理器执行的指令。rN的名称表示一个线程本地寄存器,而不是一个共享变量,我们想知道在执行结束时线程本地寄存器的值是否有可能使我们想的那样的一个特定值。

像上文给的例子一样,这种关于执行结果的问题被称为 litmus test. 因为他有一个二元的结果--(我们预测的结果)可能或者不可能?--litmus test给我们一个清晰的方法去辨别不同的内存模型:如果一个模型允许特定的执行,而另一个模型不允许,那么这两个模型显然是不同的。

如果上文提到的litmus test是顺序一致的,那么只有六种可能的交错方式:

因为没有一种交错方式在程序结束时r1 = 1, r2 = 0, 所以我们上文提到的那种结果是不存在的。

一个对于顺序一致性的比较好的理解方式是想象所有处理器直接连接到相同的共享内存,可以一次为一个线程提供读取或写入请求。没有缓存参与,所以每次处理器需要从内存读取或写入数据时,请求都会发送到共享内存。这个共享内存一次仅接受一个线程的一个请求,这种行为施加了一个access到这个共享内存的一个先后顺序:这种可以确定的先后顺序称为顺序一致性。

(原po注:本文关于内存模型硬件相关的3个图均来自Maranget et al., “A Tutorial Introduction to the ARM and POWER Relaxed Memory Models.”)

这个图是顺序一致性的一个模型,不是实现顺序一致性的唯一方法。事实上,也可以用多个共享内存模块和缓存来构建一个顺序一致的机器来帮助预测内存获取的结果(和上文一样的操作),但是顺序一致意味着需要表现得与上文提到的那几个结果一样。如果我们只是试图理解顺序一致性是什么意思,我们只考虑上文那个模型即可。

对于程序员,不幸的是,放弃严格的顺序一致性会使硬件能更快的运行程序,所以所有现代硬件在各种方式上都偏离了顺序一致性。确定特定硬件偏离的具体方式是相当困难的。这篇博文给了两个典型的例子:x86,以及ARM和POWER处理器家族的内存模型。

x86架构中对内存访问的顺序(总存储顺序Total Store Order)

现代x86简化硬件模型如下图所示:

所有处理器仍然连接到单个共享内存,但每个处理器都将写入排队到其本地写入队列。处理器在写入数据到共享内存的过程中继续执行新的指令。一个本地处理器的读请求会先去本地写队列中找(而不是直接问主存要),但本地处理器看不到其他处理器的写队列。影响是处理器在其他处理器之前看到自己的写入。但是,这一点非常重要——所有处理器都同意写入(存储)到达共享内存的(总体)顺序,从而赋予模型其名称:总存储顺序,或TSO(即大家都同意一个全局序)。当一个写操作到达共享内存时,任何处理器上的未来读取都会看到并使用该值(直到它被后续的写操作覆盖,或者可能被另一个处理器的缓冲写操作覆盖)。

处理器的写队列是一个标准的先进先出队列:内存写入是按照处理器执行的顺序到共享内存的。由于写队列保证了写入顺序,并且其他处理器可以立即看到对共享内存的写入。上文我们聊的那个litmus test 让r1 = 1,r2 = 0依然不可能实现。

Litmus Test: Message Passing

程序可以运行出: r1 = 1, r2 = 0?

// Thread 1           // Thread 2
x = 1                 r1 = y
y = 1                 r2 = x
在顺序一致性硬件上: no.
在x86 (或者其他total store order): no.

写队列保证线程1在y之前将x写入内存,并且关于内存写入顺序的系统范围协议(总存储顺序)保证线程2在得知y的新值之前得知x的新值。因此,r1 = y 不可能看到新的 y 而 r2 = x 也不看到新的 x。存储顺序在这里至关重要:线程1在写入 y 之前写入 x,因此线程2在写入 x 之前不能看到对 y 的写入。

顺序一致性以及TSO模型在上文那个例子中表现一致,但他们在某些litmus test中表现不一致,比如下面这个进场用来区分顺序一致性模型以及TSO模型的例子:

Litmus Test: Write Queue (also called Store Buffer)
程序可以运行得到 r1 = 0, r2 = 0?

// Thread 1           // Thread 2
x = 1                 y = 1
r1 = y                r2 = x
在顺序一致性硬件上: no.
在x86 (或者其他total store order): yes!

在任意顺序一致性处理中,要么x = 1或者y = 1必须先发生,然后我们的读操作发现了x与y的结果变化,所以r1 = 0, r2 = 0这个结果是不可能的。但是在TSO系统中,线程1和线程2的写操作执行完毕,并且打入了本地写队列中,此时已经进行到从内存读取操作,但对于x, y值的更改还未实际写入内存中,所以有可能两个值都读到0。

上面这个例子看起来有点做作,但确实有一些知名同步算法需要用到两个同步变量,比如Dekker's algorithm 或 Peterson's algorithm以及一些临时方案。这些算法会在一个线程没有正确读到另一个线程的写时失效。

为了修复依赖于强内存序的算法,非顺序一致的硬件提供了称为内存屏障(或栅栏)的显式指令,可用于控制内存序。我们可以添加一个内存屏障来确保每个线程在开始读取之前刷新其先前的写入到内存。

// Thread 1           // Thread 2
x = 1                 y = 1
barrier               barrier
r1 = y                r2 = x

通过添加内存障碍,r1 = 0,r2 = 0 这一结果再次变得不可能,Dekker's 或 Peterson's 算法将正确地运行。有许多种类的内存障碍,他们的实现以及使用细节因系统而异,具体细节超出了本博文的范围。关键有内存障碍存在,为程序员或语言实现者提供了一种在程序的关键时刻强制顺序一致行为的方法。

最后一个例子,来说明为什么这个模型被称为TSO。在这个模型中,有本地写队列,但读取路径上没有缓存。一旦写入到主内存,所有处理器不仅同意该值存在,而且对于它相对于其他处理器的写入时间也达成一致。思考一下如下场景:

Litmus Test: Independent Reads of Independent Writes (IRIW)
Can this program see r1 = 1, r2 = 0, r3 = 1, r4 = 0?
(Can Threads 3 and 4 see x and y change in different orders?)

// Thread 1    // Thread 2    // Thread 3    // Thread 4
x = 1          y = 1          r1 = x         r3 = y
                              r2 = y         r4 = x
On sequentially consistent hardware: no.
On x86 (or other TSO): no.

如果线程3在y之前看到x的变化,那么线程4能在x之前看到y的变化吗?对于x86和其他TSO机器来说,答案是否定的:主内存中的所有存储(写入)都有一个总的顺序,并且所有处理器都同意这个顺序,但要注意的是,每个处理器在它们到达主内存之前就知道自己的写入(可以从本地写入队列读取)。

x86-TSO进化之路

x86-TSO模型看起来逻辑清晰,但走过的路上充满了障碍和荆棘。在90年代,第一批x86多处理器的手册几乎没有提到硬件提供的内存模型。

作为问题的一个例子,Plan 9是第一个真正的多处理器操作系统之一(没有全局内核锁:查了下,这玩意是早期的锁实现,很粗粒度的锁,可以保证操作系统只有一个线程访问),可以在x86上运行。在1997年,当开发人员将其移植到多处理器Pentium Pro时,他们遇到了意想不到的行为,归结为写队列litmus test。一个微妙的同步代码假设r1 = 0,r2 = 0是不可能的,但却发生了。更糟糕的是,英特尔手册对内存模型细节含糊其辞。

对于下面列出的邮件中建议“最好保守使用锁,而不是相信硬件设计者会按我们的预期做”,Plan 9的一位开发人员很好地解释了问题:

我完全同意。在多处理器中,我们将会遇到更加灵活的顺序。问题是,硬件设计师认为什么是保守的呢?在锁定部分的开头和结尾都强制插入一个互锁对我来说似乎相当保守,但显然我想象力不够丰富。专业手册详细描述了缓存及其保持一致性的内容,但似乎并不关心执行或读取顺序的任何详细信息。事实上,我们无法知道我们是否足够保守。

在讨论中,英特尔的一位架构师非正式地解释了内存模型,指出理论上,即使是多处理器的486和Pentium 系统也可能会产生r1 = 0,r2 = 0的结果,并且Pentium Pro有更大的流水线和写入队列,更容易产生两个0的结果。

英特尔的架构师写道:

不严格地说,这意味着来自系统中任何一个处理器的事件顺序,如其他处理器所观察到的,总是相同的。然而,不同的观察者可以对来自两个或多个处理器的事件交错顺序持不同意见。

未来的英特尔处理器将实施相同的内存顺序模型。

“不同的观察者可以就两个或多个处理器的事件交错而产生分歧”的说法是在说IRIW litmus test的答案可以在x86上回答“是”,即使在前一节中我们看到x86的答案是“否”。这怎么可能呢?

答案似乎是英特尔处理器从未真正对这个litmus test回答“是”,但当时英特尔的架构师们不愿对未来的处理做出任何保证。架构手册中几乎没有任何担保的段落,这让编程变得非常困难。

Plan 9的讨论不是个例,linux 内核开发者花了上百封邮件来讨论与上文相同的困惑,原因就是Intel的处理器未提供保证。

在随后的十年里,随着越来越多的人遇到这些困难,英特尔的一群架构师开始着手撰写关于处理器行为的有用保证,既包括当前的处理器,也包括未来的处理器。对于当前以及未来的处理器,第一块保证在文章“Intel 64 Architecture Memory Ordering White Paper”,在2007年8月发行,旨在“为软件编写人员提供对不同内存访问指令序列可能产生的结果的清晰理解。”AMD也在同一年稍晚发布了相同的相关描述在AMD64 Architecture Programmer's Manual revision 3.14。这些描述是基于一个叫做“总锁定顺序total lock order+因果一致性causal consistency(因果一致性是指在分布式系统中,如果一个进程执行了一个写操作,那么这个写操作的结果将会被后续的读操作所观察到。这意味着系统中的操作执行顺序应该与它们发生的顺序一致,以确保数据的一致性和可靠性。)”(TLO+CC)的模型,有意比TSO要弱。在公开讲话中,英特尔的架构师们表示,TLO+CC“强(指内存序)到必要的程度,但不会过度强”。特别是,该模型保留了x86处理器对IRIW litmus test的“是”答案的权利。很遗憾,内存屏障的定义不够强大,无法重新建立顺序一致的内存语义,即使在每条指令后加上屏障也不行。更糟糕的是,研究人员观察到实际的英特尔x86硬件违反了TLO+CC模型。比如:

Litmus Test: n6 (Paul Loewenstein)
Can this program end with r1 = 1, r2 = 0, x = 1?

// Thread 1    // Thread 2
x = 1          y = 1
r1 = x         x = 2
r2 = y
On sequentially consistent hardware: no.
On x86 TLO+CC model (2007): no.
On actual x86 hardware: yes!
On x86 TSO model: yes! (Example from x86-TSO paper.)

ps:上面这个例子我第一次看有点懵,小巷提醒了我,end with是两个线程都要跑完。

2008年晚些时候对英特尔和AMD规格进行了修订,确保了IRIW litmus test的情况的“否定”,加强了内存屏障,但仍允许出现看似在任何合理硬件上都不可能出现的意外行为。比如:

Litmus Test: n5
Can this program end with r1 = 2, r2 = 1?

// Thread 1    // Thread 2
x = 1          x = 2
r1 = x         r2 = x
On sequentially consistent hardware: no.
On x86 specification (2008): yes!
On actual x86 hardware: no.
On x86 TSO model: no. (Example from x86-TSO paper.)

为了解决这些问题,Owens等人提出了基于早期SPARCv8 TSO模型的x86-TSO模型。当时他们声称:“据我们所知,x86-TSO是可靠的,足够强大以在其上进行编程,并且与供应商的意图大体一致。”几个月后,英特尔和AMD发布了新手册,广泛采用了这一模型。
看起来所有英特尔处理器从一开始就实现了x86-TSO,尽管花了十年时间英特尔才决定承诺。回顾起来,很明显英特尔和AMD的架构师们在努力考虑如何编写一个留下未来处理器优化空间,同时为编译器编写者和汇编语言程序员提供有用保证的内存模型。"尽可能强,但不要过度强"是一个很难平衡的行为。

AMD/POWER 宽松的内存模型

现在让我们来关注一个更加宽松的内存模型,即在ARM和POWER处理器上使用的模型。在实现层面上,这两个系统在许多方面都是不同的,但保证的内存一致性模型结果是大致相似的,比x86-TSO甚至x86-TLO+CC要弱得多。

ARM和POWER系统的内存模型(抽象一点)是,每个处理器都从自己完整的内存副本中读取和写入数据,并且每次写入都会独立地传播到其他处理器,允许重新排序。如下图所示:

图中可以看出,在这个概念模型中是没有全局序概念的。每个处理器也可以推迟读取,直到它需要结果:读取可以延迟到稍后的写入之后。在这个宽松内存模型中,每个上文出现过的litmus test的答案都可以是“是的,这种结果有可能发生”。

对于原始消息传递litmus test, 单个处理器对写入的重新排序意味着线程1的写入可能不会按相同顺序被其他线程观察到:

Litmus Test: Message Passing
Can this program see r1 = 1, r2 = 0?

// Thread 1           // Thread 2
x = 1                 r1 = y
y = 1                 r2 = x
On sequentially consistent hardware: no.
On x86 (or other TSO): no.
On ARM/POWER: yes!

在ARM/POWER模型中,我们可以认为线程1和线程2分别拥有自己独立的内存副本,写入操作可以以任何顺序在这些内存之间传播。如果线程1的内存在发送x的更新操作之前发送了y的更新操作给线程2,并且线程2在收到这两次更新操作之间执行,那么它确实会看到结果r1 = 1,r2 = 0。

这个结果表明ARM/POWER内存模型比TSO要弱:它对硬件的要求更少。ARM/POWER模型仍然允许TSO所允许的重新排序类型:

Litmus Test: Store Buffering
Can this program see r1 = 0, r2 = 0?

// Thread 1           // Thread 2
x = 1                 y = 1
r1 = y                r2 = x
On sequentially consistent hardware: no.
On x86 (or other TSO): yes!
On ARM/POWER: yes!

在ARM/POWER上,对x和y的写操作可能已经在本地内存中完成,但当在对端的线程上进行读取时,这些操作可能还没有传播。

下面这个litmus test显示了x86全局序模型的含义:

Litmus Test: Independent Reads of Independent Writes (IRIW)
Can this program see r1 = 1, r2 = 0, r3 = 1, r4 = 0?
(Can Threads 3 and 4 see x and y change in different orders?)

// Thread 1    // Thread 2    // Thread 3    // Thread 4
x = 1          y = 1          r1 = x         r3 = y
                              r2 = y         r4 = x
On sequentially consistent hardware: no.
On x86 (or other TSO): no.
On ARM/POWER: yes!

在ARM/POWER架构上,不同的线程可能会以不同的顺序观测到写入操作。它们不能保证对于写入操作的全局序,因此线程3可能会在线程4之前看到x的改变,而线程4则可能在x改变之前看到y的改变。

另一个例子,能更直观的看出ARM/POWER系统内存读取(加载)的重排,就像下述litmus test所证明的那样:

Litmus Test: Load Buffering
Can this program see r1 = 1, r2 = 1?
(Can each thread's read happen after the other thread's write?)

// Thread 1    // Thread 2
r1 = x         r2 = y
y = 1          x = 1
On sequentially consistent hardware: no.
On x86 (or other TSO): no.
On ARM/POWER: yes!

任何符合顺序一致性的交错都是从线程1的r1 = x或线程2的r2 = y开始的。这个读取操作一定会读到0,所以r1 = 1, r2 = 1是不可能的。在ARM/POWER内存模型中,处理器允许延迟读取,直到指令流中的写操作完成后,这样 y = 1 和 x = 1 就会在两个读取操作之前执行。

尽管ARM和POWER的内存模型都允许上述那种结果。Maranget 等人 reported (in 2012)仅能在ARM系统上复现上述结果,但在POWER系统上不性。这里模型和现实之间的分歧就像我们聊英特尔x86时一样:硬件实现比技术上保证的更强(内存序)的模型,软件会加大对硬件的依赖,并意味着未来更弱(内存序)的硬件将会使程序崩溃(也许)。

与TSO系统类似,ARM和POWER都有屏障,我们可以在上述示例中插入内存屏障,以强制执行顺序一致的读写行为。但显而易见的问题是,没有内存的ARM/POWER是否排除了任何行为,任何litmus test答案都可能是“不,那不可能发生”吗?当我们专注于单个内存位置时,是可以的。

下面给的这个例子即使是在ARM或者POWER上都没办法发生:

Litmus Test: Coherence
Can this program see r1 = 1, r2 = 2, r3 = 2, r4 = 1?
(Can Thread 3 see x = 1 before x = 2 while Thread 4 sees the reverse?)

// Thread 1    // Thread 2    // Thread 3    // Thread 4
x = 1          x = 2          r1 = x         r3 = x
                              r2 = x         r4 = x
On sequentially consistent hardware: no.
On x86 (or other TSO): no.
On ARM/POWER: no.

这个litmus test和上一个很类似,但现在是所有的线程都对于同一个变量x进行读写,而不是两个变量x、y。线程1和2将冲突的值1和2写入x,而线程3和线程4都两次读取x。如果线程3看到x=1被x=2覆盖,那么线程4能看到相反的情况吗?

答案是否定的,即使在ARM/POWER上也是如此:系统中的线程必须就对单个内存位置的写入达成全局序达成一致。也就是说,线程必须就哪些写入会覆盖其他写入(的顺序)达成一致。这种性质被称为连贯性(coherence)。如果没有连贯性,处理器要么对内存的最终结果意见不一,要么报告一个内存位置在两个值之间来回切换。这样一个系统的编程将会非常困难。

我故意忽略了ARM和POWER弱内存模型中的许多细微差别。如果需要更多的细节,可以看 Peter Sewell's papers on the topic。此外,ARMv8 加强了内存模型(strengthened the memory model )通过“原子的多拷贝”,但本文不对这一内容进行讲解。

这里有两个重要的要点。首先,这里有非常多微妙的东西,是非常聪明、非常执着的人们进行了超过十年的学术研究的主题。我自己也不敢说我完全理解它的所有内容。这不是我们希望向普通程序员解释的事情,也不是我们在调试普通程序时需要特别清晰的事情。第二点是,硬件允许的和实际观测到会发生的在未来可能导致一些“不幸的惊喜”,如果当前的硬件不能展示出允许行为的全部范围——特别是当很难推断首先需要考虑的的事情!—那么不可避免地会编写出一些依赖于实际硬件更限制的程序。如果一个新芯片在行为上限制较少(在内存序等上面更不严格),硬件内存模型可能在技术上破坏你的程序,这个bug在技术上是你的错。这绝对不是写程序的正确方式。

弱序以及无数据竞争顺序一致性

到现在为止,我希望你已经相信硬件细节是复杂微妙的,不是你每次写程序都想要处理的东西。相反,如果有下文阐述的这种捷径那么将很有帮助:“如果你遵循这些简单的规则,你的程序将只产生像某种顺序一致的交错一样的结果。”(我们还在谈论硬件,所以我们还在谈论交错的单个汇编指令。)

Sarita Adve 和 Mark Hill在他们1990年的论文中提出“Weak Ordering – A New Definition”,他们定义了“弱序(wealy ordered)”:

让一个同步模型成为对内存访问的一组约束条件,指定了何时以及如何需要进行同步。

硬件相对于同步模型是弱有序的,当且仅当它对遵守同步模型的所有软件来说是顺序一致的。

尽管他们的论文是关于当时的硬件设计(不是x86、ARM和POWER),但将讨论提升到超越特定设计的层面的想法,这篇论文至今与本文仍然具有相关性。

我在本文的开头提到过,“有效的优化不会改变有效程序的行为。”规则定义了什么是有效的,然后任何硬件优化都必须保持这些程序在顺序一致的机器上正常工作。当然,有趣的细节是规则本身,定义程序有效的约束条件。

Adve和Hill提出了一种同步模型,他们称之为无数据竞争模型(data-race-free, DRF)。这个模型假设硬件具有与普通内存读写分离的内存同步操作。普通的内存读写操作可能会在同步操作之间重新排序,但不能越过同步操作(即同步操作也起到了阻止重新排序的作用)。一个程序被称为无数据竞争,如果在所有理想的顺序一致执行中,来自不同线程的相同位置的任何两个普通内存访问要么都是读取(无所谓),要么通过同步操作分开,强制其中一个发生在另一个之前。

来看些例子,这些图来自Adve和Hill的论文(为了演示重画了一下)。这是一个执行变量x写入,然后读取相同变量的单线程。

竖直箭头标志着单个线程内的执行顺序:先写入,然后读取。在这个程序中没有竞争,因为一切都在单个线程中。

相反,在这个双线程程序中存在竞争:

线程2在不与线程1协调的情况下写入x。线程2的写入与线程1的写入和读取都存在竞争。如果线程2读取x而不是写入它,程序将只有一个竞争,即线程1中的写入和线程2中的读取之间的竞争。每场竞争都至少涉及一次写入:两次不协调的读取不会相互竞争。

为了避免数据竞争,我们必须添加同步操作,强制在共享同步变量的不同线程上的操作之间建立顺序。如果同步操作S(a)(在变量a上同步,由虚线箭头标记)强制线程2的写操作发生在线程1完成之后,那么就消除了竞争。

现在,线程2的写操作不能与线程1的操作同时发生。

如果线程2只是在读取,我们只需要与线程1的写操作进行同步。两个读操作仍然可以并发进行:

线程可以通过一系列的同步来进行排序,甚至可以使用中间线程。下面这个程序没有竞争:

另一方面,使用同步变量S本身并不能消除竞争:有可能使用不正确。下面程序存在竞争:

线程2的读取与其他线程中的写入是正确同步的——它绝对发生在两者之后——但两个写入本身并没有同步。这个程序不是无数据竞争的。

Adve和Hill将弱序称为“软件和硬件之间的契约”,具体来说,如果软件避免数据竞争,那么硬件会表现得好像是顺序一致的,这比我们在前面部分所研究的模型更容易理解。但是硬件如何满足契约的要求呢?

Adve和Hill提供了一个证据,即硬件“按照DRF进行弱排序”,这意味着它执行无数据竞争的程序,就好像是按照顺序一致的顺序进行,只要它满足一组特定的最低要求。我不打算详细讨论,但重点是,在Adve和Hill的论文之后,硬件设计师有了一个有证据支持的cookbook:做这些事情,你就可以断言你的硬件对于无数据竞争的程序来说是顺序一致的。实际上,大多数较弱内存序的硬件确实是这样行为的,并且在适当实现同步操作的情况下仍然如此。Adve和Hill最初关注的是VAX,但x86、ARM和POWER也可以满足这些约束。这个系统保证对于无数据竞争的程序来说,表现出来的是顺序一致性的想法通常被简称为DRF-SC。

DRF-SC标志着硬件内存模型的一个转折点,为硬件设计师和软件作者提供了明确的策略,至少是那些使用汇编语言编写软件的人。正如我们将在下一篇文章中看到的那样,关于高级编程语言的内存模型并没有那么清晰和整洁的答案。

下一篇文章将讲解编程语言的内存模型(programming language memory models

致谢

原po感谢谷歌公司一大批工程师的讨论与反馈,原po对任何错误以及一些不受欢迎的观点负责。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值