假设我有一个函数,它接受一些指针参数 – 一些非常量,可以通过它们写入,一些常量,只能通过它们读取。示例:

void f(int * a, int const *b);

还假设该函数不会以其他方式写入内存(即不使用全局变量,固定地址,将 const 指针重铸为非常量以及诸如此类的技巧)。

现在,是否足以(按照 C 语言标准)实现restrict内所有读取的好处f(),而仅restrict输出参数?即在示例中,限制a但没有b

一个(GodBolt)表明这种限制应该足够了。此来源:

int f(int * restrict a, int const * b) {
    a[0] += b[0];
    return a[0] + b[0];
}

int all_restricted(int * restrict a, int const * restrict b) {
    a[0] += b[0];
    return a[0] + b[0];
}

int unrestricted(int * a, int const * b) {
    a[0] += b[0];
    return a[0] + b[0];
}

为 x86_64 生成相同的目标代码:

f:
        mov     eax, DWORD PTR [rsi]
        mov     edx, DWORD PTR [rdi]
        add     edx, eax
        mov     DWORD PTR [rdi], edx
        add     eax, edx
        ret
all_restricted:
        mov     eax, DWORD PTR [rsi]
        mov     edx, DWORD PTR [rdi]
        add     edx, eax
        mov     DWORD PTR [rdi], edx
        add     eax, edx
        ret
unrestricted:
        mov     eax, DWORD PTR [rsi]
        add     eax, DWORD PTR [rdi]
        mov     DWORD PTR [rdi], eax
        add     eax, DWORD PTR [rsi]
        ret

但这并非普遍的保证。

7

  • 在这个简单的例子中,当您将指针声明为时,无法进行其他优化restrict。您期望什么?


    – 


  • @0___________:但是如果你移除所有限制,悲观预测是必要的。已将其添加到问题中。


    – 

  • 只有restrict在修改数据时才会发生。因此,通过受限指针修改数据指针a意味着必须通过 来访问此数据a。因此,通过指针进行的任何访问b都不能别名 指向的数据a。因此,即使const不再是优化所必需的,实际上也从来都不是。请参阅


    – 


  • @tstanisl:是的,我意识到const这对于优化来说不是必需的。我问的是输出限制是否足够。你似乎在说是的……你能引用标准中的一句话来支持这一点吗?


    – 

  • @einpoklum 在我看来:1)int * restrict a意味着赋予函数的指针a不会与其他引用数据重叠。对 的所有更改*a在函数中都是直接可见的。 int const * b,没有restrict没有 的保证*bconst意味着函数不会直接改变它的*b,但改变*a,因为a[0] += b[0];仍然可以*b通过f(p, p); 2) 发布的汇编来改变,这是一个指标,但还不够。 错误调用的函数,可以防止不安全的参数,导致 UB。 发出的代码不必有所不同。


    – 


最佳答案
3

不,这还不够。

还假设该函数不会以其他方式写入内存(即不使用全局变量,固定地址,将 const 指针重铸为非常量以及诸如此类的技巧)。

这个假设是不够的。编译器还需要看到函数没有以其他方式写入指针指向的内存(包括可以通过指针访问的任何内存,例如b[18])。例如,如果调用bar(b);,并且编译器看不到bar,那么它就无法知道指向的内存b在执行期间是否被修改f,即使没有被修改。

给出这个额外的前提,即编译器可以看到没有对通过指向的任何内存进行修改,那么对于优化来说是否和/或声明b并不重要:编译器了解有关内存的所有信息,告诉它更多信息都不会添加信息。bconstrestrict

然而,代码通常不满足这个前提。(即使满足,对程序员来说,确定这一点也可能很麻烦。)因此,让我们考虑一个没有附加前提的情况:

void f(int * restrict a, int const *b)
{
    printf("%d\n", *b);
    bar();
    printf("%d\n", *b);
}

bar被调用时,编译器不知道 是否*b被修改。即使此函数没有传递bbarbar也可能访问某个 外部对象,*b或者具有指向 的指针*b,因此bar可以更改 对象*b。因此,编译器必须*b从内存中重新加载第二个printf

