JUN 25, 2012 原文在这:http://preshing.com/20120625/memory-ordering-at-compile-time/
前言:这是译自preshing博客的第二篇文章,本文讨论的是编译优化导致的重排序问题。

从你写C/C++源代码到它在CPU里执行,这段代码的内存交互可能会根据特定的规则被重新排序了。编译器(编译期)和processor(运行期)都有可能导致内存序的改变,都是为了能够让你的代码运行的更快。
对于memory reordering,被编译器开发者和CPU制造商广泛遵循的主要原则就是:
绝对不能修改单线程程序的行为;
Thou shalt not modify the behavior of a single-threaded program.

这条规则的结果就是,对于单线程代码,程序员完全无需关注memory reordering。通常多线程程序也不需要关注,因为从设计上,mutex, semaphore和event都会在它们的调用点防止memory reordering。仅仅在编写lock-free代码时memory reordering才可能会出来捣乱——被多线程共享的内存没有任何的互斥锁,memory reordering的影响很容易观察到,可见前一章的例子。

需要提醒你的是,在编写多核平台上的lock-free代码时还是有方法可以回避memory reordering的困难。就像在introduction to lock-free programming中提到的,你可以利用sequential consistent类型,比如Java中的volatile和C++11的atomic——可能只是很小的性能代价。这里不再深入说了。本篇文章,我将集中关注编译器memory reordering对常规的non-consequential-consistent类型的影响。

Compiler Instruction Reordering

如你所知,编译器的职责就是将源代码转换为CPU可以执行的机器码,在转换过程中,编译器有许多自由空间可以操作。

其中一种自由就是指令重排序——再一次,只有单线程程序的行为不会修改。这种指令重排序只是在开启了编译优化时才生效。考虑下面的函数:

int A, B;
void foo() {A = B + 1;B = 0;
}

如果我们在关闭编译优化的情况下使用GCC4.6.1编译这个函数,它会生成如下的机器码,对全局变量B的store刚好在对A的store之后,就像在源代码中那样。

$ gcc -S -masm=intel foo.c
$ cat foo.s...mov     eax, DWORD PTR _B  (redo this at home...)add     eax, 1mov     DWORD PTR _A, eaxmov     DWORD PTR _B, 0...

注意上面的mov DWORD PTR B, 0的位置,这一语句是对B的赋值。然后开启-O2优化再编译一次,对比看看:

$ gcc -O2 -S -masm=intel foo.c
$ cat foo.s...mov     eax, DWORD PTR Bmov     DWORD PTR B, 0add     eax, 1mov     DWORD PTR A, eax...

这一次,编译器自由发挥了,将对B的store重新排序到了对A store的前面。有何不可呢?这并没有破坏memory reordering的规则:一个单线程执行的程序永远不知道这种差异。

在另一方面,这样的编译器reordering会使lock-free代码产生问题。下面就是一个被广泛引用的例子,一个共享flag用来表示另外的一些共享数据是否ready:

int Value;
int IsPublished = 0;void sendValue(int x) {Value = x;IsPublished = 1;
}

想象一下,如果编译器将对IsPublished的store操作重排到对value的store操作之前,会发生什么。即使在单核系统上,我们也会遇到一个问题:一个线程可能正好在执行这两个store操作之间被操作系统抢占,其它线程将会相信value已经更新了,而其实并没有。

当然,编译器可能不会重排这些操作,生成的机器码是lock-free的操作,并且可以在任何具有强内存模型的多核CPU上运行良好(strong memory model,后面会详解),比如x86/64,或者单核CPU。这种情况下,其实是运气成分。不用说,我们最好能认识到可能对共享变量的memory reordering,并且强制保证正确的顺序。

Explicit Compiler Barriers

阻止编译器重排序的最小手段就是使用特别的指令,称之为compiler barrier(编译器栅栏),前面已经提到过。下面就是一个GCC中的full compiler barrier,在Visual C++中,_ReadWriteBarrier具有相同的效果。

