Почему автовекторизация gcc не работает с матрицей свертки больше 3x3?

Я реализовал следующую программу для матрицы свертки

#include <stdio.h>
#include <time.h>

#define NUM_LOOP 1000
#define N 128   //input or output dimention 1
#define M N     //input or output dimention 2
#define P 5 //convolution matrix dimention 1 if you want a 3x3 convolution matrix it must be 3
#define Q P     //convolution matrix dimention 2
#define Csize P*Q   
#define Cdiv  1     //div for filter 
#define Coffset 0   //offset 

//functions
void unusual(); //unusual implementation of convolution
void naive();
//data
unsigned short int input[N][M] __attribute__(( aligned(32))); // input data
unsigned short int output[N][M] __attribute__(( aligned(32))); // out put data
unsigned short int kernel[P][Q] __attribute__(( aligned(32)));//convolution coefficients

int main(){
    struct timespec tStart, tEnd;//used to record the processiing time
    double tTotal , tBest=10000;//minimum of toltal time will asign to the best time

    int w=0;
    do{// this loop repeat the body to record the best time
        clock_gettime(CLOCK_MONOTONIC,&tStart);

        //function to be executed here :

        unusual();

        clock_gettime(CLOCK_MONOTONIC,&tEnd);
        tTotal = (tEnd.tv_sec - tStart.tv_sec);
        tTotal += (tEnd.tv_nsec - tStart.tv_nsec) / 1000000000.0;

        if(tTotal<tBest)
            tBest=tTotal;
    } while(w++ < NUM_LOOP);

    printf(" The best time: %lf sec in %d repetition for %dX%d matrix\n",tBest,w, MAX1, MAX2);

    return 0;
}

//unusual sequential convolution
void unusual(){
    int i, j,k,temp;

    for (i=P/2; i< N-P/2; i++){
        for(j=Q/2; j< M-Q/2; j++){
            temp=0;
            for(k=0; k< Csize; k++){
                temp += (kernel[k/P][k%Q]) * (input[i - (P/2) + (k/Q)][j - (Q/2) + (k%Q)]);

            }
            output[i][j]=((temp/(Cdiv))+Coffset);
        }
    }
}
//The naive implementation
inline void naive(){
    int i, j,k,l,temp;
    for (i=P/2; i< N-P/2; i++){
        for(j=Q/2; j< M-Q/2; j++){
            temp=0;

            for(k = 0; k <  P; k++){ 
                for(l = 0; l <  Q; l++){
                    temp += (kernel[k][l]) * (input[i - (P/2)+k][j - (Q/2)+l]);
                }
            }
            output[i][j]=((temp/(Cdiv))+Coffset);
        }
    }
}

Проблема в том, что когда я использую -O3 для автоматической векторизации, это работает только для матрицы свертки 3x3. Я видел, как выходные данные сборки и автоматическая векторизация просто вносят некоторые изменения для ядра 3x3 и разумно улучшают производительность (примечание в 20 раз быстрее: скалярная версия необычной функции медленнее, чем наивная забава), но для матрицы свертки 5x5 улучшений нет.

ОБНОВЛЕНИЕ: я добавил наивную реализацию к вопросу и изменил размер изображения на NxM, матрицу conv на ядро, Cdim1xCdim2 на PxQ и функцию seqConv на необычную для пояснения. Вопрос не в том, чтобы улучшить реализацию необычной функции. Вопрос в том, что если все элементы находятся в одних и тех же местах памяти, gcc использует эвристику и т. д., то почему gcc не может улучшить эту необычную реализацию? ПРИМЕЧАНИЕ: проблема не в наивной реализации. gcc -O3 улучшить наивную реализацию для ядер 3x3, 5x5 на ~7 ускорение. и это также для 7x7 и 9x9 с ускорением ~ 1,5. Чтобы улучшить свертку, я использовал встроенные функции, и ускорение более чем в 40 раз по сравнению с простой реализацией, что примерно в 2 раза быстрее, чем у обычной свертки. Так что моя векторизация примерно в 80 раз быстрее, чем моя необычная. Оптимизация ручной настройки не является проблемой. Оптимизация автоматического векторизатора является проблемой и причиной сбоев.

Команда GCC: gcc -Wall -march=native -O3 -o "%e" "%f"

Платформа: Linux mint, Skylake, gcc 6.2.

заранее спасибо


