Предоставляет ли ключевое слово ограничения значительные преимущества в gcc/g++?

Кто-нибудь когда-либо видел какие-либо цифры/анализ того, действительно ли использование ключевого слова restrict C/C++ в gcc/g++ обеспечивает любое значительное повышение производительности в реальности (а не только в теории)?

Я читал различные статьи, рекомендующие/унижающие его использование, но я не сталкивался с какими-либо реальными цифрами, практически демонстрируя аргументы сторон.

РЕДАКТИРОВАТЬ

Я знаю, что restrict не является официально частью C++, но оно поддерживается некоторыми компиляторами, и я прочитал статью Кристера Эриксона, которая настоятельно рекомендует ее использовать.

Ответ 1

Ключевое слово ограничения имеет значение.

В некоторых ситуациях я видел улучшения фактора 2 и более (обработка изображений). В большинстве случаев разница не такая большая. Около 10%.

Вот небольшой пример, иллюстрирующий разницу. В качестве теста я написал базовое векторное преобразование 4x4 vector *. Обратите внимание, что мне нужно заставить функцию не встраиваться. В противном случае GCC обнаруживает, что в моем контрольном коде нет указателей на псевдонимы, и ограничение не повлияет на вложение.

Я мог бы переместить функцию преобразования в другой файл.

#include <math.h>

#ifdef USE_RESTRICT
#else
#define __restrict
#endif


void transform (float * __restrict dest, float * __restrict src, 
                float * __restrict matrix, int n) __attribute__ ((noinline));

void transform (float * __restrict dest, float * __restrict src, 
                float * __restrict matrix, int n)
{
  int i;

  // simple transform loop.

  // written with aliasing in mind. dest, src and matrix 
  // are potentially aliasing, so the compiler is forced to reload
  // the values of matrix and src for each iteration.

  for (i=0; i<n; i++)
  {
    dest[0] = src[0] * matrix[0] + src[1] * matrix[1] + 
              src[2] * matrix[2] + src[3] * matrix[3];

    dest[1] = src[0] * matrix[4] + src[1] * matrix[5] + 
              src[2] * matrix[6] + src[3] * matrix[7];

    dest[2] = src[0] * matrix[8] + src[1] * matrix[9] + 
              src[2] * matrix[10] + src[3] * matrix[11];

    dest[3] = src[0] * matrix[12] + src[1] * matrix[13] + 
              src[2] * matrix[14] + src[3] * matrix[15];

    src  += 4;
    dest += 4;
  }
}

float srcdata[4*10000];
float dstdata[4*10000];

int main (int argc, char**args)
{
  int i,j;
  float matrix[16];

  // init all source-data, so we don't get NANs  
  for (i=0; i<16; i++)   matrix[i] = 1;
  for (i=0; i<4*10000; i++) srcdata[i] = i;

  // do a bunch of tests for benchmarking. 
  for (j=0; j<10000; j++)
    transform (dstdata, srcdata, matrix, 10000);
}

Результаты: (на моем 2 ГГц Core Duo)

[email protected]:~$ gcc -O3 test.c
[email protected]:~$ time ./a.out

real    0m2.517s
user    0m2.516s
sys     0m0.004s

[email protected]:~$ gcc -O3 -DUSE_RESTRICT test.c
[email protected]:~$ time ./a.out

real    0m2.034s
user    0m2.028s
sys     0m0.000s

На большом пальце на 20% быстрее выполняется в этой системе.

Чтобы показать, насколько это зависит от архитектуры, я разрешил запуск того же кода на встроенном ЦП Cortex-A8 (скорректировал количество циклов, потому что я не хочу так долго ждать):

[email protected]:~# gcc -O3 -mcpu=cortex-a8 -mfpu=neon -mfloat-abi=softfp test.c
[email protected]:~# time ./a.out

real    0m 7.64s
user    0m 7.62s
sys     0m 0.00s

