Почему код gcc для моего развернутого эпилога цикла выглядит слишком сложным? - PullRequest
0 голосов
/ 04 июля 2018

Спасибо за все комментарии. Мне жаль, что я использовал плохой пример в своем первоначальном вопросе, который почти все сказали бы: «О, вы должны использовать memcopy Но это не то, о чем мой вопрос.

Мой вопрос более универсальный о том, как следует выполнять ручное развертывание цикла. Рассмотрим этот пример на этот раз, суммируя все элементы в массиве:

#include <stdlib.h>

double sum (size_t n, double *x) {
  size_t nr = n & 1;
  double *end = x + (n - nr);
  double sum_x = 0.0;
  for (; x < end; x++) sum_x += *x;
  if (nr) sum_x += *x;
  return sum_x;
  }

Сгенерированная компилятором сборка допускает аналогичное поведение (что показано в примере с копированием массива в моем исходном вопросе)

sum:
  movq %rdi, %rcx
  andl $1, %ecx
  subq %rcx, %rdi
  leaq (%rsi,%rdi,8), %rdx
  cmpq %rdx, %rsi
  jnb .L5
  movq %rsi, %rax
  pxor %xmm0, %xmm0
.L3:
  addsd (%rax), %xmm0
  addq $8, %rax
  cmpq %rax, %rdx
  ja .L3
  movq %rsi, %rax
  notq %rax
  addq %rax, %rdx
  shrq $3, %rdx
  leaq 8(%rsi,%rdx,8), %rsi
.L2:
  testq %rcx, %rcx
  je .L1
  addsd (%rsi), %xmm0
.L1:
  ret
.L5:
  pxor %xmm0, %xmm0
  jmp .L2

Однако, если я теперь планирую «дробную» часть перед основным циклом (как я позже вычеркну в ответе, который я написал), компилятор сделает намного лучшую работу.

#include <stdlib.h>

double sum (size_t n, double *x) {
  size_t nr = n & 1;
  double *end = x + n;
  double sum_x = 0.0;
  if (nr) sum_x += *x;
  for (x += nr; x < end; x++) sum_x += *x;
  return sum_x;
  }

sum:
  leaq (%rsi,%rdi,8), %rdx
  pxor %xmm0, %xmm0
  andl $1, %edi
  je .L2
  addsd (%rsi), %xmm0
.L2:
  leaq (%rsi,%rdi,8), %rax
  cmpq %rax, %rdx
  jbe .L1
.L4:
  addsd (%rax), %xmm0
  addq $8, %rax
  cmpq %rax, %rdx
  ja .L4
.L1:
  ret

Я использовал только флаг компилятора -O2. Итак, как сказал Питер, сгенерированная компилятором сборка должна быть близка к исходному коду. Тогда возникает вопрос: почему компилятор работает лучше в последнем случае?

Это на самом деле не вопрос, связанный с производительностью. Это просто то, что я неосознанно обнаружил (и не могу объяснить) при проверке вывода сборки компилятора для кода C из проекта C, который я писал. Еще раз спасибо. Спасибо Питеру за предложение лучшего названия для вопроса.


Оригинальный вопрос:

Следующая маленькая функция C копирует a, вектор n записей в b. Применяется ручное развертывание петли глубиной 2.

#include <stddef.h>

void foo (ptrdiff_t n, double *a, double *b) {
  ptrdiff_t i = 0;
  ptrdiff_t nr = n & 1;
  n -= nr;                  // `n` is an even integer
  while (i < n) {
    b[i] = a[i];
    b[i + 1] = a[i + 1];
    i += 2;
    }                       // `i = n` when the loop ends
  if (nr) b[i] = a[i];
  }

Это дает сборку x64 под gcc -O2 (любая gcc версия 5.4+). Тем не менее, я нахожу часть вывода как закомментированный странным. Почему компилятор их генерирует?

foo:
  movq %rdi, %rcx
  xorl %eax, %eax
  andl $1, %ecx
  subq %rcx, %rdi
  testq %rdi, %rdi
  jle .L11
