代码之家  ›  专栏  ›  技术社区  ›  Marc

GCC:优化内存加载和存储

  •  4
  • Marc  · 技术社区  · 6 年前

    编辑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,on 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];
    }
    

    不同的是 在这种情况下:

    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 已返回,以便编译器可以优化内存访问。

    举个更简单的例子:

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

    之后无法访问 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知道 不能通过药物的任何副作用观察到 马洛克 __builtin_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;
    }
    

    在这种情况下,函数是内联的,编译器会忘记属性,从而生成与函数相同的代码 .

    附笔。: 起初,我认为 restrict 关键字可能有帮助,但似乎不是这样。

    3 回复  |  直到 4 年前
        1
  •  1
  •   Marc    6 年前

    编辑: 讨论 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
    

    在编辑中添加: 如果 无线 malloc 马洛克 无线 属性,函数不会内联。那么,由于 马洛克 属性,GCC理解对该函数的调用是不必要的,并将其完全删除。

    不幸的是,这意味着(平凡的)函数不会被内联,因为 马洛克 属性。

        2
  •  0
  •   0___________    6 年前

    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);
    }
    

    这段代码将被优化为一个简单的返回

        3
  •  0
  •   Brendan    6 年前

    对于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() 在输出中 . 第一个版本存在于其他代码(在其他对象文件中)想要使用该函数的情况下(并且可能被链接器丢弃)。第二版 直接嵌入到 foo() 然后优化到完全没有。

    如果将代码更改为:

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

    然后它告诉编译器 不需要只用于链接其他对象文件的,因此编译器只生成第二个版本的 h()

    foo():
            ret
    

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

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

    • 找一个更好的编译器

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