212

我一直不确定,C++ 中的 restrict 关键字是什么意思?

这是否意味着赋予函数的两个或多个指针不重叠?还有什么意思?

4

7 回答 7

153

在他的论文Memory Optimization中,Christer Ericson 说虽然restrict它还不是 C++ 标准的一部分,但许多编译器都支持它,他建议在可用时使用它:

限制关键字

!新 1999 ANSI/ISO C 标准

!尚不在 C++ 标准中,但被许多 C++ 编译器支持

!只是一个提示,所以可能什么都不做但仍然符合

限制限定的指针(或引用)...

!...基本上是对编译器的承诺,对于指针的范围,指针的目标将只能通过该指针(以及从它复制的指针)访问。

在支持它的 C++ 编译器中,它的行为可能与 C 中的相同。

有关详细信息,请参阅此 SO 帖子: C99 'restrict' 关键字的实际用法?

花半小时浏览一下 Ericson 的论文,很有趣,值得花时间。

编辑

我还发现 IBM 的AIX C/C++ 编译器支持__restrict__关键字.

g++ 似乎也支持这一点,因为以下程序可以在 g++ 上干净地编译:

#include <stdio.h>

int foo(int * __restrict__ a, int * __restrict__ b) {
    return *a + *b;
}

int main(void) {
    int a = 1, b = 1, c;

    c = foo(&a, &b);

    printf("c == %d\n", c);

    return 0;
}

我还发现了一篇关于使用的不错的文章restrict

揭秘 Restrict 关键字

编辑2

我遇到了一篇专门讨论在 C++ 程序中使用限制的文章:

加载命中存储和 __restrict 关键字

此外,Microsoft Visual C++也支持__restrict关键字.

于 2009-12-27T08:22:11.827 回答
131

正如其他人所说,它从 C++14 开始没有任何意义,所以让我们考虑__restrict__与 C99 相同的 GCC 扩展restrict

C99

restrict表示两个指针不能指向重叠的内存区域。最常见的用法是用于函数参数。

这限制了函数的调用方式,但允许更多的编译优化。

如果调用者不遵守restrict合同,可能会发生未定义的行为。

C99 N1256 草案6.7.3 /7“类型限定符”说:

限制限定符(如寄存器存储类)的预期用途是促进优化,并且从构成符合程序的所有预处理翻译单元中删除限定符的所有实例不会改变其含义(即,可观察的行为)。

和 6.7.3.1 “限制的正式定义”给出了血淋淋的细节。

可能的优化

维基百科的例子很有启发性。

它清楚地显示了它如何允许保存一条汇编指令

无限制:

void f(int *a, int *b, int *x) {
  *a += *x;
  *b += *x;
}

伪组装:

load R1 ← *x    ; Load the value of x pointer
load R2 ← *a    ; Load the value of a pointer
add R2 += R1    ; Perform Addition
set R2 → *a     ; Update the value of a pointer
; Similarly for b, note that x is loaded twice,
; because x may point to a (a aliased by x) thus 
; the value of x will change when the value of a
; changes.
load R1 ← *x
load R2 ← *b
add R2 += R1
set R2 → *b

有限制:

void fr(int *restrict a, int *restrict b, int *restrict x);

伪组装:

load R1 ← *x
load R2 ← *a
add R2 += R1
set R2 → *a
; Note that x is not reloaded,
; because the compiler knows it is unchanged
; "load R1 ← *x" is no longer needed.
load R2 ← *b
add R2 += R1
set R2 → *b

海湾合作委员会真的做到了吗?

g++4.8 Linux x86-64:

g++ -g -std=gnu++98 -O0 -c main.cpp
objdump -S main.o

-O0,它们是相同的。

-O3