int A, B;void foo() {A = B + 1;asm volatile("" ::: "memory");B = 0;
}

加上了这个barrier,然后再打开编译器优化,内存store指令的顺序将不会被编译器重排。

$ gcc -O2 -S -masm=intel foo.c
$ cat foo.s...mov     eax, DWORD PTR _Badd     eax, 1mov     DWORD PTR _A, eaxmov     DWORD PTR _B, 0...

类似的,如果你想保证前面的sendMessage正确工作,并且我们只关注单核系统,我们就可以使用compiler barrier。不仅发送操作需要compiler barrier来防止store操作的reordering,接收端同样需要。

#define COMPILER_BARRIER() asm volatile("" ::: "memory")int Value;
int IsPublished = 0;void sendValue(int x) {Value = x;COMPILER_BARRIER();   // prevent reordering of storesIsPublished = 1;
}int tryRecvValue() {if (IsPublished) {COMPILER_BARRIER(); // prevent reordering of loadsreturn Value;}return -1;  // or some other value to mean not yet received
}

就像我提到的,compiler barrier对于防止单核系统的memory reordering是足够的。但是在当前的时代,多核系统是很普遍的。如果要确保我们的代码在多核环境下按照期望的顺序执行,那么仅仅只有compiler barrier是不够的,我们或者需要一个CPU fence,或者任何具有运行时memory barrier的操作。这就是下一篇文章了。

Linux内核以宏的方式暴露了集中CPU fence指令,比如smb_rmb,并且这些宏在单核系统下编译时会退化为compiler barrier。

Implied Compiler Barriers

还有其他的方式可以阻止编译器重排序,确实,刚才提到的CPU fence就可以作为compiler barrier。这里是PowerPC上的fence命令,在GCC中是一个宏:

#define RELEASE_FENCE() asm volatile("lwsync" ::: "memory")

无论我们在代码的任何地方加上RELEASE_FENCE宏,除了阻止compiler reordering,还有processor reordering。比如它可以使我们的sendValue函数安全的运行在多核环境下:

void sendValue(int x) {Value = x;RELEASE_FENCE();IsPublished = 1;
}

在新的C++11 atomic标准库,每一个non-relaxed atomic操作都可以作为compiler barrier。

int Value;
std::atomic<int> IsPublished(0);void sendValue(int x) {Value = x;// <-- reordering is prevented here!IsPublished.store(1, std::memory_order_release);
}

如何所期望的那样,每一个包含compiler barrier的函数同样也是一个compiler barrier,即使是inline函数(然而微软的文档建议在早期的Visual C++编译器中可能并非如此!!!)。

void doSomeStuff(Foo* foo) {foo->bar = 5;sendValue(123);   // prevents reordering of neighboring assignmentsfoo->bar2 = foo->bar;
}

实际上不管函数本身有没有compiler barrier,大多数的函数调用都可以作为memory barrier,除了inline函数,使用了pure属性的函数声明,以及使用了link-time代码生成的情况。其它情况下,调用外部函数甚至是比compiler barrier更强的barrier,因为编译器不知道函数是否有其它影响(side effects)。对于被函数潜在可见的内存,它必须忘记任何有关的假设。

仔细想想,这很有道理。在上面的代码片段中,假设我们的sendValue实现是在一个外部库中。Compiler怎么会知道sendValue并不依赖foo->bar呢?它不会知道。因此,为了遵守memory reordering规则,它必须不能重排sendValue函数调用周围的所有内存操作。类似的,在调用完成后,它必须从内存重新load foo->bar,而不能假设它还是5,即使优化是打开的。

$ gcc -O2 -S -masm=intel dosomestuff.c
$ cat dosomestuff.s...mov    ebx, DWORD PTR [esp+32]mov    DWORD PTR [ebx], 5            // Store 5 to foo->barmov    DWORD PTR [esp], 123call    sendValue                     // Call sendValuemov    eax, DWORD PTR [ebx]          // Load fresh value from foo->barmov    DWORD PTR [ebx+4], eax...

正如你所看到的,有很多情况下编译器指令reordering都是被禁止的,甚至编译器必须重新从内存中reload一些值。我相信,人们一直以来之所以说在C中正确编写多线程程序时volatile并不是必要的,和这些隐藏的规则有很大的关系。
——>spark注:这里可能不完全正确,结合Meyers在关于DCLP的文章,有些编译器可以利用过程间分析来发现你对temp懂得小脑筋,再一次优化掉temp。还有一些编译环境会采用链接时内联的代码优化。
也就是说编译器对于本地函数可能有优化的手段,导致实际生产的memory ordering与你的期望不符。
<——over

Out-Of-Thin-Air Stores

(spark:out-of-thin-air,无中生有的,可见这种编译器技巧有多么的坑人!)
指令重排会导致lock-free编程变得很tricky?在C++11标准化之前,技术上没有能阻止编译器通向更加糟糕的窍门的技巧。特别的,编译器可以自由的引入对共享内存的store操作,如果目前还没有。这是一个极度简化的例子,灵感来自于Hans Boehm在多篇文章中提供的例子。

int A, B;void foo() {if (A) B++;
}

尽管实际中很不常见,并没有什么能阻止编译器在检查A之前将B提升到一个register,从而生成和下面等价的机器码:

void foo() {register int r = B;  // Promote B to a register before checking A.if (A)  r++;B = r;    // Surprise! A new memory store where there previously was none.
}

再一次,依然遵循了memory reordering的规则。一个单线程运行的程序依然不会察觉。但是在多线程环境下,这个函数可能会将其它线程对B的任何并发修改清除掉——即使当A为0的时候,而原始代码不会这样。这类很晦涩的技术上并非不可能的情况,正是人们说C++不支持线程的一部分原因,即使我们已经愉快的使用C/C++写了几十年的多线程和lock-free的代码。

我不知道是否有人在实际中踩过这种“out-of-thin-air” stores的大坑(I don’t know anyone who ever fell victim to such “out-of-thin-air” stores in practice)。可能正好因为我们趋向于写的lock-free代码,并没有大量的优化对应这种模式。如果你遇到过,希望能在评论中让我知道。

无论如何,新的C++11标准明确禁止了编译器的这种行为,以防止它可能引入的竞争。在最新的C++11草稿的1.10.22节:
Compiler transformations that introduce assignments to a potentially shared memory location that would not be modified by the abstract machine are generally precluded by this standard.
这句话的大意是说,对不会被抽象机器修改的潜在共享内存地址,编译器不能引入赋值操作。这样,上面的那种“无中生有”类型的store优化就被禁止了,因为它引入了对r的赋值,而实际上r是不会被抽象机器修改的内存(编译器自己引入的)。

Why Compiler Reordering?

就像我在开始时指出的,编译器修改内存指令的顺序的原因和processor是一样的——性能优化。这些优化是现代CPU复杂性的直接后果。
后面回顾了CPU的发展历程,以及编译优化的情况。不翻译了。

后注:还真有人在评论中提到了“out-of-thin-air”的例子:
http://www.airs.com/blog/archives/79,Linux kernel和gcc的开发者都抱怨过这种问题。

Memory Ordering at Compile Time相关推荐

  1. [c/c++] memory ordering

    参考: std::memory_order - cppreference.com 前言: memory ordering 又叫内存序,这个翻译其实不直观,更加具体应该叫做 cpu 访问内存的顺序(FI ...

  2. Memory Ordering

    Background 很久很久很久以前,CPU忠厚老实,一条一条指令的执行我们给它的程序,规规矩矩的进行计算和内存的存取. 很久很久以前, CPU学会了Out-Of-Order,CPU有了Cache, ...

  3. volatile关键字及编译器指令乱序总结

    本文简单介绍volatile关键字的使用,进而引出编译期间内存乱序的问题,并介绍了有效防止编译器内存乱序所带来的问题的解决方法,文中简单提了下CPU指令乱序的现象,但并没有深入讨论. 以下是我搭建的博 ...

  4. Memory Barriers

    转自:Unsorted Documentation - The Linux Kernel documentation Unsorted Documentation¶ brief tutorial on ...

  5. 理解 C++ 的 Memory Order 以及 atomic 与并发程序的关系

    为什么需要 Memory Order 如果不使用任何同步机制(例如 mutex 或 atomic),在多线程中读写同一个变量,那么,程序的结果是难以预料的.简单来说,编译器以及 CPU 的一些行为,会 ...

  6. Why Memory Barriers?中文翻译(上)

    转载自:Why Memory Barriers?中文翻译(上) 本文是对perfbook的附录C Why Memory Barrier的翻译,希望通过对大师原文的翻译可以弥补之前译者发布的关于memo ...

  7. Why Memory Barriers中文翻译(下)

    转载自:Why Memory Barriers中文翻译(下) 在上一篇why memory barriers文档中,由于各种原因,有几个章节没有翻译.其实所谓的各种原因总结出一句话就是还没有明白那些章 ...

  8. 【Manual】Memory Cache Control

    [Intel-64 and IA-32 Architectures Software Developer's Manual]Chapter 11 本章节关于 memory cache.cache co ...

  9. [zz from newsmth]王大牛的Memory Model白话系列(2)

    发信人: yifanw (王轶凡), 信区: CPlusPlus 标  题: 内存模型之白话解决方案 发信站: 水木社区 (Sun Mar 15 17:02:31 2009), 站内 99%的情况下: ...

最新文章

  1. 计算机专业的第二批本科大学,我校22个专业入选第二批一流本科专业建设“双万计划”...
  2. Windows消息机制学习笔记(三)—— 消息的接收与分发
  3. Linux学习笔记(四)|软件安装指令
  4. python3 解析html_Python3.x网页抓取HTMLParser
  5. Linux学习之系统编程篇:死锁的情形及其解决
  6. express+mongodb+vue实现增删改查-全栈之路
  7. kaggle奖牌发放体系(转)
  8. BZOJ4122 : [Baltic2015]File paths
  9. STM32中C语言知识点:初学者必看,老鸟复习(长文总结)
  10. linux 远程桌面配置,linux 远程桌面的配置
  11. IBM Power System P550双机系统方案
  12. 华为18级工程师三年心血终成趣谈网络协议文档(附大牛讲解)
  13. python特点 可移植性_下面的选项中,不属于Python特点的是( )_学小易找答案
  14. 怎么查看php是否安装了symfony_为什么开发人员讨厌PHP???
  15. springboot菜鸟入门
  16. 我的数学之美(一)——RANSAC算法详解
  17. 电脑端几行代码完成微信多开
  18. 浅谈java中的ServerSocket和Socket的通信原理实现聊天及多人聊天
  19. Spring MVC参数化测试 - Junit Parameterized
  20. [Unity3D]Unity3D游戏开发之鼠标旋转、缩放实现3D物品展示

热门文章

  1. Tornado基础知识
  2. 硬盘知识:硬盘结构、盘片、磁道、扇区、柱面、磁头数、寻址模式
  3. 涉嫌出售 50 亿个人数据,甲骨文面临集体诉讼
  4. Photoshop 2022(PS 2022) 激活版 win/mac
  5. Java 简单实现计算器
  6. openstack报错:Failed to discover available identity versions when contacting http://controller:5000/v3
  7. 阿里CEO张勇:网络安全不仅要防守更要进攻 核心是大数据
  8. 机器学习-贝叶斯公式
  9. 使用 VaultWarden 搭建个人密码管理器 原先Bitwarden
  10. 云堡垒机的作用_三分钟了解什么是云堡垒机