person Martin    schedule 04.12.2016    source источник
comment
Не могли бы вы завершить это достаточно, чтобы оно скомпилировалось?   -  person harold    schedule 05.12.2016
comment
Конечно, я просто добавил пропущенную часть. программа отверстия содержит множество других функций, реализованных с помощью встроенных функций AVX2. В программе я выровнял все матрицы с помощью __attribute__(( aligned(32)))   -  person Martin    schedule 07.12.2016
comment
Я скомпилировал с #define Cdim1 3 в clang и MVC++, а ускорение по сравнению с gcc -O2 составляет 0.97 и 4.34 соответственно clang -O3 и MVC++ O2. Я также включил /arch:AVX2 и Enhancement extension Ot, но разницы нет.   -  person Martin    schedule 07.12.2016
comment
Нет ответа на этот вопрос?   -  person Martin    schedule 13.12.2016
comment
Я не увидел ничего определенного. Трудно что-либо разглядеть сквозь огромный кусок ассемблера, который это генерирует. Единственная подсказка, которую я увидел, заключалась в том, что для 3x3 используется много регистров, 5x5 потребовалось бы больше, поэтому, возможно, для 5x5 GCC пугается количества материала, который ему придется пролить ... но опять же это может быть что-то еще, их много эвристических решений в этот момент   -  person harold    schedule 13.12.2016
comment
Как вы думаете, у gcc есть библиотека свертки 3x3? а для 5х5 нет?   -  person Martin    schedule 13.12.2016
comment
Это может быть размер матрицы свертки, 3x3xsizeof(short int) <256 (the vector size) whenever 5x5x16>256   -  person Martin    schedule 13.12.2016
comment
Однако наличие этой матрицы в одном регистре не кажется очень полезным, потребуется несколько безумных перетасовок, чтобы выровнять данные с ней, поскольку она двумерная. Но я снова пройдусь по этой огромной мешанине asm и посмотрю, что она делает.   -  person harold    schedule 13.12.2016
comment
Может существовать алгоритм автоматической векторизации, адаптированный к регистрам меньше 256.   -  person Martin    schedule 13.12.2016
comment
Он использует один вектор для каждого элемента conv, поэтому размер вектора не имеет значения. Количество регистров еще может быть, но я действительно не уверен. Конечно, он не может использовать 25 векторов для хранения записей в случае 5x5, но он может транслировать некоторые из них на лету. Однако GCC продолжает отказываться от сотрудничества, я потратил много времени, пытаясь убедить его сделать хороший код для 5x5, но это просто не так, даже со встроенными функциями. Ну, может быть, если я буду очень явным и ничего не оставлю оптимизатору, но тогда мне придется разворачивать вручную и терять общность.   -  person harold    schedule 13.12.2016
comment
Кроме того, GCC выполняет всю математику с двойными словами, что не является необходимым (было бы, если бы Cdiv перестало быть 1, за исключением некоторых конкретных случаев и округления немного, оно определенно должно быть степенью двойки, по крайней мере, поскольку нет целочисленное векторное деление). Таким образом, short temp приводит к более красивому коду. Однако не для 5x5, это все еще слишком много, чтобы просить, по-видимому. GCC также очень осторожен, не перезаписывая отступы, поэтому каждая строка начинается и заканчивается 15 скалярными свертками, этого легко избежать с помощью встроенных функций (верхний и нижний все еще должны иметь это, иначе чтение выйдет за пределы ввода).   -  person harold    schedule 13.12.2016
comment
По внутренностям все ок. На самом деле, эта программа является жесткой реализацией 2D-свертки. С четырьмя вложенными циклами gcc может значительно улучшить его. Но не для этой реализации   -  person Martin    schedule 01.04.2017
comment
Кажется, вы не первый, кто изучает это, автоматическая вакторизация довольно сложна и работает (или нет) очень зависит от того, как вы пишете свой код. переписывание структуры кода может очень помочь, я считаю, что ссылки на страницы не всегда подходят для stackoverflow, но я хотел бы связать вас со следующей страницей: locklessinc.com/articles/vectorize Этот парень приложил немало усилий для вакторизации, и я надеюсь, что это поможет вам решить вашу проблему.   -  person koldewb    schedule 03.04.2017
comment
@koldewb, большое спасибо, ссылка кажется очень полезной. Да, можно изменить. Действительно, я изменил наивную реализацию с циклами for на эту с тремя циклами. Скалярный код тормозил и автовекторизатор его не понимал!   -  person Martin    schedule 04.04.2017


Ответы (3)


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

Первое обновление: По моему опыту, gcc -fopt-info-vec сообщает о векторизации для Csize <= 16. Это связано с тем, что коэффициент векторизации равен 16, и это одна из причин, по которой gcc не векторизует необычную реализацию для других размеров ядра. Фактор векторизации относится к количеству элементов, которые можно поместить в вектор. В этом случае short integer равно элементу 16-bit.

