【问题标题】:Why gcc isn't optimizing the global variable?为什么 gcc 不优化全局变量?
【发布时间】:2017-09-20 03:00:26
【问题描述】:

我试图通过一个示例来了解volatile 的行为和C 中的编译器优化。

为此,我提到了:

Where to use volatile?

Why is volatile needed in C?

https://software.intel.com/en-us/blogs/2007/11/30/volatile-almost-useless-for-multi-threaded-programming

以上所有帖子都至少有一个与信号处理程序相关的答案,因此为此,我编写了一个简单的代码来实际实现和观察 Linux 中的行为,只是为了理解。

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <pthread.h>

int counter = 0;

void *thread0_func(void *arg)
{
    printf("Thread 0\n");
    while(1)
    {

    }
    return NULL;
}

void *thread1_func(void *arg)
{
    printf("Thread 1\n");
    while(counter == 0)
    {
        printf("Counter: %d\n", counter);
        usleep(90000);
    }
    return NULL;
}

void action_handler(int sig_no)
{
    printf("SigINT Generated: %d\n",counter);
    counter += 1;
}

int main(int argc, char **argv)
{
    pthread_t thread_id[2];

    struct sigaction sa;

    sa.sa_handler = action_handler;

    if(sigaction(SIGINT, &sa, NULL))
        perror("Cannot Install Sig handler");


    if(pthread_create(&thread_id[0], NULL, thread0_func, NULL))
    {
        perror("Error Creating Thread 0");
    }
    if(pthread_create(&thread_id[1], NULL, thread1_func, NULL))
    {
        perror("Error Creating Thread 0");
    }
    else
    {

    }
    while(1)
    {
        if(counter >= 5)
        {
            printf("Value of Counter is more than five\n");
        }
        usleep(90000);
    }
    return (0);
}

此代码仅供学习和理解。

我尝试使用以下代码编译代码:
gcc -O3 main.c -o main -pthread

但是编译器没有作用于全局变量counter,也没有对其进行优化。
我期待*thread1_func 永远循环执行,而if (counter &gt;= 5) 永远不会是真的。

我在这里错过了什么?

GCC 版本:gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.4)

【问题讨论】:

  • 如果action_handler被实际执行,程序有未定义的行为。
  • 为什么会这样?
  • 您正在修改counter,同时从另一个线程访问它。这是不允许的。
  • 那么,解决办法是声明它为volatile?
  • 如果你没有线程,你可以声明它volatile sig_atomic_t。对于线程,它不能保证工作,您需要一个实际的同步机制(互斥锁、信号量或类似的)。不过,我不确定您是否可以从信号处理程序中使用它们。

标签: c linux gcc optimization volatile


【解决方案1】:

您对counter 值的if 测试穿插着对usleepprintf 的调用。这些是不透明的库调用。编译器无法看穿它们,因此它必须假设它们可以访问counter 外部变量,因此它必须在这些调用之后重新加载counter 变量。

如果您将这些调用移出,代码会如您预期的那样得到优化:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <pthread.h>

int counter = 0;

void *thread0_func(void *arg)
{
    printf("Thread 0\n");
    while(1)
    {

    }
    return NULL;
}

void *thread1_func(void *arg)
{
    printf("Thread 1\n");
    unsigned i=0;
    while(counter == 0)
    {
       i++;
    }
    printf("Thread 1: %d, i=%u\n", counter, i);
    return NULL;
}

void action_handler(int sig_no)
{
    printf("SigINT Generated: %d\n",counter);
    counter += 1;
}

int main(int argc, char **argv)
{
    pthread_t thread_id[2];

    struct sigaction sa;

    sa.sa_handler = action_handler;

    if(sigaction(SIGINT, &sa, NULL))
        perror("Cannot Install Sig handler");


    if(pthread_create(&thread_id[0], NULL, thread0_func, NULL))
    {
        perror("Error Creating Thread 0");
    }
    if(pthread_create(&thread_id[1], NULL, thread1_func, NULL))
    {
        perror("Error Creating Thread 0");
    }
    else
    {

    }
    while(1)
    {
        if(counter >= 5)
        {
            printf("Value of Counter is more than five\n");
        }
        usleep(90000);
    }
    return (0);
}

即使你把计数器变量设为static,编译器仍然不会优化,因为虽然外部库肯定看不到计数器变量,但外部调用理论上可能有互斥锁,这将允许另一个线程在没有数据竞争的情况下更改变量。现在usleepprintf 都不是互斥锁的包装器,但是编译器不知道,也不做线程间优化,所以它必须保守并在调用和重新加载后重新加载计数器变量是什么阻碍了您期望的优化。

当然,一个简单的解释是,如果信号处理程序执行,您的程序是未定义的,因为您应该创建 counter volatile sig_atomic_t 并且您应该已经同步了对它的线程间访问_Atomic 或互斥体——在未定义的程序中,一切皆有可能。

【讨论】:

  • C 和它的编译器真的很神秘!! @PSkocik 谢谢!!
猜你喜欢
  • 2021-12-10
  • 2016-01-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-06-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多