.L12:
  movsd (%rsi,%rax,8), %xmm0
  movsd %xmm0, (%rdx,%rax,8)
  movsd 8(%rsi,%rax,8), %xmm0
  movsd %xmm0, 8(%rdx,%rax,8)
  addq $2, %rax
  cmpq %rax, %rdi           // `i` in %rax, `n` in %rdi
  jg .L12                   // the loop ends, with `i = n`, BELOW IS WEIRD
  subq $1, %rdi             // n = n - 1;
  shrq %rdi                 // n = n / 2;
  leaq 2(%rdi,%rdi), %rax   // i = 2 * n + 2;  (this is just `i = n`, isn't it?)
.L11:
  testq %rcx, %rcx
  je .L10
  movsd (%rsi,%rax,8), %xmm0
  movsd %xmm0, (%rdx,%rax,8)
.L10:
  ret

Аналогичная версия, использующая size_t вместо ptrdiff_t, дает нечто похожее:

#include <stdlib.h>

void bar (size_t n, double *a, double *b) {
  size_t i = 0;
  size_t nr = n & 1;
  n -= nr;                  // `n` is an even integer
  while (i < n) {
    b[i] = a[i];
    b[i + 1] = a[i + 1];
    i += 2;
    }                       // `i = n` when the loop ends
  if (nr) b[i] = a[i];
  }

bar:
  movq %rdi, %rcx
  andl $1, %ecx
  subq %rcx, %rdi
  je .L20
  xorl %eax, %eax
.L21:
  movsd (%rsi,%rax,8), %xmm0
  movsd %xmm0, (%rdx,%rax,8)
  movsd 8(%rsi,%rax,8), %xmm0
  movsd %xmm0, 8(%rdx,%rax,8)
  addq $2, %rax
  cmpq %rax, %rdi           // `i` in %rax, `n` in %rdi
  ja .L21                   // the loop ends, with `i = n`, BUT BELOW IS WEIRD
  subq $1, %rdi             // n = n - 1;
  andq $-2, %rdi            // n = n & (-2);
  addq $2, %rdi             // n = n + 2;  (this is just `i = n`, isn't it?)
.L20:
  testq %rcx, %rcx
  je .L19
  movsd (%rsi,%rdi,8), %xmm0
  movsd %xmm0, (%rdx,%rdi,8)
.L19:
  ret

А вот еще один эквивалент,

#include <stdlib.h>

void baz (size_t n, double *a, double *b) {
  size_t nr = n & 1;
  n -= nr;
  double *b_end = b + n;
  while (b < b_end) {
    b[0] = a[0];
    b[1] = a[1];
    a += 2;
    b += 2;
    }                       // `b = b_end` when the loop ends
  if (nr) b[0] = a[0];
  }

, но следующая сборка выглядит более странно (хотя производится под -O2). Теперь n, a и b все скопированы, и когда цикл заканчивается, мы берем 5 строк кода, чтобы в итоге получить b_copy = 0?!

baz:                        // initially, `n` in %rdi, `a` in %rsi, `b` in %rdx
  movq %rdi, %r8            // n_copy = n;
  andl $1, %r8d             // nr = n_copy & 1;
  subq %r8, %rdi            // n_copy -= nr;
  leaq (%rdx,%rdi,8), %rdi  // b_end = b + n;
  cmpq %rdi, %rdx           // if (b >= b_end) jump to .L31
  jnb .L31
  movq %rdx, %rax           // b_copy = b;
  movq %rsi, %rcx           // a_copy = a;
.L32:
  movsd (%rcx), %xmm0
  addq $16, %rax
  addq $16, %rcx
  movsd %xmm0, -16(%rax)
  movsd -8(%rcx), %xmm0
  movsd %xmm0, -8(%rax)
  cmpq %rax, %rdi           // `b_copy` in %rax, `b_end` in %rdi
  ja .L32                   // the loop ends, with `b_copy = b_end`
  movq %rdx, %rax           // b_copy = b;
  notq %rax                 // b_copy = ~b_copy;
  addq %rax, %rdi           // b_end = b_end + b_copy;
  andq $-16, %rdi           // b_end = b_end & (-16);
  leaq 16(%rdi), %rax       // b_copy = b_end + 16;
  addq %rax, %rsi           // a += b_copy;   (isn't `b_copy` just 0?)
  addq %rax, %rdx           // b += b_copy;
