restrict
关键字的行为在C99中由6.7.3.1定义:
设D是提供方法的普通标识符的声明 将对象P指定为类型T的限制限定指针。
如果D出现在块内并且没有存储类extern, 让B表示块。如果D出现在参数列表中 声明函数定义,让B表示关联的 块。否则,让B表示主块(或块) 在独立的程序启动时调用的任何函数 环境)。
在下文中,指针表达式E被称为基于对象 P if(在B之前执行B的某个序列点) 评估E)修改P以指向数组对象的副本 它以前指出的将改变E.119的值。注意 ''based''仅针对具有指针类型的表达式定义。
在每次执行B期间,让L为任何具有& L基础的左值 P.如果L用于访问它的对象X的值 指定,并且X也被修改(通过任何方式),然后是以下 要求适用:T不得符合要求。每个其他左值 用于访问X的值也应具有基于P的地址。 修改X的每个访问也应被视为修改P,for 本条款的目的。如果P被赋值为a的值 指针表达式E基于另一个受限制的指针 对象P2,与块B2相关联,然后执行B2 应在执行B之前开始,或者执行B2 在转让之前结束。如果不满足这些要求,那么 行为未定义。
就像其他人一样,我很难理解这个定义的所有复杂性。作为这个问题的答案,我希望看到第4段中每个要求违反要求的一些好例子。这篇文章:
在“编译器可能假设......”方面很好地呈现规则;扩展该模式并将编译器可以做出的假设以及它们如何无法保持,每个示例都很棒。
答案 0 :(得分:8)
下面,我将参考与问题相关的Sun论文中的用例。
(相对)明显的情况是mem_copy()情况,它属于Sun论文中的第二个用例类别(f1()
函数)。假设我们有以下两种实现:
void mem_copy_1(void * restrict s1, const void * restrict s2, size_t n);
void mem_copy_2(void * s1, const void * s2, size_t n);
因为我们知道s1和s2指向的两个数组之间没有重叠,所以第一个函数的代码将是直截了当的:
void mem_copy_1(void * restrict s1, const void * restrict s2, size_t n)
{
// naively copy array s2 to array s1.
for (int i=0; i<n; i++)
s1[i] = s2[i];
return;
}
s2 = '....................1234567890abcde' <- s2 before the naive copy
s1 = '1234567890abcde....................' <- s1 after the naive copy
s2 = '....................1234567890abcde' <- s2 after the naive copy
OTOH,在第二个功能中,可能存在重叠。在这种情况下,我们需要检查源数组是否位于目标之前,反之亦然,并相应地选择循环索引边界。
例如,说s1 = 100
和s2 = 105
。然后,如果n=15
,则在复制之后,新复制的s1
数组将超出源s2
数组的前10个字节。我们需要确保首先复制较低的字节。
s2 = '.....1234567890abcde' <- s2 before the naive copy
s1 = '1234567890abcde.....' <- s1 after the naive copy
s2 = '.....67890abcdeabcde' <- s2 after the naive copy
但是,如果s1 = 105
和s2 = 100
,则首先写入较低字节将超出源s2
的最后10个字节,并最终得到错误的副本。< / p>
s2 = '1234567890abcde.....' <- s2 before the naive copy
s1 = '.....123451234512345' <- s1 after the naive copy - not what we wanted
s2 = '123451234512345.....' <- s2 after the naive copy
在这种情况下,我们需要先复制数组的最后一个字节,然后可能会向后移动。代码看起来像:
void mem_copy_2(void *s1, const void *s2, size_t n)
{
if (((unsigned) s1) < ((unsigned) s2))
for (int i=0; i<n; i++)
s1[i] = s2[i];
else
for (int i=(n-1); i>=0; i--)
s1[i] = s2[i];
return;
}
很容易看出restrict
修饰符如何为更好的速度优化提供机会,消除额外的代码,以及if-else决策。
与此同时,这种情况对于不谨慎的程序员来说是危险的,程序员将重叠数组传递给restrict
- ed函数。在这种情况下,没有防护装置可以确保正确复制阵列。根据编译器选择的优化路径,结果是未定义的。
第一个用例(init()
函数)可以看作是第二个用例的变体,如上所述。这里,使用单个动态内存分配调用创建两个数组。
将两个指针指定为restrict
- ed可以实现优化,否则指令顺序就会很重要。例如,如果我们有代码:
a1[5] = 4;
a2[3] = 8;
然后,如果发现它有用,优化器可以重新排序这些语句。
OTOH,如果指针不 restrict
- ed,那么第一次分配将在第二次分配之前执行是很重要的。这是因为a1[5]
和a2[3]
实际上可能是相同的内存位置!很容易看出,在这种情况下,那里的结束值应该是8.如果我们重新排序指令,那么结束值将是4!
同样,如果给这个restrict
- ed假定代码赋予非相交指针,则结果是未定义的。