为什么gcc和clang没有将strlen从这个循环中提升出来?

时间:2018-01-28 00:31:57

标签: c gcc clang compiler-optimization hoisting

请考虑以下代码:

nameData <- "email ID
Alicia@gmail.com 1
Jane@aol.com 2
Thomas@msn.com 3
Henry@yale.edu 4
LaShawn@uga.edu 5
"
examData <- "email exam1 exam2 exam3
Alicia@gmail.com 98 77 87
Jane@aol.com 99 88 93
Thomas@msn.com 73 62 73
Henry@yale.edu 100 98 99
LaShawn@uga.edu 84 98 92"

names <- read.table(text=nameData,header=TRUE,stringsAsFactors=FALSE)
exams <- read.table(text=examData,header=TRUE,stringsAsFactors=FALSE)
# merge data and drop email, which is first column 
mergedData <- merge(names,exams)[,-1]
mergedData[order(mergedData$ID),] 

我希望> mergedData[order(mergedData$ID),] ID exam1 exam2 exam3 1 1 98 77 87 3 2 99 88 93 5 3 73 62 73 2 4 100 98 99 4 5 84 98 92 > 在这些基本理想的条件下被提升出来;然而 - it isn't, neither by clang 5.0 nor by gcc 7.3具有最大优化(#include <string.h> void bar(char c); void foo(const char* __restrict__ ss) { for (int i = 0; i < strlen(ss); ++i) { bar(*ss); } } )。

为什么会这样?

注意:灵感来自(我的答案)this question

3 个答案:

答案 0 :(得分:5)

其他答案声称无法提升strlen来电,因为字符串的内容可能会在来电之间发生变化。这些答案没有恰当地说明restrict的语义;即使bar通过全局变量或其他机制访问了字符串,restrict指向const类型的指针的语义也应该(参见警告)禁止bar修改字符串。

来自C11, N1570 draft, 6.7.3.1

  

1设D是提供a的普通标识符的声明   将对象P指定为限制限定指针的方法   输入T。

     

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

     

3在下文中,指针表达式E被认为是基于   对象P if(在B之前执行B的某个序列点)   评估E)修改P以指向数组对象的副本   它以前指出的将改变E的值。 137)注意   基于&#39;&#39;&#39;&#39;&#39;&#39;仅为具有指针类型的表达式定义。

     

4在每次执行B期间,让L为任何具有&amp; L基础的左值   P.如果L用于访问它的对象X的值   指定,并且X也被修改(通过任何方式),然后是以下   要求适用:T不得符合要求。每个其他左值   用于访问X的值也应具有基于P的地址。   修改X的每个访问也应被视为修改P,for   本条款的目的。如果P被赋值为a的值   指针表达式E基于另一个受限制的指针   对象P2,与块B2相关联,然后执行B2   应在执行B之前开始,或者执行B2   在转让之前结束。如果不满足这些要求,那么   行为未定义。

     

5这里执行B意味着执行部分   与标量对象的生命周期相对应的程序   与B相关的类型和自动存储持续时间。

