下面,我将参考问题中链接到的 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
,那么首先写入低字节将超出源的最后 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 假定代码,则结果是未定义的。