Подтвердить что ты не робот

Что такое псевдонимы и как это влияет на производительность?

В событии GoingNative во время Интерактивная панель в День2 с отметкой в ​​9 минут Чандлер Каррут говорит:

Указатели создают проблемы с псевдонимом. Они замедляют ваши двоичные файлы, а не ускоряют их.

Что это значит? Можно ли это проиллюстрировать (простым) примером?

4b9b3361

Ответ 1

Алиасинг влияет на производительность, не позволяя компилятору выполнять определенные оптимизации. Например:

void foo(int *array,int *size,int *value) {
    for(int i=0;i<*size;++i) {
        array[i] = 2 * *value;
    }
}

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

Если переменные не могли быть псевдонимом, тогда приведенный выше код будет эквивалентен следующему:

void foo(int *array,int size,int value) {
    for(int i=0;i<size;++i) {
        array[i] = 2 * value;
    }
}

Используя LLVM онлайн-демонстрацию, чтобы получить сгенерированный код, вот разные результаты:

1) С псевдонимом

foo:                                    # @foo
    .cfi_startproc
# BB#0:
    cmpl    $0, (%rsi)
    jle .LBB0_3
# BB#1:
    xorl    %eax, %eax
    .align  16, 0x90
.LBB0_2:                                # %.lr.ph
                                        # =>This Inner Loop Header: Depth=1
    movl    (%rdx), %ecx
    addl    %ecx, %ecx
    movl    %ecx, (%rdi,%rax,4)
    incq    %rax
    cmpl    (%rsi), %eax
    jl  .LBB0_2
.LBB0_3:                                # %._crit_edge
    ret
    .size   foo, .Ltmp1-foo
    .cfi_endproc
.Leh_func_end0:

2) Без сглаживания

foo:                                    # @foo
    .cfi_startproc
# BB#0:
    testl   %esi, %esi
    jle .LBB0_3
# BB#1:                                 # %.lr.ph
    addl    %edx, %edx
    .align  16, 0x90
.LBB0_2:                                # =>This Inner Loop Header: Depth=1
    movl    %edx, (%rdi)
    addq    $4, %rdi
    decl    %esi
    jne .LBB0_2
.LBB0_3:                                # %._crit_edge
    ret
    .size   foo, .Ltmp1-foo
    .cfi_endproc
.Leh_func_end0:

Вы можете видеть, что версия с псевдонимом должна делать больше работы в теле цикла (между метками LBB0_2 и LBB0_3).

Ответ 2

Тип проблемы, о которой говорил Чендлер, можно легко проиллюстрировать упрощенным strcpy:

char *stpcpy (char * dest, const char * src);

При написании этой реализации вы можете предположить, что память, на которую указывает dest, полностью отделена от памяти, на которую указывает src. Компилятор), возможно, захочет оптимизировать его, прочитав блок символов из строки, на которую указывает src, и сразу все записи в dest. Но если dest указал на один байт впереди src, поведение этого будет отличаться от простой посимвольной копии.

Здесь проблема сглаживания заключается в том, что src может иметь псевдоним dest, а сгенерированный код должен быть менее эффективным, чем мог бы быть, если src не было разрешено псевдоним dest.

В реальном strcpy используется дополнительное ключевое слово Restrict (которое технически только часть C, а не С++, которая сообщает компилятору предположить, что src и dest не перекрываются, и это позволяет компилятору генерировать гораздо более эффективный код.


Вот еще более простой пример, где мы видим большую разницу в сборке:

void my_function_1(int* a, int* b, int* c) {
    if (*a) *b = *a;
    if (*a) *c = *a;
}

void my_function_2(int* __restrict a, int* __restrict b, int* __restrict c) {
    if (*a) *b = *a;
    if (*a) *c = *a;
}

Предположим, что это упрощение функции, где на самом деле имеет смысл использовать два оператора if, а не только if (*a) { *b=*a; *c=*a; }, но намерение одинаков.

Мы можем предположить при написании этого, что a != b, потому что есть некоторая причина, почему это не имеет смысла для my_function. Но компилятор не может этого допустить и хранить в памяти b и перезагрузку a из памяти перед выполнением второй строки, чтобы охватить случай, когда b == a:

0000000000400550 <my_function_1>:
  400550:       8b 07                   mov    (%rdi),%eax
  400552:       85 c0                   test   %eax,%eax                 <= if (*a)
  400554:       74 0a                   je     400560 <my_function_1+0x10>
  400556:       89 06                   mov    %eax,(%rsi)
  400558:       8b 07                   mov    (%rdi),%eax
  40055a:       85 c0                   test   %eax,%eax                 <= if (*a)
  40055c:       74 02                   je     400560 <my_function_1+0x10>
  40055e:       89 02                   mov    %eax,(%rdx)
  400560:       f3 c3                   repz retq

Если мы удалим потенциал для сглаживания, добавив __restrict, компилятор генерирует более короткий и быстрый код:

0000000000400570 <my_function_2>:
  400570:       8b 07                   mov    (%rdi),%eax
  400572:       85 c0                   test   %eax,%eax
  400574:       74 04                   je     40057a <_Z9my_function_2PiS_S_+0xa>
  400576:       89 06                   mov    %eax,(%rsi)
  400578:       89 02                   mov    %eax,(%rdx)
  40057a:       f3 c3                   repz retq

Ответ 3

Рассмотрим следующую функцию:

void f(float* lhs, float* rhs, float* out, int size) {
    for(int i = 0; i < size; i++) {
        out[i] = *lhs + *rhs;
    }
}

Какая самая быстрая версия этой функции? Вероятно, вы вытащили *lhs + *rhs из цикла. Проблема заключается в том, что происходит, когда псевдонимы указателей. Представьте себе, что делает эта оптимизация, если я называю это следующим образом:

float arr[6] = { ... };
f(arr, arr + 1, arr, 6);

Как вы можете видеть, проблема в том, что *lhs + *rhs нельзя выровнять из цикла, потому что out[i] изменяет свои значения. Фактически, компилятор не может вытащить любую логику из цикла. Таким образом, компилятор не может выполнить "очевидную" оптимизацию, потому что если параметры псевдонимы логики теперь неверны. Однако, если поплавки берутся по значению, компилятор знает, что они не могут использовать псевдоним и могут выполнять подъем.

Конечно, эта функция довольно глупа, но она демонстрирует точку.

Ответ 4

указатель - это значение, которое представляет адрес памяти, иногда два указателя могут представлять один и тот же адрес памяти, в котором используется псевдонимы

int * p;
*p = 5;

int * alias;
alias = p;

переменная alias является псевдонимом p и *alias равна 5, если вы меняете *alias, а затем *p изменяется вместе с ней