此处,声明Dconst char* __restrict__ ss,关联的块Bfoo的正文。 strlen访问字符串的所有左值都基于&L (请参阅警告) ss,并且这些访问在执行B期间发生(因为,根据第5节的定义,strlen的执行是B的执行的一部分。 ss指向const限定类型,因此在第4节中,允许编译器假定strlen访问的字符串元素在执行foo期间不被修改;修改它们将是未定义的行为。

(警告)上述分析假设strlen通过&#34;普通&#34;来访问字符串。指针解除引用或索引。如果strlen使用SSE内在函数或内联汇编等技术,我不清楚这种访问在技术上是否计算为使用左值来访问它指定的对象的值。如果它们不算这样,restrict的保护可能不适用,编译器可能无法执行吊装。

上述警告可能会使restrict的保护无效。也许编译器对strlen的定义知之甚少,无法分析它与restrict的交互(我很惊讶它没有被内联)。也许编译器可以自由地执行吊装并且没有意识到它;可能没有实现某些相关优化,或者它无法在正确的编译器组件之间传播必要的信息。确定确切的原因需要更多地了解GCC和Clang内部结构。

消除strlen

Further-simplified tests和循环显示Clang肯定对限制指针到const的优化有一些支持,但我无法从GCC观察到任何此类支持

答案 1 :(得分:2)

由于strlen正在传递指针,并且它指向的内存的内容可能会在调用strlen之间发生变化,因此优化调用可能会引入错误。如果你可以向gcc保证函数将始终返回相同的值,它将优化它。来自documentation

  

<强>常量

     

许多函数不检查除参数之外的任何值,除了返回值之外没有任何效果。对这些函数的调用有助于优化,例如公共子表达式消除。 const属性对函数的定义施加了比下面类似的纯属性更大的限制,因为它禁止函数读取全局变量。因此,函数声明中存在属性允许GCC为函数的某些调用发出更有效的代码。诊断出使用const和pure属性装饰相同的函数。

因此,取消对strlen的外部依赖关系,请查看以下两个编译中的差异:

int baz (const char* s) __attribute__ ((pure));

void foo(const char* __restrict__ ss)
{
    for (int i = 0; i < baz(ss); ++i)
        bar(*ss);
}

收率:

foo:
        push    rbp
        push    rbx
        mov     rbp, rdi
        xor     ebx, ebx
        sub     rsp, 8
        jmp     .L2
.L3:
        movsx   edi, BYTE PTR [rbp+0]
        add     ebx, 1
        call    bar
.L2:
        mov     rdi, rbp
        call    baz
        cmp     eax, ebx
        jg      .L3
        add     rsp, 8
        pop     rbx
        pop     rbp
        ret

但是,如果我们将pure上的baz属性更改为const,您可以看到呼叫已从循环中提升:

foo:
        push    r12
        push    rbp
        mov     r12, rdi
        push    rbx
        xor     ebx, ebx
        call    baz
        mov     ebp, eax
        jmp     .L2
.L3:
        movsx   edi, BYTE PTR [r12]
        add     ebx, 1
        call    bar
.L2:
        cmp     ebp, ebx
        jg      .L3
        pop     rbx
        pop     rbp
        pop     r12
        ret

因此,可能会查看头文件并查看strlen的声明方式。

答案 2 :(得分:1)

ss可能是某些全局变量,因为您可以使用foo之类的全局数组作为其参数调用char str[100];(例如,通过foo(str);中的main ...

bar可以修改该全局变量(然后strlen(ss)可能会在每个循环中发生变化)。

BTW restrict可能没有你所相信的意思。仔细阅读C11标准的§6.7.3部分和§6.7.3.1。恕我直言restrict实际上对两个相同功能的正式论证大多有用,以表达它们不是&#34;别名&#34;或&#34;重叠&#34;指针(如果你猜到我的意思),也许restrict的优化工作可能已经集中在这些情况上。

也许(但不太可能),在您的特定程序中,如果您invoke将其gcc -flto -fwhole-program -O3(在每个翻译单元上,并且在节目链接时间)。我不打赌(但我让你去检查)。

  

为什么会这样?

至于为什么当前的GCC(或Clang)没有像您希望的那样进行优化,这是因为 nobody编写了这样的优化传递并启用了它在-O3

编译器不是必需的进行优化,只需允许执行其中的一些(选择其实现者)。

由于它是免费软件,随时可以向{GCC (或Clang)提出contributing补丁。您可能需要一整年的工作,并且您不确定您的优化是否会被接受(因为在实践中没有像您显示的任何代码,或者因为您的优化太具体,所以不太可能被触发,但仍然会减慢编译器)。但欢迎您尝试。

即使§6.7.3.1允许您进行优化(如answer by user2357112所示),实际上也不值得付出努力。

(我的直觉是实施这样的优化是 hard ,并且在实践中不会对现有的程序带来太多利润)

顺便说一下,你绝对可以通过编写一些GCC plugin来编写来实验这样的优化(因为插件框架是为这样的实验而设计的)。您可能会发现编码这样的优化是很多工作,实际上它并没有提高大多数现有程序的性能(例如在Linux发行版中),因为很少有人以这种方式编码。

GCC和Clang都是自由软件项目,他们的贡献者(从FSF的角度来看)是志愿者。所以随意改进GCC (或Clang)就像你想要优化一样。根据过去的经验,即使为GCC贡献一小段代码也需要花费很多时间。 GCC是一个庞大的程序(大约一千万行代码),所以理解它的内部结构并不容易。