C99'conctrict'关键字的实际用法?

use*_*052 176 c gcc c99 restrict-qualifier

我正在浏览一些文档和问题/答案,并看到它提到.我读了一个简短的描述,声明它基本上是程序员的承诺,指针不会用于指向其他地方.

任何人都可以提供一些现实案例,其值得实际使用吗?

Mic*_*ael 174

restrict说指针是唯一访问底层对象的东西.它消除了指针别名的可能性,使编译器能够更好地进行优化.

例如,假设我有一台具有专用指令的机器,可以在内存中乘以数字向量,我有以下代码:

void MultiplyArrays(int* dest, int* src1, int* src2, int n)
{
    for(int i = 0; i < n; i++)
    {
        dest[i] = src1[i]*src2[i];
    }
}
Run Code Online (Sandbox Code Playgroud)

编译器需要正确处理if dest,src1src2重叠,这意味着它必须从开始到结束一次进行一次乘法运算.通过使用restrict,编译器可以使用向量指令自由地优化此代码.

维基百科在这里有一个条目restrict,另一个例子.

  • limit 通常仅在指向已修改的对象时才有效,在这种情况下,它断言不需要考虑隐藏的副作用。大多数编译器使用它来促进矢量化。为此,Msvc 使用运行时检查来检查数据重叠。 (3认同)
  • @Michael - 如果我没有误会,那么只有当`dest`与任何一个源向量重叠时才会出现问题.如果`src1`和`src2`重叠,为什么会出现问题? (2认同)
  • 实际上,register关键字仅是建议性的。从2000年左右开始,在编译器中,无论您是否使用register关键字,示例中的i(以及用于比较的n)都将被优化为一个寄存器。 (2认同)
  • 对于像我这样想知道“指针别名”到底意味着什么的人,康奈尔大学在这里写了相关内容:https://cvw.cac.cornell.edu/vector/coding_aliasing。 (2认同)

Cir*_*四事件 136

维基百科例子非常照明.

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

没有限制:

void f(int *a, int *b, int *x) {
  *a += *x;
  *b += *x;
}
Run Code Online (Sandbox Code Playgroud)

伪装配:

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 a may be equal to x.
load R1 ? *x
load R2 ? *b
add R2 += R1
set R2 ? *b
Run Code Online (Sandbox Code Playgroud)

有限制:

void fr(int *restrict a, int *restrict b, int *restrict x);
Run Code Online (Sandbox Code Playgroud)

伪装配:

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
load R2 ? *b
add R2 += R1
set R2 ? *b
Run Code Online (Sandbox Code Playgroud)

海湾合作委员会真的这样做吗?

GCC 4.8 Linux x86-64:

gcc -g -std=c99 -O0 -c main.c
objdump -S main.o
Run Code Online (Sandbox Code Playgroud)

-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) 
Run Code Online (Sandbox Code Playgroud)

对于没有经验的人,调用约定是:

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

GCC输出甚至比wiki文章更清晰:4条指令vs 3条指令.

数组

到目前为止,我们有单指令节省,但如果指针表示要循环的数组,一个常见的用例,那么可以保存一堆指令,如supercat所述.

考虑例如:

void f(char *restrict p1, char *restrict p2) {
    for (int i = 0; i < 50; i++) {
        p1[i] = 4;
        p2[i] = 9;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

memset(p1, 4, 50);
memset(p2, 9, 50);
Run Code Online (Sandbox Code Playgroud)

这可能更高效,因为它可能在一个体面的libc实现(如glibc)上进行程序集优化:在性能方面使用std :: memcpy()或std :: copy()会更好吗?

海湾合作委员会真的这样做吗?

GCC 5.2.1.Linux x86-64 Ubuntu 15.10:

gcc -g -std=c99 -O0 -c main.c
objdump -dr main.o
Run Code Online (Sandbox Code Playgroud)

-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
    
    Run Code Online (Sandbox Code Playgroud)

    两个memset电话如预期.

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

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

C99

让我们看一下标准的完整性.

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

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

如果调用者不遵循restrict合同,则定义未定义的行为.

C99 N1256草案 6.7.3/7 "类型的限定"说:

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

和6.7.3.1"限制的正式定义"给出了血淋淋的细节.

严格别名规则

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

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

也可以看看

  • "限制"限定符实际上可以节省更多.例如,给定`void zap(char*restrict p1,char*restrict p2){for(int i = 0; i <50; i ++){p1 [i] = 4; p2 [i] = 9; 限制限定符会让编译器将代码重写为"memset(p1,4,50); memset(p2,9,50);".限制远远优于基于类型的别名; 令人遗憾的是编译器更关注后者. (8认同)
  • @tim18:将反引号中包含双下划线的内容括起来,如`__restrict`。否则双下划线可能会被误解为表示您在大喊大叫。 (4认同)
  • 比不喊更重要的是,下划线的含义与您要表达的观点直接相关。 (4认同)
  • @ tim18:“ restrict”关键字可以启用许多优化,即使基于类型的优化也无法实现。此外,语言中“限制”的存在(与基于类型的积极混叠不同)永远不会像没有任务时那样高效地完成任务(因为代码可能会被“限制”破坏不使用它,而经常被攻击性TBAA破坏的代码通常必须以较低效率的方式重写)。 (2认同)

归档时间:

查看次数:

44799 次

最近记录:

7 年,5 月 前