17

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

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

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

在下文中,如果(在对 E 求值之前执行 B 的某个序列点)修改 P 以指向它以前指向的数组对象的副本,则称指针表达式 E 基于对象 P将改变 E.119 的值)请注意,“基于”仅针对具有指针类型的表达式定义。

在每次执行 B 期间,令 L 为基于 P 具有 &L 的任何左值。如果 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

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

4

1 回答 1

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,那么首先写入低字节将超出源的最后 10 个字节s2,我们最终会得到一个错误的副本。

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 假定代码,则结果是未定义的。

于 2012-09-04T03:05:50.347 回答