[email protected]:~# gcc -O3 -mcpu=cortex-a8 -mfpu=neon -mfloat-abi=softfp -DUSE_RESTRICT test.c 
[email protected]:~# time ./a.out

real    0m 7.00s
user    0m 6.98s
sys     0m 0.00s

Здесь разница составляет всего 9% (тот же самый компилятор).

Ответ 3

Предоставляет ли ключевое слово ограничения значительные преимущества в gcc/g++?

Он может уменьшить количество инструкций, как показано в приведенном ниже примере, поэтому используйте его, когда это возможно.

GCC 4.8 Linux x86-64 exmample

Input:

void f(int *a, int *b, int *x) {
  *a += *x;
  *b += *x;
}

void fr(int *restrict a, int *restrict b, int *restrict x) {
  *a += *x;
  *b += *x;
}

Скомпилировать и декомпилировать:

gcc -g -std=c99 -O0 -c main.c
objdump -S main.o

С -O0 они совпадают.

С -O3:

void f(int *a, int *b, int *x) {
    *a += *x;
   0:   8b 02                   mov    (%rdx),%eax
   2:   01 07                   add    %eax,(%rdi)
    *b += *x;
   4:   8b 02                   mov    (%rdx),%eax
   6:   01 06                   add    %eax,(%rsi)  

void fr(int *restrict a, int *restrict b, int *restrict x) {
    *a += *x;
  10:   8b 02                   mov    (%rdx),%eax
  12:   01 07                   add    %eax,(%rdi)
    *b += *x;
  14:   01 06                   add    %eax,(%rsi) 

Для непосвященных соглашение о вызовах:

  • rdi= первый параметр
  • rsi= второй параметр
  • rdx= третий параметр

Заключение: 3 команды вместо 4.

Конечно, инструкции могут иметь разные задержки, но это дает хорошую идею.

Почему GCC смог оптимизировать это?

Вышеприведенный код был взят из Пример Википедии, который очень освещает.

Pseudo assembly для f:

load R1 ← *x    ; Load the value of x pointer
load R2 ← *a    ; Load the value of a pointer
add R2 += R1    ; Perform Addition
set R2 → *a     ; Update the value of a pointer
; Similarly for b, note that x is loaded twice,
; because a may be equal to x.
load R1 ← *x
load R2 ← *b
add R2 += R1
set R2 → *b

Для fr:

load R1 ← *x
load R2 ← *a
add R2 += R1
set R2 → *a
; Note that x is not reloaded,
; because the compiler knows it is unchanged
; load R1 ← *x
load R2 ← *b
add R2 += R1
set R2 → *b

Действительно ли это происходит быстрее?

Ermmm... не для этого простого теста:

.text
    .global _start
    _start:
        mov $0x10000000, %rbx
        mov $x, %rdx
        mov $x, %rdi
        mov $x, %rsi
    loop:
        # START of interesting block
        mov (%rdx),%eax
        add %eax,(%rdi)
        mov (%rdx),%eax # Comment out this line.
        add %eax,(%rsi)
        # END ------------------------
        dec %rbx
        cmp $0, %rbx
        jnz loop
        mov $60, %rax
        mov $0, %rdi
        syscall
.data
    x:
        .int 0

И затем:

as -o a.o a.S && ld a.o && time ./a.out

на Ubuntu 14.04 AMD64 Процессор Intel i5-3210M.

Я признаюсь, что до сих пор не понимаю современных процессоров. Дайте мне знать, если вы:

  • нашел недостаток в моем методе
  • нашел тестовый пример ассемблера, где он становится намного быстрее
  • понять, почему не было разницы.

Ответ 4

Я протестировал this C-Program. Без restrict потребовалось 12.640 секунд для завершения, restrict 12.516. Похоже, это может сэкономить некоторое время.

Ответ 5

Обратите внимание, что компиляторы С++, которые позволяют ключевое слово restrict, могут игнорировать его. Например, здесь.