Ускорение с помощью AVX2 и AVX512

Я пытаюсь представить себе ускорение за счет включения AVX2 и AVX512.

#include <stdio.h>
#include <stdlib.h>
#include <immintrin.h>
#include <omp.h>
#include <time.h>
int main()
{
  long i, N = 160000000;
  int * A = (int *)aligned_alloc(sizeof(__m256), sizeof(int) * N);
  int * B = (int *)aligned_alloc(sizeof(__m256), sizeof(int) * N);
  int * C = (int *)aligned_alloc(sizeof(__m256), sizeof(int) * N);

  int * E = (int *)aligned_alloc(sizeof(__m512), sizeof(int) * N);
  int * F = (int *)aligned_alloc(sizeof(__m512), sizeof(int) * N);
  int * G = (int *)aligned_alloc(sizeof(__m512), sizeof(int) * N);

  srand(time(0));

  for(i=0;i<N;i++)
  {
    A[i] = rand();
    B[i] = rand();
    E[i] = rand();
    F[i] = rand();
  }

  double time = omp_get_wtime();
  for(i=0;i<N;i++)
  {
    C[i] = A[i] + B[i];
  }
  time = omp_get_wtime() - time;
  printf("General Time taken %lf\n", time);

  __m256i A_256_VEC, B_256_VEC, C_256_VEC;
  time = omp_get_wtime();
  for(i=0;i<N;i+=8)
  {
    A_256_VEC = _mm256_load_si256((__m256i *)&A[i]);
    B_256_VEC = _mm256_load_si256((__m256i *)&B[i]);
    C_256_VEC = _mm256_add_epi32(A_256_VEC, B_256_VEC);
    _mm256_store_si256((__m256i *)&C[i],C_256_VEC);
  }
  time = omp_get_wtime() - time;
  printf("AVX2 Time taken %lf\n", time);

  free(A);
  free(B);
  free(C);

  __m512i A_512_VEC, B_512_VEC, C_512_VEC;
  time = omp_get_wtime();
  for(i=0;i<N;i+=16)
  {
    A_512_VEC = _mm512_load_si512((__m512i *)&E[i]);
    B_512_VEC = _mm512_load_si512((__m512i *)&F[i]);
    C_512_VEC = _mm512_add_epi32(A_512_VEC, B_512_VEC);
    _mm512_store_si512((__m512i *)&G[i],C_512_VEC);
  }
  time = omp_get_wtime() - time;
  printf("AVX512 Time taken %lf\n", time);

  for(i=0;i<N;i++)
  {
    if(G[i] != E[i] + F[i])
    {
      printf("Not Matched !!!\n");
      break;
    }
  }
  free(E);
  free(F);
  free(G);

  return 1;
}

Итак, код распространяется в три этапа. Присутствуют три массива. Это просто добавление массива. Сначала мы выполняем это с помощью общего цикла, затем с помощью AVX2, а затем AVX 512. Я использую процессор Intel Xeon 6130.

Код компилируется с помощью команды,

gcc -o test.o test.c -mavx512f -fopenmp -mavx2

Результат:

General Time taken 0.532550
AVX2 Time taken 0.175549
AVX512 Time taken 0.264475

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

Я проверил отдельные операции загрузки, добавления, хранения. Работа магазина AVX512 занимает максимальное время.

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

General Time taken 0.530248
AVX2 Time taken 0.115234
AVX512 Time taken 0.107062

Может ли кто-нибудь пролить свет на такое поведение или это ожидается?

********* ОБНОВЛЕНИЕ 1 *********

После компиляции с -O3 -march = native extension новые тайминги:

General Time taken 0.014887
AVX2 Time taken 0.008072
AVX512 Time taken 0.014630

Это все инструкции по загрузке, добавлению и сохранению.

********* ОБНОВЛЕНИЕ 2 *********

Тест 1:

Общий цикл был изменен следующим образом:

for(i=0;i<N;i++)
{
    //C[i] = A[i] + B[i];
    //G[i] = E[i] + F[i];
}

Результат:

General Time taken 0.000003
AVX2 Time taken 0.014877
AVX512 Time taken 0.014334

Поэтому в обоих случаях происходит сбой страницы.