void f(int *a, int *b, int *x) {
    *a += *x;
   0:   8b 02                   mov    (%rdx),%eax
   2:   01 07                   add    %eax,(%rdi)
    *b += *x;
   4:   8b 02                   mov    (%rdx),%eax
   6:   01 06                   add    %eax,(%rsi)  

void fr(int *__restrict__ a, int *__restrict__ b, int *__restrict__ x) {
    *a += *x;
  10:   8b 02                   mov    (%rdx),%eax
  12:   01 07                   add    %eax,(%rdi)
    *b += *x;
  14:   01 06                   add    %eax,(%rsi) 

对于初学者,调用约定是:

  • rdi= 第一个参数
  • rsi= 第二个参数
  • rdx= 第三个参数

GCC 输出比 wiki 文章更清晰:4 条指令与 3 条指令。

数组

到目前为止,我们节省了单条指令,但是如果指针表示要循环的数组,这是一个常见的用例,那么可以节省一堆指令,正如supercatmichael所提到的。

考虑例如:

void f(char *restrict p1, char *restrict p2, size_t size) {
     for (size_t i = 0; i < size; i++) {
         p1[i] = 4;
         p2[i] = 9;
     }
 }

因为restrict,智能编译器(或人类)可以将其优化为:

memset(p1, 4, size);
memset(p2, 9, size);

哪个可能更有效,因为它可能在体面的 libc 实现(如 glibc)上进行了汇编优化 在性能方面使用 std::memcpy() 或 std::copy() 更好吗?,可能带有SIMD 指令

如果没有限制,则无法进行此优化,例如考虑:

char p1[4];
char *p2 = &p1[1];
f(p1, p2, 3);

然后for版本制作:

p1 == {4, 4, 4, 9}

memset版本使:

p1 == {4, 9, 9, 9}

海湾合作委员会真的做到了吗?

GCC 5.2.1.Linux x86-64 Ubuntu 15.10:

gcc -g -std=c99 -O0 -c main.c
objdump -dr main.o

-O0两者是一样的。

-O3

  • 有限制:

    3f0:   48 85 d2                test   %rdx,%rdx
    3f3:   74 33                   je     428 <fr+0x38>
    3f5:   55                      push   %rbp
    3f6:   53                      push   %rbx
    3f7:   48 89 f5                mov    %rsi,%rbp
    3fa:   be 04 00 00 00          mov    $0x4,%esi
    3ff:   48 89 d3                mov    %rdx,%rbx
    402:   48 83 ec 08             sub    $0x8,%rsp
    406:   e8 00 00 00 00          callq  40b <fr+0x1b>
                            407: R_X86_64_PC32      memset-0x4
    40b:   48 83 c4 08             add    $0x8,%rsp
    40f:   48 89 da                mov    %rbx,%rdx
    412:   48 89 ef                mov    %rbp,%rdi
    415:   5b                      pop    %rbx
    416:   5d                      pop    %rbp
    417:   be 09 00 00 00          mov    $0x9,%esi
    41c:   e9 00 00 00 00          jmpq   421 <fr+0x31>
                            41d: R_X86_64_PC32      memset-0x4
    421:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)
    428:   f3 c3                   repz retq
    

    两个memset电话符合预期。

  • 没有限制:没有 stdlib 调用,只有 16 次迭代宽循环展开,我不打算在这里重现 :-)

我没有耐心对它们进行基准测试,但我相信限制版本会更快。

严格的别名规则

restrict关键字仅影响兼容类型的指针(例如 two int*),因为严格的别名规则表明别名不兼容的类型默认情况下是未定义的行为,因此编译器可以假定它不会发生并优化掉。

请参阅:什么是严格的别名规则?

它对参考有用吗?

根据 GCC 文档,它确实:https ://gcc.gnu.org/onlinedocs/gcc-5.1.0/gcc/Restricted-Pointers.html语法:

int &__restrict__ rref

甚至还有一个this成员函数的版本:

void T::fn () __restrict__
于 2015-06-14T09:37:03.800 回答
23

没有什么。它被添加到 C99 标准中。

于 2009-04-22T09:01:59.540 回答
12

是添加此关键字的原始建议。不过,正如迫切指出的那样,这是C99的一项功能。它与 C++ 无关。

于 2009-04-22T09:03:43.140 回答
4

由于某些 C 库的头文件使用关键字,C++ 语言将不得不对此做一些事情.. 至少忽略关键字,因此我们不必将关键字#define 为空白宏来抑制关键字.

于 2009-12-05T17:47:27.817 回答
4

C++ 中没有这样的关键字。C++ 关键字列表可以在 C++ 语言标准的第 2.11/1 节中找到。restrict是 C 语言 C99 版本中的关键字,而不是 C++ 中的关键字。

于 2009-12-27T10:06:42.440 回答
0

有了__restrict__compiler可以进行复杂的优化,因为程序员已经保证__restrict__修饰点指向绝对不会相互重叠的数据范围。pointers

这通常是这种情况,因此为了实现高性能目标,您最多可以在代码中添加一个__restrict__装饰器pointers

于 2022-01-13T14:03:53.710 回答