正确的C++并行

在 《C++0x漫谈》系列之:多线程内存模型 (简称为漫谈)解释了多线程并行的困难,在这里我分层地看看这个问题。


1.为什么分层次?
C++语言表达的抽象和机器的抽象距离比较近,而且几乎看不到C++在虚拟机上跑的实例,所以两者的关系容易混淆。有人常常把反汇编拿出来一看,并作为C++语言是如此的证据,这是本末倒置的。假定C++都是直接翻译为汇编,于是这只是一个如何将一个语言翻译为另外一个语言的问题。编译器干的事就是用目标语言的一些抽象来实现源语言一些抽象,所以目标语言的语义,行为,并不能决定源语言,而只能说明翻译的正确性和质量。比如,某个C++类在语义上有个构造函数的调用,但是其行为是平凡的,在翻译后的代码中看不到任何构造函数调用的代码,这并不是构造函数不存在的证据。所以,这里我们分出层次,一方面是汇编及其实现(即CPU)的结合体,另一方面就是C++了。

2.怎么翻译?

显然,翻译建立在对两种语言上的充分理解上。理解一个语言,就要理解其提供的抽象及其语义。如何定义语义?这是研究程序设计语言的人干的,比较经典的有操作语义,指称语义。在这里我们不严格地从操作语义的角度来看这两种语言。

根据维基的描述:一个计算机语言的操作语义描述一段合理的程序是怎样被理解为一系列计算机步骤的。这些步骤就是这个程序的意义。


2.1[汇编语言的语义]如果不考虑较高层次的汇编(比如宏汇编)因为其中添加了一些东西,使得更抽象。只考虑一些常见的指令,由于汇编本身就描述了一些操作步骤,在操作过程中对环境进行改变,从一个状态转换到另一个状态,所以其操作语义和语言没什么大的间隙。如果考虑多个核在同一个环境下执行代码,intel(这里只谈x86)也定义了其memory order,其中展示了一些并行执行的汇编代码,并说明什么样的结果是可能的,什么样的结果是不可能的,以一种晦涩的方式说明了多个执行单元同时执行时执行环境的状态变化情况,换句话说,定义了语义。

此时,翻译的正确性就变成了执行环境的对等性和操作的对等性。比如C++中有一个对象,由一个32位整数,一个double浮点数描述其状态,执行环境的对等性就意味着:在汇编的执行环境中使用12个字节或16个字节(考虑对齐)来表示这个对象。C++中这个对象上的状态变化,对应于汇编中对这些字节的操作。比如在C++中有64位整数,(假定)汇编中只有32位整数,执行环境的对等性意味着:在汇编的执行环境中使用两个32位整数来表示这个64位整数。C++中定义64位整数加法操作,在汇编中用若干条指令完成64位整数的运算。

等等,在2.1中提到了并行,2.2中并没有提到(或者说在c++11之前都没提到),是的,这就是问题所在:C++本没有这样的语义,在一个C++执行环境中,有多个操作同时进行时,没有定义过其语义,不知道什么样的结果是正确的。编译器按照只有一个线程在执行的假定生成和优化代码,所以《漫谈》一文中的各种case才有可能存在。在C++11中,引入一些序关系,比如happens before,为evaluation,side effect等定义序关系,进而定义了多线程的执行模型。

3.如何保证无并行语义的语言在并行执行的时候的正确性?
《漫谈》一文中已讲。其中提到的“一个简单的办法就是禁止编译器作任何优化:所有的操作严格按照代码顺序执行,所有的操作都触发cache coherence操作以确保它们的副作用在跨线程间的visibility顺序。”我认为并不是简单禁止编译器作任何优化就能达到后面的效果,甚至需要编译器做更多的事。这些做的更多的事,相当于对源语言有所扩展:出现了并行的苗头。而后面提到的“data-race-free”使得在保证正确性的同时,提高效率,其实相当于我们面向的是编译器扩展的一个有并行语义的语言(当同步原语是编译器提供的时候认为是编译器扩展语言,当同步原语是库提供的时候,认为是库扩展了语义,同时编译器必须理解库所提供的语义,否则在调用库代码的地方给个优化就完蛋了)。如何实现库呢?面向执行环境编程。要么库直接用执行环境的语言写,要么通过插入执行环境的代码(内联汇编),并行在插入代码处编译器不作优化。

上述正确性考虑得比较理论,如果从实际上讲呢?
a假定优化不会跨函数调用。这一方面使得库的代码被保护,另一方面保证了调用处的正确性。
b面向执行环境编程,通过内联汇编来保证并行语义。
c假定优化不跨内联汇编。
上述假定较容易实现,在实现后我们得到了依赖于库的提供了并行语义的语言。

4.如果源语言有并行语义呢?
定义了C++11中的mutex,atom的语义后,翻译时编译器知道某些操作的顺序不能改变,在适当的地方应该加入mb。

你可能感兴趣的:(c++)