在C中使用strict关键字的规则?


69

我试图了解何时以及何时不使用 restrictC中关键字,以及在什么情况下它提供了明显的好处。

阅读了“ Demystifying The Restrict Keyword ”(提供了一些使用经验的规则)后,我得到的印象是,当函数传递指针时,它必须考虑所指向的数据可能重叠的可能性(别名)并将任何其他参数传递到函数中。给定一个功能:

编译器必须c在第二个表达式中重新加载,因为也许bc指向同一位置。由于相同的原因,它还必须等待b存储后才能加载a。然后,它必须等待a存储和必须重新加载b,并c在下一循环的开始。如果您像这样调用函数:

然后您会看到编译器为什么必须这样做。restrict有效使用会告诉编译器您永远都不会这样做,因此它可以删除存储的冗余负载ca之前的负载b

在另一篇SO文章中,Nils Pipenbrinck提供了此方案的一个有效示例,展示了性能优势。

到目前为止,我已经收集到,使用它是一个好主意 restrict传递给不会内联的函数的指针。显然,如果代码是内联的,则编译器可以确定指针没有重叠。

现在,这里对我来说开始变得模糊。

在Ulrich Drepper的论文中,“每个程序员都应该了解内存的知识”,他说:“除非使用限制,否则所有指针访问都是潜在的别名来源”,并且他给出了子矩阵矩阵乘以的具体代码示例,其中用途restrict

但是,无论有没有编译我的示例代码restrict,两种情况下我都得到相同的二进制文件。我正在使用gcc version 4.2.4 (Ubuntu 4.2.4-1ubuntu4)

在下面的代码中,我无法确定的事情是是否需要重写它以更广泛地使用restrict,或者GCC中的别名分析是否很好,以至于能够确定每个参数都没有别名其他。 纯粹出于教育目的,我该如何使用或不使用restrict在代码中物质-为什么?

适用restrict于:

只是删除-DUSE_RESTRICT不使用restrict


快速的答案是:不要。使用另一个类型限定符会使代码难以辨认,并增加难以调试的错误的机会。在大多数情况下,您应该信任编译器来解决这些问题。
GeorgSchölly2010年

17
但是,如果您正在编写一个库,则编译器无法解决该问题,因为它无法识别所有调用者。此外,restrict用于功能参数还用作API用户的文档。
JaakkoK,2010年

18
@gs:考虑到那些为生活编写高度优化的代码的受人尊敬的人都建议使用restrict“最佳实践”,我认为值得尝试了解所涉及的问题。否则我不会问这个问题。
罗伯特·S·巴恩斯

8
很好的问题。建议更改for (int i = n; i<n; ++i)for (int i = 0; i<n; ++i),否则for循环的内容永远不会执行。
chux-恢复莫妮卡2013年

3
@chux三年来第一个注意到错字的人...
Robert S. Barnes

Answers:



14

这是对代码优化器的提示。使用限制可确保它可以将指针变量存储在CPU寄存器中,而不必将指针值的更新刷新到内存中,从而也可以更新别名。

是否利用它在很大程度上取决于优化器和CPU的实现细节。代码优化器已经非常投入用于检测非锯齿,因为它是非常重要的优化。在您的代码中检测到它应该没有问题。


因此,基本上,您是说gcc中的别名分析是如此出色,以至于即使没有使用restrict关键字的提示,它也能够自行检测出没有别名问题?
罗伯特·S·巴恩斯

对。您需要传递double **作为参数,并对其进行更新以引入优化程序无法排除的公然别名。
汉斯·帕桑特

7
但是,如果不知道潜在呼叫者,结果是一样的,所以我怀疑这是这里发生的事
shodanex

3

(实际上,我不知道使用此关键字是否会给您带来显着的优势。由于没有强制性,程序员很容易使用此限定符来犯错,因此优化器无法确定程序员没有“说谎”。 )

当您知道指针A是指向内存某个区域的唯一指针时,也就是说,它没有别名(也就是说,任何其他指针B必然不等于A,B!= A),您可以知道通过使用“ restrict”关键字限定A的类型,这对于优化器来说是事实。

我在这里写过这篇文章:http : //mathdev.org/node/23,并试图证明某些受限指针实际上是“线性的”(如该文章中所述)。


3
不,优化器完全有资格假设程序员没有“撒谎”-如果这样做,那么无论发生什么结果都是程序员的错,而不是编译器的错。
克里斯·斯特拉顿