相反,如果我们声明函数void f(int * restrict a, int const * restrict b),则restrict断言,如果 在*b的执行过程中被修改f(包括间接地,在 内bar),那么对它的每次访问都将通过b(直接,如*b,或间接地,如通过 中可见地复制或计算的指针b)进行。由于编译器可以看到bar不接收b,它知道bar不包含任何*b基于 的访问b,因此它可以假设bar不改变*b

因此,即使所有其他参数也已声明,restrict向参数添加指向限定类型​​的指针也可能会启用一些优化constrestrict

1

  • 关于“假设不足”的很好观察。


    – 

假设我有一个函数,它接受一些指针参数 – 一些非常量,可以通过它们写入,一些常量,只能通过它们读取。示例:

void f(int * a, int const *b);

还假设该函数不会以其他方式写入内存(即不使用全局变量,固定地址,将 const 指针重铸为非常量以及诸如此类的技巧)。

现在,是否足以(按照 C 语言标准)实现restrict内所有读取的好处f(),而仅restrict
输出参数?即在示例中,限制
a但没有b

restrict是片面的。它允许编译器“基于” restrict-qualified 指针对通过左值进行的对象访问做出假设,而不依赖于任何其他指针是否也是restrict-qualified。

具体来说,如果你restrict使用参数,a那么无论你是否也restrict 使用参数b,你都允许编译器假设在任何给定的执行过程中f()

  • 如果L是任何左值,其地址“基于” a,并且
  • L用于访问其指定的对象(读取或写入),并且
  • 在函数执行期间,该对象被以任何方式修改,那么
  • 该函数执行期间对该对象的每次访问(读取或写入)都将通过其地址“基于”的左值进行a,但不一定通过L特定方式进行。
  • (例如,该对象将不会通过“基于”b但不基于的左值来访问a。)

C17 第 6.7.3.1 节对此进行了更详细的说明,尽管该文本既复杂又有点令人担忧。

因此,在您描述的情况下,restricting 仅a使编译器有权假设通过派生自的指针进行的读取b永远不会观察到通过派生自的指针进行的写入a。 当然,它是否真的会生成不同的代码是一个完全不同的问题。

2

  • “… 使编译器有权假设… 来自”<- 但这并没有用“是”或“否”来回答问题 🙁


    – 

  • @einpoklum,开头的“restrict是片面的。它允许编译器做出不依赖于其他指针是否也restrict符合条件的假设[…]”旨在更直接地回答提出的问题。但我的问题在于,我不确定您对“好处restrict”的期望是否与规范相符,因此,我不会直接回答“是”,而是告诉您您可以期待哪些好处。


    – 


如果您尝试修改指针指向的值,则显示的关键字const将导致警告或错误,但它不一定阻止您修改该值,并且肯定不会阻止其他人修改该值。 因此,编译器可能必须假设可能发生此类修改。

例如,如果调用在不同源文件中定义的某个函数,编译器将不知道该函数的作用,因此它必须假定该函数可能以某种方式访问​​该指针指向的值,并可能对其进行修改。

此外,即使您修改了非常量指针指向的值,该非常量指针也可能指向与常量指针相同的值。(如f( &x, &x );)如果没有restrict常量指针,编译器必须假设这也是一种可能性。

因此,即使const参数也可以从关键字中受益restrict,因为它承诺该指针指向的内存不会被任何人修改。本质上,您将承诺永远不会做类似的事情f( &x, &x );

9

  • 此声明无效。如果对象不存在,volatile则您承诺在执行函数时,引用的对象不会被任何东西改变。


    – 

  • @0___________ 我以前也这么认为volatile,但事实证明并非如此。如果你调用另一个函数,编译器必须考虑另一个函数可能更改指针指向的内存的可能性const。如果你修改另一个指针指向的值,编译器必须考虑另一个指针可能指向与指针相同的位置的可能性const


    – 


  • “我认为”——……


    – 


  • 1
    const指针上的关键字是一个约束性承诺,即您不会(不能)修改指针指向的内存”是错误的。在限定指向类型时,const仅提供咨询功能:如果const-qualified 左值是赋值或其他会修改它的操作(例如++)的主题,则 C 实现必须发出诊断。将对象定义const为-qualified 类型意味着实现可以假定它从未被修改过。但是用于指向对象的类型和用于定义对象的类型是完全独立的:…


    – 

  • 1
    也许“如果您试图修改, constinint const *b将导致编译器向您发出警告或错误*b,但它不能保证编译器*b不会被修改……”


    –