通过示例了解限制限定符

时间:2012-09-04 01:19:38

标签: c c99 strict-aliasing restrict-qualifier

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段中每个要求违反要求的一些好例子。这篇文章:

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

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

1 个答案:

答案 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 = 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个字节,并最终得到错误的副本。< / 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假定代码赋予非相交指针,则结果是未定义的。