C ++:一个对象既可以存储也可以不存储?

时间:2018-06-06 20:52:32

标签: c++ multithreading language-lawyer undefined-behavior

我一直在辩论一个关于多线程环境中局部变量的案例。

问题在于形成如下的程序:

std::mutex mut;

int main()
{
    std::size_t i = 0;
    doSomethingWhichMaySpawnAThreadAndUseTheMutex();
    mut.lock();
    i += 1;        // can this be reordered?
    mut.unlock();
    return i;
}

问题在于i += 1是否可以在互斥锁定之上重新排序。

明显的部分是mut.lock() 发生在 i += 1之前,所以如果任何其他线程可能能够观察到i的值,编译器有义务不增加它。从C ++规范的3.9.2.3开始,"如果类型T的对象位于地址A,则类型为cv T *的指针(其值为地址A)被称为指向该对象,无论如何获得该值。""这意味着如果我使用任何方法获得指向i的指针,我可以期望看到正确的值。

但是,规范确实说明编译器可能会使用" as-if"规则不给对象一个记忆地址(第1.8.6节的脚注4)。例如,i可以存储在没有内存地址的寄存器中。在这种情况下,没有内存地址可以指向,因此编译器可以证明没有其他线程可以访问i

我感兴趣的问题是如果编译器没有这样做" as-if"优化,确实存储了对象。是否允许编译器存储i,但是如果实际上没有存储i那么会重新排序吗?从实现的角度来看,这意味着i可能存储在堆栈中,因此可能有一个指针指向它,但让编译器假设没有人能看到i,并且重新订购?

3 个答案:

答案 0 :(得分:2)

只要在没有这些优化的情况下合法地获得程序执行的可观察结果(" as-if"),就允许编译器执行优化。 [1] 所以这个问题使用" as-if"以误导的方式,如果不是实际问一个倒退的问题:

  

是否允许编译器存储i,但是如果i实际上没有存储,那么会重新排序吗?

只要可以通过优化获得程序执行结果,就会询问是否允许编译器执行操作。这不是问题。该问题应使用非优化行为作为参考。所以更像是:"编译器是否允许重新排序语句?" 答案是肯定的,只要可观察的结果不变。这个特定函数的外部没有任何东西被告知如何访问i,因此应该允许编译器在它的周围使用之间的任何地方实现增量(具体来说:它的定义和return语句)。 / p>

话虽这么说,我希望编译器在这种情况下做的是既不给i一个内存地址也不把它当作寄存器变量。我希望编译器将其视为常量,有效地将您的函数更改为:

int main()
{
    doSomethingWhichMaySpawnAThreadAndUseTheMutex();
    mut.lock();
    mut.unlock();
    return 1;
}

只要你无法检测到它已经完成(没有直接检查机器代码),这是允许的。

注意:
[1] 使用"可能是"是承认C ++规范的某些部分使用了单词" unspecified"。这些部分允许编译器做出选择(当处理非健壮的代码时)可能改变可观察的行为。也就是说,可以存在一组允许的行为,而不是单个允许的行为。只要结果保留在此集合中,就允许进行优化。

答案 1 :(得分:1)

我发现这个问题非常混乱。如果代码已发布,编译器显然意识到i语句中唯一使用return,因此i将被优化掉,故事结束。互斥体不会进入它。

但是一旦你拿到i的地址 - 并将其交给其他人 - 游戏就会改变。现在,编译器必须在堆栈上放置一个实变量,并在mutex.lock()mutex.unlock()之间仅操作它。做其他事情会改变程序的语义。互斥锁还为你提供了一个记忆围栏。

您可以在Godbolt清楚地看到这一点。

编辑:我修复了该代码中的一个愚蠢的错误,这个错误掩盖了我试图制作的内容,对不起。

答案 2 :(得分:0)

整个序列:

unsigned char* send_buffer[32] = {0}; // zero out buffer to use as scratch
send_buffer[0] = 0x90;
send_buffer[1] = 0x48;
send_buffer[2] = 0x00;

SuperpoweredUSBMIDI::send(deviceID, send_buffer, 3);

可以编译为 mut.lock(); // i == 0 at that point i += 1; // can this be reordered? mut.unlock(); // i == 1 at that point exit(i); } ,其他线程可以忽略,因为没有正确的同步。

您需要等待其他线程终止,或者让他们再次无所事事。你不这样做,所以可以假设所有其他线程都没有做任何事情。

互斥体在这里没有任何有意义的作用。