3

值得注意的是, clang能够通过运行时检查别名来生成代码,并且提供两种代码路径:一种用于可能存在别名的情况,另一种用于明显没有别名的情况。

显然,这取决于指向编译器显眼的数据范围-就像上面的示例中那样。

我认为主要理由是大量使用STL的程序-特别<algorithm>是在难以或不可能引入STL的情况下__restrict限定符的程序。

当然,所有这些都以牺牲代码大小为代价,但是消除了很多潜在的隐患,这些隐患可能导致声明为__restrict开发人员认为不完全不重叠的指针。

如果海湾合作委员会还没有得到这种优化,我会感到惊讶。


1

可能是在这里完成的优化不依赖于没有别名的指针吗?除非在将结果写入res2之前预加载多个mul2元素,否则我看不到任何别名问题。

在显示的第一段代码中,很明显会出现哪种别名问题。在这里还不清楚。

在重读Dreppers文章时,他没有具体说限制可能解决任何问题。甚至还有这个短语:

{理论上,1999年修订版在C语言中引入了strict关键字可以解决该问题。但是,编译器尚未赶上。原因主要是存在太多不正确的代码,它们会误导编译器并导致其生成不正确的目标代码。}

在此代码中,内存访问的优化已在算法内完成。剩余的优化似乎是在附录中提供的矢量化代码中完成的。因此,对于这里介绍的代码,我想没有什么区别,因为没有完成任何依赖于限制的优化。每次指针访问都是别名的来源,但并非每种优化都依赖于别名。

过早的优化是万恶之源,限制关键字的使用应仅限于您正在积极研究和优化的情况,而不是在可能使用的地方使用。


@shodanex:这正是我想知道的。Drepper似乎在他的论文中指出,该代码特别受益于restrict。您可以在此处使用SSE2指令查看同一代码的矢量化版本:lwn.net/Articles/258188。是否有可能他总是一直将其restrict作为自己的最佳实践,而又不想检查此代码是否有任何不同?
罗伯特·S·巴恩斯

在您提到的代码中,仍然存在for(j2 = 0; j2 <SM; j2 + = 2),这是我可以看到发生混叠的地方。但这不容易看到
shodanex

@shodanex:那为什么不rres[j2] += rmul1[k2] * rmul2[j2];出现别名问题呢?假设我打电话给mm(_res, _res, _res)rres[j2]并且rmul2[j2] 无论如何每次都要通过循环重新加载。所以即使&rres[j2] == &rmul2[j2]没关系。现在,如果&rres[j2] == &rmul1[k2]在循环执行的某个时刻怎么办?如果是这样,则rmul1[k2]每次都必须通过循环重新加载,否则它可以进入寄存器。我猜编译器展开循环,看到这种情况永远不会发生,这真是太好了。潜在的混叠无关紧要。
罗伯特·S·巴恩斯

@shodanex:因此,基本上,Drepper始终坚持restrict最佳做法(考虑到他将其放在代码的矢量化版本中),并且只是不费心去检查它是否与该特定代码无关。反正这就是我的样子。
罗伯特·S·巴恩斯


0

您正在32或64位Ubuntu上运行吗?如果是32位,则需要添加-march=core2 -mfpmath=sse(或任何处理器体系结构),否则它不使用SSE。其次,为了通过GCC 4.2启用矢量化,您需要添加该-ftree-vectorize选项(从4.3或4.4开始,默认情况下已包括在内-O3)。可能还需要添加-ffast-math为了允许编译器对浮点运算重新排序(或提供松弛浮点语义的另一个选项)。

另外,添加-ftree-vectorizer-verbose=1选项以查看它是否能够对循环进行矢量化处理。这是检查添加strict关键字效果的简便方法。


我在带有-march = native的奔腾M上使用32位Ubuntu。我添加了快速数学。我发布的代码不使用SSE。使用建议的选项似乎无济于事。在两种情况下,我仍然都获得相同的二进制文件。
罗伯特·S·巴恩斯

确实如此,看来如此。我确定您也可以通过-ftree-vectorizer-verbose =选项看到矢量化失败的原因。这就解释了为什么Drepper不得不对代码的矢量化版本求助于内在函数。因此,您需要提出另一个示例。
janneb 2010年

0

您的示例代码的问题在于,编译器将内联该调用,并发现示例中永远不会出现别名。我建议您删除main()函数并使用-c对其进行编译。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.