Ускорение AVX2 и AVX512 - PullRequest
       102

Ускорение AVX2 и AVX512

0 голосов
/ 04 февраля 2020

Я пытаюсь визуализировать ускорение включения 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;
}

Итак, код распределяется в три этапа. Три массива присутствуют. Это просто добавление массива. Сначала мы выполняем это с использованием общего l oop, затем с помощью 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

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

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

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

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

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

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

После компиляции с -O3 -march = собственные расширения, новые тайминги:

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

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

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

Тест 1:

Общие l oop был изменен следующим образом,

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:

Общие l oop был изменен

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:

Пустой внешний l oop добавлен во все сценарии ios и из 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

1 Ответ

5 голосов
/ 04 февраля 2020

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

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

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

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


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

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


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

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

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

...