Тест 2:

Общий цикл был изменен следующим образом:

for(i=0;i<N;i++)
{
    C[i] = A[i] + B[i];
    G[i] = E[i] + F[i];
}

Итак, кеширование выполняется в обоих случаях.

Выход,

General Time taken 0.029703
AVX2 Time taken 0.008500
AVX512 Time taken 0.008560

Тест 3:

Во всех сценариях добавляется фиктивный внешний цикл, а размер N уменьшается до 160000.

for(j=0;j<N;j++)
{
    for(i=0;i<N;i+= /* 1 or 8 or 16 */)
    {
         // Code
    }
}

Теперь на выходе

General Time taken 6.969532
AVX2 Time taken 0.871133
AVX512 Time taken 0.447317

person Pritam Pallab    schedule 04.02.2020    source источник
comment
Вы забыли включить оптимизацию !! Используйте -O3 -march=native для оптимизации и настройки вашего процессора. (И включите все поддерживаемые им расширения ISA.) Хотя оставление AVX512VL отключенным может помешать GCC быть глупым и использовать его без надобности. Возможно, GCC делает что-то еще более неэффективное в цикле AVX512, чем в цикле AVX2.   -  person Peter Cordes    schedule 04.02.2020
comment
Ваши массивы достаточно велики, поэтому инициализация A и B прямо перед чтением, а не E и F задолго до того, когда они будут прочитаны, вероятно, не имеет значения.   -  person Peter Cordes    schedule 04.02.2020
comment
Если вы удалите операции хранения из этих циклов, компилятор просто полностью удалит циклы (поскольку они не имеют никакого эффекта). Кроме того, если вы профилируете без -O2 или -O3, вы действительно не получите значимых результатов.   -  person robthebloke    schedule 04.02.2020
comment
@robthebloke: Но OP компилируется без оптимизации, поэтому каждый оператор C компилируется в отдельный блок asm. например A_256_VEC = _mm256_load_si256((__m256i *)&A[i]); будет компилироваться в блок, который загружает адрес массива и i из стека, использует его для загрузки vmovdqa, а затем сохраняет результирующий вектор в стек (в объекте A_256_VEC). Или, возможно, с дополнительной копией из-за накладных расходов из-за отказа от оптимизации встроенных функций, которые встраиваются. Но да, после исправления команды компиляции работа может быть полностью оптимизирована, если удалить хранилища.   -  person Peter Cordes    schedule 04.02.2020
comment
@PeterCordes Я скомпилировал с расширением -O3 -march = native и указал время.   -  person Pritam Pallab    schedule 04.02.2020
comment
Общий комментарий: Пожалуйста, если у кого-то, просматривающего пост, есть какие-либо мысли или мнения, укажите то же самое в комментарии. Голосование за сообщение без его просмотра не помогает в решении проблемы.   -  person Pritam Pallab    schedule 04.02.2020
comment
Вы забыли включить оптимизацию для последнего раунда тестов? Обычно это примерно в 8 раз дольше, чем AVX2, звучит так, как будто вы упустили -O3. Или, может быть, вы использовали -O2 или -O3 -fno-tree-vectorize, чтобы обмануть компилятор и заставить его делать медленный скалярный код, даже не SSE2.   -  person Peter Cordes    schedule 05.02.2020


Ответы (1)


Ваш тест AVX2 повторно использует тот же массив, который вы уже написали с «общим» тестом. Так что это уже ошибка страницы.

Ваш тест AVX512 выполняет запись в массив, который еще не был затронут, и должен оплатить стоимость этих ошибок страниц в заданном диапазоне. Либо испачкайте его за пределами временной области, либо просто повторно используйте C[] снова. Или mmap(MAP_POPULATE) тоже работает, создавая записываемые страницы. (Для реального использования могут быть лучше отложенные ошибки страниц. Если позволить ядру обнулить несколько страниц прямо перед их записью, это может снизить общую стоимость, позволив вашим реальным операциям записи попасть в кеш L1d до того, как хранилища обнуления ядра обратятся к внешним кешам. .)

