GCC:优化内存加载和存储

时间:2019-01-12 22:29:36

标签: c gcc compiler-optimization

编辑1:在此问题的末尾添加了另一个示例(表明GCC原则上可以完成我想实现的目标),并进行了更多讨论。

编辑2::找到了malloc函数属性,应该该做什么。请看问题的结尾。

这是一个有关如何告诉编译器在区域外部不可见存储到内存区域的问题(因此可以进行优化)。为了说明我的意思,让我们看下面的代码

int f (int a)
{
    int v[2];
    v[0] = a;
    v[1] = 0;
    while (v[0]-- > 0)
       v[1] += v[0];
    return v[1];
}

gcc -O2生成以下汇编代码(x https://godbolt.org上的x86-64 gcc,主干):

f:
        leal    -1(%rdi), %edx
        xorl    %eax, %eax
        testl   %edi, %edi
        jle     .L4
.L3:
        addl    %edx, %eax
        subl    $1, %edx
        cmpl    $-1, %edx
        jne     .L3
        ret
.L4:
        ret

正如人们所看到的,优化之后就不再需要对数组v进行加载和存储了。

现在考虑以下代码:

int g (int a, int *v)
{
    v[0] = a;
    v[1] = 0;
    while (v[0]-- > 0)
       v[1] += v[0];
    return v[1];
}

区别在于v没有(栈)分配在函数中,而是作为参数提供。在这种情况下,gcc -O2的结果是:

g:
        leal    -1(%rdi), %edx
        movl    $0, 4(%rsi)
        xorl    %eax, %eax
        movl    %edx, (%rsi)
        testl   %edi, %edi
        jle     .L4
.L3:
        addl    %edx, %eax
        subl    $1, %edx
        cmpl    $-1, %edx
        jne     .L3
        movl    %eax, 4(%rsi)
        movl    $-1, (%rsi)
        ret
.L4:
        ret

很明显,代码必须将v[0]v[1]的最终值存储在内存中,因为它们可能是可见的。

现在,我正在寻找一种告诉编译器在函数v返回后无法再访问第二个示例中的g指向的内存的方法,编译器可以优化内存访问。

举一个更简单的例子:

void h (int *v)
{
    v[0] = 0;
}

如果v返回后无法访问h所指向的内存,则应该可以将函数简化为单个ret

我尝试通过使用严格的别名规则来实现自己想要的功能,但没有成功。

添加到编辑1:

GCC似乎内置了必要的代码,如以下示例所示:

include <stdlib.h>

int h (int a)
{
    int *v = malloc (2 * sizeof (int));
    v[0] = a;
    v[1] = 0;
    while (v[0]-- > 0)
      v[1] += v[0];
    return v[1];
}

生成的代码不包含任何加载和存储:

h:
        leal    -1(%rdi), %edx
        xorl    %eax, %eax
        testl   %edi, %edi
        jle     .L4
.L3:
        addl    %edx, %eax
        subl    $1, %edx
        cmpl    $-1, %edx
        jne     .L3
        ret
.L4:
        ret

换句话说,GCC知道通过v的任何副作用都无法观察到malloc指向的存储区域的改变。为此目的,GCC具有__builtin_malloc

所以我还可以问:用户代码(例如malloc的用户版本)如何利用此功能?

已添加到编辑2:

GCC具有以下功能属性:

  

malloc

     

这告诉编译器函数类似于malloc,即函数返回时,函数返回的指针P不能别名任何其他有效的指针,而且在P寻址的任何存储中都没有指向有效对象的指针。

     

使用此属性可以改善优化。编译器预测,在大多数情况下,具有属性的函数将返回非null。诸如malloc和calloc之类的函数具有此属性,因为它们返回指向未初始化或归零存储的指针。但是,像realloc这样的函数不具有此属性,因为它们可以将指针返回包含指针的存储。

似乎做我想做的事,如以下示例所示:

__attribute__ (( malloc )) int *m (int *h);

int i (int a, int *h) 
{ 
    int *v = m (h);
    v[0] = a;
    v[1] = 0;
    while (v[0]-- > 0)
        v[1] += v[0];
    return v[1];
}

生成的汇编代码没有加载和存储:

i:
        pushq   %rbx
        movl    %edi, %ebx
        movq    %rsi, %rdi
        call    m
        testl   %ebx, %ebx
        jle     .L4
        leal    -1(%rbx), %edx
        xorl    %eax, %eax
.L3:
        addl    %edx, %eax
        subl    $1, %edx
        cmpl    $-1, %edx
        jne     .L3
        popq    %rbx
        ret
.L4:
        xorl    %eax, %eax
        popq    %rbx
        ret

但是,一旦编译器看到m的定义,它可能会忘记该属性。例如,当给出以下定义时就是这种情况:

__attribute__ (( malloc )) int *m (int *h)
{
    return h;
}

在这种情况下,该函数会内联,并且编译器会忽略该属性,从而产生与函数g相同的代码。

P.S。:起初,我认为restrict关键字可能会有所帮助,但事实并非如此。

3 个答案:

答案 0 :(得分:1)

编辑:有关最后添加的noinline属性的讨论。

使用以下函数定义,可以实现我的问题的目标:

__attribute__ (( malloc, noinline )) static void *get_restricted_ptr (void *p)
{
    return p;
}

此函数get_restricted_ptr仅返回其指针参数,但通知编译器该函数返回时返回的指针P不能别名任何其他有效的指针,此外,在P寻址的任何存储中都没有指向有效对象的指针。

此功能的使用在此处演示:

int i (int a, int *h)
{
    int *v = get_restricted_ptr (h);
    v[0] = a;
    v[1] = 0;
    while (v[0]-- > 0)
        v[1] += v[0];
    return;
}

生成的代码不包含加载和存储:

i:
        leal    -1(%rdi), %edx
        xorl    %eax, %eax
        testl   %edi, %edi
        jle     .L6
.L5:
        addl    %edx, %eax
        subl    $1, %edx
        cmpl    $-1, %edx
        jne     .L5
        ret
.L6:
        ret

已添加编辑::如果忽略了noinline属性,则GCC会忽略malloc属性。显然,在这种情况下,该函数首先被内联,这样就不再存在GCC将检查malloc属性的函数调用。 (可以讨论是否应将此行为视为GCC中的错误。)使用noinline属性,该函数不会被内联。然后,由于具有malloc属性,GCC知道对该函数的调用是不必要的,因此将其完全删除。

不幸的是,这意味着当由于malloc属性而导致调用未消除时,将不会内联(平凡的)函数。

答案 1 :(得分:0)

这两个功能都有副作用,并且无法优化内存的读取和存储

void h (int *v)
{
    v[0] = 0;
}

int g (int a, int *v)
{
    v[0] = a;
    v[1] = 0;
    while (v[0]-- > 0)
       v[1] += v[0];
    return v[1];
}

副作用必须在功能范围之外才能观察到。内联函数可能具有其他行为,因为副作用可能必须在封闭代码之外才能观察到。

inline int g (int a, int *v)
{
    v[0] = a;
    v[1] = 0;
    while (v[0]-- > 0)
       v[1] += v[0];
    return v[1];
}

void h(void)
{
    int x[2],y ;

    g(y,x);
}

此代码将被优化为简单的返回结果

您可以向编译器保证不会发生任何事情,可以通过使用关键字limit来简化优化。但是,当然,您的代码必须遵守这一诺言。

答案 2 :(得分:0)

对于C语言,唯一的限制是编译器必须确保代码的行为相同。如果编译器可以证明代码的行为相同,则可以并且将删除存储。

例如,我将其放入https://godbolt.org/中:

void h (int *v)
{
    v[0] = 0;
}

void foo() {
    int v[2] = {1, 2};
    h(v);
}

并告诉它使用GCC 8.2和“ -O3”,并得到以下输出:

h(int*):
        mov     DWORD PTR [rdi], 0
        ret
foo():
        ret

请注意,输出中有函数h()的两个不同版本。如果其他代码(在其他目标文件中)想要使用该功能(并且可能被链接器丢弃),则存在第一个版本。 h()的第二个版本直接内联到foo()中,然后进行了优化,几乎没有任何内容。

如果将代码更改为此:

static void h (int *v)
{
    v[0] = 0;
}

void foo() {
    int v[2] = {1, 2};
    h(v);
}

然后它告诉编译器不需要仅用于与其他目标文件链接的h()版本,因此编译器仅生成h()的第二版本,并且输出变为:

foo():
        ret

当然,所有编译器中的所有优化器都不是完美的-对于更复杂的代码(对于包括不同版本的GCC的不同编译器),结果可能会有所不同(编译器可能无法执行此优化)。这纯粹是编译器优化程序的限制,而不是C本身的限制。

对于编译器的优化器不够好的情况,有4种可能的解决方案:

  • 获得更好的编译器

  • 改进编译器的优化程序(例如,向编译器的开发人员发送一封电子邮件,其中包含一个最小的示例,请您耐心等待)

  • 修改代码,以简化编译器的优化程序(例如,将输入数组复制到本地数组,例如“ void h(int *v) { int temp[2]; temp[0] = v[0]; temp[1] = v[1]; ...)。

  • 耸耸肩,说“哦,可惜”,什么也不做