Запретить gcc искажать мою собственную копию AVX2 в REP MOVS - PullRequest
4 голосов
/ 01 ноября 2019

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

template <typename T>
void copytail(T* __restrict__ dest, const T* __restrict__ src, size_t count) {
  constexpr size_t chunk_size = 4 * 32;
  size_t byte_count = sizeof(T) * count;
  size_t chunks = byte_count / chunk_size;
  auto rest = byte_count - byte_count / chunk_size * chunk_size;
  auto rest_vecs = (rest + 31) / 32;
  __m256i* dest256 = (__m256i*)((char *)dest + byte_count - rest_vecs * 32);
  __m256i* src256  = (__m256i*)((char *)src  + byte_count - rest_vecs * 32);
  for (size_t j = 0; j < rest_vecs; j++) {
      _mm256_storeu_si256(dest256 + j, _mm256_loadu_si256(src256 + j));
  }
}


void tail_copy(char* d, const char* s, size_t overshoot) {
    copytail(d, s, overshoot);
}

Не слишком задумывайтесь о том, что он делает, так как это сокращенный контрольный пример, основанный на более полной функции - но в основном он копирует до 4 AVX2векторы от src до dest, выровненные по концу областей.

По какой-либо причине 1 , gcc 8.1 при -O3 создает это странноесборка:

tail_copy(char*, char const*, unsigned long):
  mov rax, rdx
  and eax, 127
  add rax, 31
  mov rcx, rax
  and rcx, -32
  sub rdx, rcx
  shr rax, 5
  je .L30
  sal rax, 5
  mov r8d, eax
  add rdi, rdx
  add rsi, rdx
  test dil, 1
  jne .L32
.L3:
  test dil, 2
  jne .L33
.L4:
  test dil, 4
  jne .L34
.L5:
  mov ecx, r8d
  shr ecx, 3
  rep movsq   # oh please no
  xor eax, eax
  test r8b, 4
  jne .L35
  test r8b, 2
  jne .L36
  # many more tail-handling cases follow

В основном rep movsq для вызова микрокода для основной копии, а затем набор кода обработки хвоста для обработки нечетных байтов (большинство не показано, полную сборку можно увидеть на godbolt ).

Это на порядок медленнее, чем vmovdqu загрузка / хранение в моем случае.

И даже если он собирался использовать rep movsЦП имеет ERMSB, поэтому rep movsb может, вероятно, выполнять точное количество байтов без дополнительной очистки, необходимой примерно так же эффективно, как rep movsq. Но у процессора нет есть функция "fast short rep" (Ice Lake), поэтому у нас rep movs накладные расходы при запуске - большая проблема.

Я бы хотел, чтобы gcc выдал свою копиюцикл более или менее, как написано - по крайней мере 32-байтовые загрузки AVX2 и хранилище должны отображаться как в исходном коде. Важно отметить, что я хочу, чтобы это было локально для этой функции: то есть не изменять аргументы компилятора.


1 Возможно, это memcpy распознавание, за которым следует memcpy вставка.

Ответы [ 2 ]

2 голосов
/ 02 ноября 2019

Возможно, это решение слишком очевидно, но вы можете помешать gcc (и clang) распознавать memcpy в вашем коде, просто удалив __restrict__:

template <typename T>
void copytail(T* dest, const T* src, size_t count) {
  constexpr size_t chunk_size = 4 * 32;
  size_t byte_count = sizeof(T) * count;
  size_t chunks = byte_count / chunk_size;
  auto rest = byte_count - byte_count / chunk_size * chunk_size;
  auto rest_vecs = (rest + 31) / 32;
  __m256i* dest256 = (__m256i*)((char *)dest + byte_count - rest_vecs * 32);
  __m256i* src256  = (__m256i*)((char *)src  + byte_count - rest_vecs * 32);
  for (size_t j = 0; j < rest_vecs; j++) {
      _mm256_storeu_si256(dest256 + j, _mm256_loadu_si256(src256 + j));
  }
}

Сравнение Godbolt: https://godbolt.org/z/osjO91

2 голосов
/ 02 ноября 2019

Ваше предположение о memcpy распознавании кажется правильным (__builtin_memcpy впервые появляется в проходе ldist, как видно из журналов -fdump-tree-all), и это препятствует оптимизации:

__attribute__ ((optimize ("no-tree-loop-distribute-patterns")))
void tail_copy(char* d, const char* s, size_t overshoot) {
    copytail(d, s, overshoot);
}

Применение его к определению шаблона, похоже, тоже работает.

Если процессор поддерживает ERMS (как это делают большинство процессоров Intel с AVX2), неясно, является ли это улучшением.

...