.L31:
  testq %r8, %r8            // if (nr == 0) jump to .L30
  je .L30
  movsd (%rsi), %xmm0       // xmm0 = a[0];
  movsd %xmm0, (%rdx)       // b[0] = xmm0;
.L30:
  ret

Кто-нибудь может объяснить, что имеет в виду компилятор во всех трех случаях?

Ответы [ 2 ]

0 голосов
/ 04 июля 2018

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

#include <stdlib.h>
#include <stddef.h>

void foo (ptrdiff_t n, double *a, double *b) {
  ptrdiff_t i = n & 1;
  if (i) b[0] = a[0];
  while (i < n) {
    b[i] = a[i];
    b[i + 1] = a[i + 1];
    i += 2;
    }
  }

void bar (size_t n, double *a, double *b) {
  size_t i = n & 1;
  if (i) b[0] = a[0];
  while (i < n) {
    b[i] = a[i];
    b[i + 1] = a[i + 1];
    i += 2;
    }
  }

void baz (size_t n, double *a, double *b) {
  size_t nr = n & 1;
  double *b_end = b + n;
  if (nr) b[0] = a[0];
  b += nr;
  while (b < b_end) {
    b[0] = a[0];
    b[1] = a[1];
    a += 2;
    b += 2;
    }
  }

foo:
  movq %rdi, %rax
  andl $1, %eax
  je .L9
  movsd (%rsi), %xmm0
  movsd %xmm0, (%rdx)
  cmpq %rax, %rdi
  jle .L11
.L4:
  movsd (%rsi,%rax,8), %xmm0
  movsd %xmm0, (%rdx,%rax,8)
  movsd 8(%rsi,%rax,8), %xmm0
  movsd %xmm0, 8(%rdx,%rax,8)
  addq $2, %rax
.L9:
  cmpq %rax, %rdi
  jg .L4
.L11:
  ret

bar:
  movq %rdi, %rax
  andl $1, %eax
  je .L20
  movsd (%rsi), %xmm0
  movsd %xmm0, (%rdx)
  cmpq %rax, %rdi
  jbe .L21
.L15:
  movsd (%rsi,%rax,8), %xmm0
  movsd %xmm0, (%rdx,%rax,8)
  movsd 8(%rsi,%rax,8), %xmm0
  movsd %xmm0, 8(%rdx,%rax,8)
  addq $2, %rax
.L20:
  cmpq %rax, %rdi
  ja .L15
.L21:
  ret

baz:
  leaq (%rdx,%rdi,8), %rcx
  andl $1, %edi
  je .L23
  movsd (%rsi), %xmm0
  movsd %xmm0, (%rdx)
.L23:
  leaq (%rdx,%rdi,8), %rax
  cmpq %rax, %rcx
  jbe .L22
.L25:
  movsd (%rsi), %xmm0
  addq $16, %rax
  addq $16, %rsi
  movsd %xmm0, -16(%rax)
  movsd -8(%rsi), %xmm0
  movsd %xmm0, -8(%rax)
  cmpq %rax, %rcx
  ja .L25
.L22:
  ret
0 голосов
/ 04 июля 2018

Если вы спрашиваете, почему сборка относительно большая, то это потому, что компилятор не может предположить то, что вы знаете.

Например, если вы знаете, что исходный массив не будет изменен во время копирования, сообщите об этом компилятору, добавив квалификатор const к указанным исходным данным.

void foo (ptrdiff_t n, double *a, double const *b)

Кроме того, если вы знаете, что два диапазона памяти никогда не будут перекрываться, добавьте квалификатор restrict к каждому из двух указателей.

void foo (ptrdiff_t n, double *restrict a, double const *restrict b)

В конечном итоге, если вам нужна максимально оптимизированная копия (производители компиляторов тратят на это МНОГО времени), используйте memcpy для непересекающихся диапазонов и memmove для перекрывающихся диапазонов.

...