Обратите внимание, что «общее» время (для автоматически векторизованного первого цикла) почти идентично времени «AVX512».gcc -O3 -march=native, GCC автоматически векторизует «общий» цикл с 256- битовые векторы согласно настройке по умолчанию -mprefer-vector-width=256 для -march=skylake-avx512).

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


Более низкие тактовые частоты из-за использования 512-битных векторов (ограничение максимального турбо) не должны сильно снижать пропускную способность памяти. (У вас будет узкое место в памяти с этим шаблоном доступа 2 для чтения / 1 для записи.) Если uncore (L3 / mesh) замедляется, чтобы соответствовать самому быстрому ядру, это может немного снизить пропускную способность, но кажется, что эффект крошечный, если присутствует вообще.

Пропускная способность памяти для этого STREAM-подобного теста должна быть примерно такой же, как для 256-битных, так и для 512-битных векторов. Если вы хотите увидеть измеримое ускорение от 512-битных векторов для проблемы с таким малым объемом вычислений на пропускную способность памяти, вам понадобятся ваши массивы, которые поместятся в кеш L1d и уже будут горячими. Или, возможно, кеш L2. (Используйте цикл повтора вокруг внутреннего цикла, который выполняет итерацию по массиву, чтобы он мог работать достаточно долго для хорошей точности синхронизации). AVX2 может легко не отставать от L3 или памяти для этого, поэтому AVX512 не поможет с большими массивами, если вы не выполняете больше работы для каждого вектора.


Нет ничего странного в циклах asm после включения оптимизации (https://godbolt.org/z/w4zcrC), поэтому мне пришлось более внимательно посмотреть, какие массивы вы на самом деле писали.

A и B, вероятно, полностью удаляются из кеша еще до запуска цикла AVX2 (потому что ваш N слишком велик; по 662 МБ каждый для A, B и C). Но все же немного странно запускать разные массивы для AVX2 и AVX512 и не запускать какой-либо цикл прогрева, чтобы убедиться, что процессор работает на максимальном турбо.

«Общее» время в основном действует как цикл разогрева как для тактовой частоты, так и для отказа страниц в массиве C[], поэтому фактическое время, измеренное для него, не будет показывать пропускную способность памяти для записи в уже загрязненную память. Возможно, вы сможете использовать perf, чтобы узнать, сколько времени затрачено на ядро.

person Peter Cordes    schedule 04.02.2020
comment
Будет ли mmap(MAP_LOCKED) заставлять его выделять доступные для записи страницы вместо нулевой страницы? - person Maxim Egorushkin; 04.02.2020
comment
@MaximEgorushkin: да, я так думаю, но по умолчанию вы можете заблокировать только несколько страниц. ulimit -l на моем рабочем столе Arch Linux составляет всего 64 КБ для без полномочий root. Это не то, что вы действительно хотите делать для реальных случаев использования, но наверняка, возможно, для микробенчмаркинга. Я думаю, что в реальных случаях использования вам часто нужны ленивые ошибки страниц прямо перед записью, особенно в Linux, где ядро ​​обнуляет страницу прямо тогда (так что страница или несколько страниц являются горячими в L1d после возврата к пользователю- Космос). Если ядру когда-нибудь придется его обнулить, это может быть сделано прямо перед записью. И предварительная выборка HW других массивов продолжается (?) - person Peter Cordes; 04.02.2020
comment
Я просто дважды проверил и MAP_POPULATE сопоставил доступные для записи страницы, а не нулевую страницу. wandbox.org/permlink/Vts2vHfn77sv0uAL - person Maxim Egorushkin; 04.02.2020
comment
@PeterCordes Большое спасибо за подробный ответ. Это полностью устранило проблему. Одно любопытство из вашего ответа заключается в том, что вы упомянули о загрузке массива в кеш L1d или кеш L2. Можете ли вы пролить на него немного света? Как процедура или если какой-либо доступный онлайн-ресурс по этому поводу, насколько вам известно. - person Pritam Pallab; 05.02.2020
comment
@PritamPallab: кеши ЦП маленькие, например 32 КБ (на ядро) для L1d. Если вы выполняете цикл по небольшому массиву несколько раз, все, кроме первого, попадут в кеш L1d (если вы не сделаете что-то еще между циклами, которое также затрагивает кучу памяти). - person Peter Cordes; 05.02.2020