GCC: оптимизация загрузки и хранения памяти - PullRequest
0 голосов
/ 13 января 2019

РЕДАКТИРОВАТЬ 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];
}

Разница в том, что 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, т. Е. Что указатель P, возвращаемый функцией, не может присвоить псевдониму любой другой указатель, действительный при возврате из функции, и, кроме того, в любом хранилище, адресуемом P, нет указателей на действительные объекты. .

Использование этого атрибута может улучшить оптимизацию. Компилятор предсказывает, что функция с атрибутом в большинстве случаев возвращает ненулевое значение. Такие функции, как 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 голосов
/ 13 января 2019

Для 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]; ...).

  • пожать плечами и сказать: «О, это жаль» и ничего не делать

0 голосов
/ 13 января 2019

РЕДАКТИРОВАТЬ: Обсуждение атрибута 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.

0 голосов
/ 13 января 2019

Обе функции имеют побочные эффекты, и чтение и сохранение в памяти не могут быть оптимизированы

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

этот код будет оптимизирован для простого возврата

Вы можете пообещать компилятору, что ничего не произойдет, чтобы упростить оптимизацию, используя ограничение по ключевым словам. Но, конечно, ваш код должен выполнять это обещание.

...