通过示例了解限制限定符

restrict关键字的行为在C99中由6.7.3.1定义:

设D是普通标识符的声明,它提供了一种将对象P指定为类型T的限制限定指针的方法。

如果D出现在块内并且没有存储类extern,则让B表示该块。 如果D出现在函数定义的参数声明列表中,则让B表示关联的块。 否则,让B表示main的块(或者在独立环境中的程序启动时调用任何函数的块)。

在下文中,指针表达式E被称为基于对象P if(在评估E之前执行B中的某个序列点)修改P以指向其先前指向的数组对象的副本将改变E.119的值)注意”based”仅针对具有指针类型的表达式定义。

在每次执行B期间,让L为任何具有&L基于P的左值。如果L用于访问它指定的对象X的值,并且X也被修改(通过任何方式),则以下要求适用:T不应该是const限定的。 用于访问X值的每个其他左值也应具有基于P的地址。出于本子条款的目的,每次修改X的访问也应被视为修改P. 如果P被赋予指针表达式E的值,该指针表达式E基于与块B2相关联的另一个受限指针对象P2,则B2的执行应在执行B之前开始,或者B2的执行应在该执行之前结束。分配。 如果不满足这些要求,则行为未定义。

就像其他人一样,我很难理解这个定义的所有复杂性。 作为这个问题的答案,我希望看到第4段中每个要求违反要求的一些好例子。 本文:

http://web.archive.org/web/20120225055041/http://developers.sun.com/solaris/articles/cc_restrict.html

在“编译器可能假设……”方面做得很好。 扩展该模式并将编译器可以做出的假设以及它们如何无法保持,每个示例都很棒。

下面,我将参考问题中链接的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 

s2 = '....................1234567890abcde' <- s2 before the naive copy
s1 = '1234567890abcde....................' <- s1 after the naive copy
s2 = '....................1234567890abcde' <- s2 after the naive copy

OTOH,在第二function中,可能存在重叠。 在这种情况下,我们需要检查源数组是否位于目标之前,反之亦然,并相应地选择循环索引边界。

例如,假设s1 = 100s2 = 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 = 105s2 = 100 ,则首先写入较低字节将超出源s2的最后10个字节,并且最终会出现错误的副本。

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=0; i--) s1[i] = s2[i]; return; } 

很容易看出restrict修饰符如何为更好的速度优化提供机会,消除了额外的代码,以及if-else决策。

同时,这种情况对于不合规的程序员来说是危险的,程序员将重叠数组传递给restrict -ed函数。 在这种情况下,没有防护装置可以确保正确复制arrays。 根据编译器选择的优化路径,结果是未定义的。


第一个用例( init()函数)可以看作是第二个用例的变体,如上所述。 这里,使用单个动态内存分配调用创建两个数组。

将两个指针指定为restrict -ed可以实现指令顺序无关紧要的优化。 例如,如果我们有代码:

 a1[5] = 4; a2[3] = 8; 

然后,如果发现它有用,优化器可以重新排序这些语句。

OTOH,如果指针没有被 restrict ,那么第一个赋值将在第二个赋值之前执行是很重要的。 这是因为a1[5]a2[3]实际上可能是相同的内存位置! 很容易看出,在这种情况下,那里的结束值应该是8.如果我们重新排序指令,那么结束值将是4!

同样,如果给这个restrict假定代码赋予非不相交指针,则结果是未定义的。