C++别名规则

23

请问有人能为我确认一些别名规则吗?

我知道别名问题(即load-store问题)可能会使以下代码不够优化,因为我们不能假设x,y,z不重叠:

// case 1:
void plus(size_t n, double *x, double *y, double *z)
{
    for (size_t i = 0; i != n; ++i)
        z[i] = x[i] + y[i];
} 

我知道 C 语言关键字__restrict可以提示编译器不去考虑重叠的情况,因此可能会生成更好的代码:

// case 2:
void plus(size_t n, double *__restrict x, double *__restrict y, double *__restrict z)
{ // as above... }

但是当我们使用C++风格的代码时,别名如何工作呢?在这种情况下,我们需要处理通过引用传递的容器对象,而不是上面使用原始指针的C语言示例?

例如,如果我们执行以下操作,我认为会出现别名问题:

// case 3:
void plus(std::vector<double> &x, std::vector<double> &y, std::vector<double> &z)
{ // similar to above... }

那么,为了举例说明,如果容器中的基础数据类型不同,是否会有任何区别?在实现层面上,大多数容器使用指针动态管理存储,因此我不清楚编译器如何确保以下内容不重叠:

// case 4:
void foo(std::vector<mytype1> &x, std::vector<mytype2> &y)
{ // interwoven operations on x, y... }

我并不试图进行微小的优化,但我想知道目前将受限制的指针传递到容器中是否比传递引用更好。

编辑:为了澄清一些术语,正如指出的那样:restrict是C99关键字。各种编译器中还有__restrict__restrict__,但它们都做同样的事情。


对于容器示例,除非&x == &y,否则我不相信它们会重叠,假设符合标准的向量实现。 - Sven
@Sven:不同的std::vector不会重叠,但通常你可以通过引用传递任何对象类型,可能是重叠的。因此,我希望编译器需要确保正确性,并对别名进行保守假设。这就是为什么__restrict出现的原因... - Darren Engwirda
@Darran:是否有编译器可以在传递重载operator[]的对象时进行矢量优化或其他优化,而不是实际的数组或指针?我有点怀疑。 - Sven
@Sven:据我所知,负载存储问题不仅仅是关于循环是否可以展开为sse/sse2等向量指令,而更多地涉及到在循环内部,在存储操作之后,通过可能别名的指针访问的所有变量都需要重新加载,这会影响寄存器使用。至于std::vector,我希望大多数编译器/实现都会内联operator[],否则可能会出现严重的性能问题,无论是否存在别名... - Darren Engwirda
restrict 是 C 语言的一个关键字(自 C99 起),但 __restrict 不是。由于 __ 的存在,它是编译器的扩展。 - Luc Danton
2个回答

9
根据严格别名规则,除了char*和相关类型,您不允许使用指向不同类型的指针来别名相同的内存,因此如果其中一个类型是char*,则仅适用于第4种情况。
然而,对于所有编译器,引用都是以指针形式实现的,尽管标准并不要求这样做,实现可以提出其他方案,因此第3种情况与第1种情况并没有太大区别。

我同意情况1和3在别名方面本质上是相同的,但是如果你按照情况3使用引用,你就不能使用__restrict,这就是为什么我认为通过受限指针传递容器可能比引用更好的原因。 - Darren Engwirda
@Darren:C++标准甚至没有提到__restrict关键字,这是MSVC的扩展。也许你应该在问题中加上这一点。 - Xeo
感谢提供严格别名信息,这对我来说是新的。在实际代码中,情况4可能是最常见的情况,因此别名问题可能不会像我想象的那样经常发生。而 _restrict、restrict、_restrict 等(正如我所指出的)是 C(99)关键字,但至少 GCC 和 MSVC 支持它用于 C++。 - Darren Engwirda
除了char*和相关类型,您能否帮忙添加一个特定的允许类型列表,以使您的答案更完整?谢谢。=) - kevinarpe

5

这与C++无关。考虑一下C99的这段代码:

struct vector {
    double* data;
    size_t n;
};

void
plus(struct vector* restrict x, struct vector* restrict y, struct vector* restrict z)
{
    // same deal as ever
}

这里,restrict 对我们几乎无用: x->datay->dataz->data 都是 double*,并且允许别名。这与情况1完全相同,即使使用 restrict 也是如此。
如果在C ++(或使用扩展时)中有一个restrict 关键字,最好的方法可能是使用与情况2中相同的plus,执行 plus(vecA.size(), &vecA[0], &vecB[0], &vecB[0])。事实上,现在可以使用不带有restrict 但在内部使用该关键字的C89样式接口来完成此操作。

1
你能否在这种情况下定义类似于struct vector { double *restrict data; //etc };,以防止别名问题? - Darren Engwirda
@Darren 在这种情况下你可以这样做并且它会起作用,但是我认为考虑到 void f(struct vector* x, struct vector* y),那么 f(p, p) 是有问题的:x->data 将会是 double* restrict 但是会与 y->data 别名(如果我理解正确的话)。restrict 看起来更像是一个强大的工具,应该保留给特殊情况而不是万能药。 - Luc Danton

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接