【问题标题】:How to forbid C++ compiler optimizing when using global variable as parameters使用全局变量作为参数时如何禁止C++编译器优化
【发布时间】:2016-09-02 02:31:49
【问题描述】:

在 C++ 中使用全局变量作为参数时,我发现一个奇怪的现象。 这是代码。如果使用 -O2,程序将永远不会结束。

#include <thread>
#include <iostream>

using namespace std;

#define nthreads 2
struct Global{
    int a[nthreads];
};

void func(int* a){
    while(*a == 0){
    }
    cout << "done" << endl;
}

struct Global global;

int main(){
    thread pid[nthreads];
    memset(global.a, 0, nthreads*sizeof(int));

    for(int i=0; i<nthreads; i++){
        pid[i] = std::thread(func, &global.a[i]);
    }

    sleep(2);
    cout << "finished" << endl;
    memset(global.a, 1, nthreads*sizeof(int));

    for(int i=0; i<nthreads; i++){
        pid[i].join();
    }

    return 0;
}

如果使用 -O0,一切似乎都正常。 并在while循环中打印变量*a,还是可以的。

所以我猜一定是C++优化的问题。 但是编译器怎么可能对全局变量和多线程进行如此激进的优化呢?


感谢所有答案和 cmets,我尝试使用 volatile,它确实有效。我不想使用互斥锁,因为在每个循环中使用互斥锁都会影响性能。

其实我想做这样的事情:

  1. 一个工作线程循环遍历一个全局列表,并在每个 while 循环中执行一些操作。(我不想在这里使用互斥锁,因为即使错误发生在一个循环中也没关系)

  2. 其他一些线程可能会将项目添加到此列表中。(这里使用互斥锁是可以的。因为每个线程只添加一次)

我应该怎么做?

【问题讨论】:

  • 只在两个线程之一中使用互斥锁是没有意义的——它实际上是一种互斥机制。仅在一侧不起作用。
  • 在最后一部分,您所描述的称为生产者-消费者模式。您可能想查看专为此类模式设计的 Boost LockFree。

标签: c++ multithreading global-variables compiler-optimization


【解决方案1】:

当前代码允许编译器像没有线程一样进行优化。所以当编译器看到一个条件不变的循环时,它可以优化它。或者,就像您观察到的行为一样,将条件中的预期内存提取替换为来自寄存器的值。

完成这项工作的一种方法是使用std::atomic。

出于学习和探索的目的,我只涉足现代 C++ 中的多线程,但这段代码可以正常工作:

#include <atomic>
#include <array>
#include <thread>
#include <iostream>
using namespace std;

int const nthreads = 2;

void func( atomic<int>* a )
{
    while( a->load() == 0 )
    {}
    cout << "done" << endl;
}

namespace global {
    array<atomic<int>, nthreads> a;     // Zero-initialized automatically.
}  // namespace global

auto main()
    -> int
{
    using namespace std::chrono_literals;

    thread pid[nthreads];
    for( int i = 0; i < nthreads; ++i )
    {
        pid[i] = thread( func, &global::a[i] );
    }

    this_thread::sleep_for( 2ms );
    cout << "finished" << endl;
    for( auto& item : global::a )
    {
        item = ( int( unsigned( -1 ) & 0x0101010101010101 ) );
    }

    for( int i = 0; i < nthreads; ++i) { pid[i].join(); }
}

【讨论】:

    【解决方案2】:

    因为编译器对线程一无所知。它所做的只是编译它被告知要编译的代码。

    允许编译器优化冗余代码。在这里,由于编译器知道它检索了指针所指向的值,所以它不需要再做一次。

    有几种方法可以正确地做到这一点。

    1. 使用 volatile,一个 volatile 限定符明确告诉编译器不要优化对 volatile 对象的任何访问。

    2. volatile 关键字本身并不总是足以正确实现多线程执行。尽管您可以在这里侥幸成功,但正确排序多线程执行的正确方法是使用互斥锁和条件变量。你会在你最喜欢的 C++ 书籍中找到完整的描述。

    【讨论】:

    • volatile is basically useless here。获得所需的行为既没有必要也不够。
    • @JerryCoffin: volatile 阻止编译器优化检查循环。这基本上就是 volatile 所做的:防止每个内存访问被优化掉,例如内存映射 i/o 卡寄存器。但它不能保证获取的值是更新的值:它可能是缓存中的值。
    • −1 因为“正确执行此操作。1 使用 volatile”的建议在许多(大多数?)平台上是不正确的。并且不受标准保证。 Andrei Alexandrescu,我认为和 Herb Sutter 一起写了一篇不错的文章,说明为什么 volatile 对可移植代码中的多线程非常不利。我没有链接,对不起,但谷歌它。而不是volatile,代码可以使用例如atomic 变量。或显式同步。
    • @Cheersandhth.-Alf:我猜你想的论文是Alexandrescu and Meyers。
    • @JerryCoffin:是的,就是这样。谢谢! :)
    猜你喜欢
    • 2014-08-29
    • 1970-01-01
    • 2010-10-06
    • 1970-01-01
    • 2010-11-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多