GCC:优化内存负载和存储

编辑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生成以下汇编代码(x86-64 gcc,trunk,在https://godbolt.org上):

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不是在函数中分配(stack-),而是作为参数提供.在这种情况下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]的最终值存储在内存中,因为它们可以被观察到.

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

有一个更简单的例子:

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

如果在h返回后无法访问v指向的内存,则应该可以将函数简化为单个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

This tells the compiler that a function is malloc-like, i.e., that the pointer P returned by the function cannot alias any other pointer valid when the function returns, and moreover no pointers to valid objects occur in any storage addressed by P.

Using this attribute can improve optimization. Compiler predicts that a function with the attribute returns non-null in most cases. Functions like malloc and calloc have this property because they return a pointer to uninitialized or zeroed-out storage. However, functions like realloc do not have this property, as they can return a pointer to storage containing pointers.

它似乎做我想要的,如下例所示:

__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关键字可能有所帮助,但似乎并非如此.

最佳答案 编辑:讨论最后添加的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属性而没有消除其调用时,不会内联(普通)函数.

点赞