假设我有一个函数,它接受一些指针参数 – 一些非常量,可以通过它们写入,一些常量,只能通过它们读取。示例:
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
最佳答案
3
不,这还不够。
还假设该函数不会以其他方式写入内存(即不使用全局变量,固定地址,将 const 指针重铸为非常量以及诸如此类的技巧)。
这个假设是不够的。编译器还需要看到函数没有以其他方式写入指针指向的内存(包括可以通过指针访问的任何内存,例如b[18]
)。例如,如果调用bar(b);
,并且编译器看不到bar
,那么它就无法知道指向的内存b
在执行期间是否被修改f
,即使没有被修改。
给出这个额外的前提,即编译器可以看到没有对通过指向的任何内存进行修改,那么对于优化来说是否用和/或声明b
并不重要:编译器了解有关内存的所有信息,告诉它更多信息都不会添加信息。b
const
restrict
然而,代码通常不满足这个前提。(即使满足,对程序员来说,确定这一点也可能很麻烦。)因此,让我们考虑一个没有附加前提的情况:
void f(int * restrict a, int const *b)
{
printf("%d\n", *b);
bar();
printf("%d\n", *b);
}
当bar
被调用时,编译器不知道 是否*b
被修改。即使此函数没有传递b
给bar
,bar
也可能访问某个 外部对象,*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
向参数添加指向限定类型的指针也可能会启用一些优化。const
restrict
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 节对此进行了更详细的说明,尽管该文本既复杂又有点令人担忧。
因此,在您描述的情况下,restrict
ing 仅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也许“如果您试图修改,
const
inint const *b
将导致编译器向您发出警告或错误*b
,但它不能保证编译器*b
不会被修改……”
–
|
restrict
。您期望什么?–
–
restrict
在修改数据时才会发生。因此,通过受限指针修改数据指针a
意味着必须通过 来访问此数据a
。因此,通过指针进行的任何访问b
都不能别名 指向的数据a
。因此,即使const
不再是优化所必需的,实际上也从来都不是。请参阅–
const
这对于优化来说不是必需的。我问的是输出限制是否足够。你似乎在说是的……你能引用标准中的一句话来支持这一点吗?–
int * restrict a
意味着赋予函数的指针a
不会与其他引用数据重叠。对 的所有更改*a
在函数中都是直接可见的。int const * b
,没有restrict
没有 的保证*b
。const
意味着函数不会直接改变它的*b
,但改变*a
,因为a[0] += b[0];
仍然可以*b
通过f(p, p);
2) 发布的汇编来改变,这是一个指标,但还不够。 错误调用的函数,可以防止不安全的参数,导致 UB。 发出的代码不必有所不同。–
|