Из википедии:

На первом этапе компилятор ищет препятствия, которые могут помешать векторизации. Основным препятствием для векторизации является истинная зависимость данных короче, чем длина вектора. Другие препятствия включают вызовы функций и короткие счетчики итераций.

person Martin    schedule 07.04.2017

Я предполагаю, что он не может оптимизироваться из-за проблем с выравниванием памяти. Вы указали, что свертка состоит из 2-байтовых шорт. Большинству функций SSE нравится работать со 128-битными векторами, а AVX предпочитает 512-битные векторы.

На моей машине я объявил conv следующим образом:

uint16_t conv[Cdim1][8] = {0}; //You need to pad extra fields with zeroes

А позже замените внутренний цикл следующим образом:

for(ki = 0; ki < Cdim; ++ki) 
    for(kj = 0; kj < 8; ++kj)
        temp += (conv[ki][kj]) * (input[i - (Cdim1/2) + ki][j - (Cdim2/2) + kj]);

Компиляция с: gcc so.c -Wall -Wextra -Ofast -mtune=native дала мне векторную оптимизацию!

Плохое:

  • Не используйте 8. Попробуйте найти минимально необходимое заполнение и сделать макрос, чтобы он работал с матрицами сверток размерности> = 8
  • Заполнить ввод несколькими нулями, чтобы исчезло неопределенное поведение в конце
  • Обратите внимание, что на самом деле это не увеличивает вашу производительность. На самом деле он работает медленнее!
  • Обратите внимание, что вы можете сжать пару циклов, если вы измените это таким образом, чтобы вы выполняли циклы в следующем порядке for(ki) for(i) for(j) for(kj). Вероятно, это связано с меньшим давлением на регистры, поскольку каждая строка conv может храниться дольше. Это также может быть сбой на моем процессоре.
  • Возможно, вы также захотите использовать __attribute__ ((aligned (8))) при объявлении переменных. В данном случае это ничего не изменило, но при оптимизации нужно учитывать и это. Естественно, это будет работать только на GCC, а для MSVC вам потребуются другие хаки.
person vguberinic    schedule 04.04.2017
comment
Спасибо, но вы изменили внутренний цикл. Как я уже упоминал в комментариях, я обновлю вопрос. gcc может векторизовать реализацию четырех циклов. мой gcc 6.2 векторизовал его для ядер 3x3 и 5x5 с ускорением ~ 6x. Даже gcc улучшает 7x7 и 9x9. Кстати, проблема в необычной реализации, которая называется seqconv не наивной реализацией. - person Martin; 05.04.2017

Основным препятствием для автоматического векторизатора является вариант с непостоянным циклом. В вашей реализации, если вы используете int Csize = P*Q;, он не будет векторизован. Так что для помощи автовектору вам следует учесть это. Это не проблема, потому что вы объявили Csize как #define Csize. Но обратите внимание на это в своих работах. Тогда ваша необычная реализация представляет собой петлевое преобразование реализации нефа, которая является методом оптимизации в компиляторах. Кажется, вы испортили наивную реализацию. Ваш вывод говорит, что он ограничен из-за 16, поэтому я развернул вашу необычную функцию, и авто-векторизатор говорит, что она была векторизована.

for(k=0; k< P*Q; k+=2){
                temp += (kernel[k/Q][k%Q]) * (input[i - (P/2) + (k/Q)][j - (Q/2) + (k%Q)]);
                temp += (kernel[k/Q][k%Q]) * (input[i - (P/2) + ((k+1)/Q)][j - (Q/2) + ((k+1)%Q)]);
}

Это также работает для ядра 7x7:

for(k=0; k< P*Q; k+=4){//IACA_START
                temp += (kernel[k/Q][k%Q]) * (input[i - (P/2) + (k/Q)][j - (Q/2) + (k%Q)]);
                temp += (kernel[k/Q][k%Q]) * (input[i - (P/2) + ((k+1)/Q)][j - (Q/2) + ((k+1)%Q)]);
                temp += (kernel[k/Q][k%Q]) * (input[i - (P/2) + ((k+2)/Q)][j - (Q/2) + ((k+2)%Q)]);
                temp += (kernel[k/Q][k%Q]) * (input[i - (P/2) + ((k+3)/Q)][j - (Q/2) + ((k+3)%Q)]);
}

вам не нужно разворачивать его самостоятельно, вы можете заставить компилятор развернуть или изменить структуру цикла с помощью #pragma. Это связано с концепцией SLP, которую компиляторы используют для автовекторизации и, что интересно, SLP основана на разворачивается!.

person ADMS    schedule 09.04.2017