编译器指令重新排序

Posted

技术标签:

【中文标题】编译器指令重新排序【英文标题】:Compiler instruction reordering 【发布时间】:2016-06-29 03:00:52 【问题描述】:
struct Vec

    double x, y, z;
;

Vec vec;
vec.x = 1.0;
vec.y = 2.0;
vec.z = 3.0;
double res = (&vec.x)[2];    // (&vec.x)[2] should be equal to vec.z

变量 res 的值应该等于 3。但是当我打开优化时,编译器会错误地重新排序指令并且 res 包含一些垃圾。一些可能的重新排序示例:

vec.x = 1.0;
vec.y = 2.0;
vec.z = 3.0;
res = (&vec.x)[2];    // correct value

vec.x = 1.0;
vec.y = 2.0;
res = (&vec.x)[2];    // incorrect value
vec.z = 3.0;

vec.x = 1.0;
res = (&vec.x)[2];    // incorrect value
vec.y = 2.0;
vec.z = 3.0;

这是编译器中的错误吗?还是不允许这样访问struct数据成员?

编辑:

我刚刚意识到前面的代码确实有效,对此感到抱歉。但这不起作用:

Vec vec;
vec.x = 1.0;
vec.y = 2.0;
vec.z = 3.0;
double res = (&vec.x)[i];    // (&vec.x)[i] should be equal to vec.z when i == 2

当编译时不知道变量i时,编译器会错误地重新排序指令。

【问题讨论】:

@BatCoder:如果是未定义的行为,是否可以重现并不重要。 Javers,(&vec.x)[2]是什么意思,你怎么理解这个? (&vec.x)[2] 获取 vec.x 的地址,添加 2 并取消引用它。它应该返回 vec.z 的值 如果您想要类似数组的行为,您应该重载结构的下标运算符(即 operator[]),而不是尝试以无法合法使用的方式使用指针运算。见:learncpp.com/cpp-tutorial/98-overloading-the-subscript-operator 原始代码使用 operator[] 做同样的事情——它返回 (&x)[i] 【参考方案1】:

您正在调用未定义的行为。 (&vec.x)[2] 等同于 (&vec.x) + 2,标准 (§[expr.add]/4) 对指针添加有以下说法(强调我的):

当具有整数类型的表达式被添加或减去时 从一个指针,结果具有指针操作数的类型。如果 指针操作数指向数组对象84的一个元素,而 数组足够大,结果指向一个元素偏移量 原始元素使得下标的差异 结果和原始数组元素等于积分表达式。 换句话说,如果表达式 P 指向一个 数组对象,表达式 (P)+N(等价于 N+(P))和 (P)-N (其中 N 的值为 n)分别指向 i + n-th 和 i - 数组对象的第 n 个元素,前提是它们存在。此外,如果 表达式 P 指向数组对象的最后一个元素, 表达式 (P)+1 指向数组对象的最后一个元素, 如果表达式 Q 指向数组的最后一个元素 对象,表达式 (Q)-1 指向数组的最后一个元素 目的。 如果指针操作数和结果都指向元素 相同的数组对象,或数组的最后一个元素 对象,评估不应产生溢出;否则, 行为未定义。

另见上文引用的注释 84:

84) 为此目的,一个不是数组元素的对象被认为属于一个单元素数组;见 5.3.1。

由于vec.x不是数组,所以被认为是单元素数组的一个元素。因此vec.xvec.z 不是同一个数组的元素,行为未定义。

【讨论】:

【参考方案2】:

除了未定义的行为,请注意 C++ 不会强制将它们分配在一起。如果您想同时通过索引和名称来引用它们,我建议您采用另一种方式:

struct Vec

    double elems[3];
    double& x;
    double& y;
    double& z;

    Vec()
    : x( elems[0] ), y( elems[1] ), z( elems[2] )
    
    
;

如果您负担不起每个对象可能(但不是必需的)额外空间,您不妨将 x、y、z 声明为返回引用的函数。

【讨论】:

【参考方案3】:

我最终找到了一个解决方案,它可以防止指令重新排序并且不会增加 Vec 结构的大小:

struct Vec

    union
    
        struct
        
            double x, y, z;
        ;
        double data[3];
    ;
;

Vec vec;
vec.x = 1.0;
vec.y = 2.0;
vec.z = 3.0;
double res = vec.data[i];

感谢您的回答。

【讨论】:

这保证有效吗?还是编译器特定的行为?你用其他编译器测试过吗? 那仍然是 UB,两次。 sizeof(Vec) == 24. union 为 x 和 data[0]、y 和 data[1]、z 和 data[2] 共享相同的内存。

以上是关于编译器指令重新排序的主要内容,如果未能解决你的问题,请参考以下文章

SSE 优化(行重新排序、操作整理)中的编译器(例如 g++)有多聪明

指令重排序

重排序

指令重排序导致的可见性问题

jvm 指令重排

指令重排序