Неоновая оптимизация с использованием встроенных функций - PullRequest
5 голосов
/ 19 апреля 2011

Узнав об встроенных функциях ARM NEON, я рассчитывал написанную мной функцию для удвоения элементов в массиве. Версия, в которой использовались встроенные функции, отнимает больше времени, чем простая функция на языке Си.

Без НЕОНА:

    void  double_elements(unsigned int *ptr, unsigned int size)
 {
        unsigned int loop;
        for( loop= 0; loop<size; loop++)
                ptr[loop]<<=1;
        return;
 }

с NEON:

 void  double_elements(unsigned int *ptr, unsigned int size)
{    
        unsigned int i;
        uint32x4_t Q0,vector128Output;
        for( i=0;i<(SIZE/4);i++)
        {
                Q0=vld1q_u32(ptr);               
                Q0=vaddq_u32(Q0,Q0);
                vst1q_u32(ptr,Q0);
                ptr+=4;

        }
        return;
}

Интересно, занимает ли операция загрузки / сохранения между массивом и вектором больше времени, что компенсирует преимущество параллельного сложения.

ОБНОВЛЕНИЕ: дополнительная информация в ответ на ответ Игоря.
1. Код размещен здесь:
plain.c
plain.s
neon.c
neon.s
Из раздела (метки) L7 в обоих листингах сборки я вижу, что неоновая версия имеет большее количество инструкций по сборке. (Следовательно, больше времени требуется?)
2. Я скомпилировал, используя -mfpu = neon на arm-gcc, без других флагов или оптимизаций. Для простой версии, без флагов компилятора вообще.
3. Это была опечатка, размер должен был быть размером, оба одинаковы.
4,5.Пробовал на массиве 4000 элементов. Я рассчитал использование gettimeofday () до и после вызова функции. NEON = 230us, обычный = 155us.
6. Да, я напечатал элементы в каждом случае.
7. Сказал это, никаких улучшений вообще.

Ответы [ 3 ]

4 голосов
/ 13 июня 2011

Что-то вроде этого может работать немного быстрее.

void  double_elements(unsigned int *ptr, unsigned int size)
{    
    unsigned int i;
    uint32x4_t Q0,Q1,Q2,Q3;

    for( i=0;i<(SIZE/16);i++)
    {
            Q0=vld1q_u32(ptr);               
            Q1=vld1q_u32(ptr+4);               
            Q0=vaddq_u32(Q0,Q0);
            Q2=vld1q_u32(ptr+8);               
            Q1=vaddq_u32(Q1,Q1);
            Q3=vld1q_u32(ptr+12);               
            Q2=vaddq_u32(Q2,Q2);
            vst1q_u32(ptr,Q0);
            Q3=vaddq_u32(Q3,Q3);
            vst1q_u32(ptr+4,Q1);
            vst1q_u32(ptr+8,Q2);
            vst1q_u32(ptr+12,Q3);
            ptr+=16;

    }
    return;
}

Есть несколько проблем с исходным кодом (некоторые из них оптимизатор может исправить, а другие - нет, необходимо проверить всгенерированный код):

  • Результат добавления доступен только на этапе N3 конвейера NEON, поэтому следующее хранилище остановится.
  • Предполагается, что компилятор не разворачивает циклмогут быть некоторые издержки, связанные с циклом / ветвью.
  • Он не использует возможности двойной выдачи загрузки / сохранения с другой инструкцией NEON.
  • Если исходные данные не 'т в кеше тогда нагрузки остановятся.Вы можете предварительно загрузить данные, чтобы ускорить это с помощью встроенного __ builtin_prefetch .
  • Кроме того, как другие отметили, что операция довольно тривиальна, вы увидите больше преимуществ для более сложных операций.

Если бы вы написали это с помощью встроенной сборки, вы могли бы также:

  • Использовать выровненную загрузку / хранилища (которые, я не думаю, могут генерировать встроенные функции) и обеспечитьваш указатель всегда выровнен на 128 бит, например, vld1.32 {q0}, [r1: 128]
  • Вы также можете использовать постинкрементную версию (которую я также не уверен, что встроенные функции будутгенерировать), например, vld1.32 {q0}, [r1: 128]!

95us для 4000 элементов звучит довольно медленно, на процессоре с частотой 1 ГГц это ~ 95 циклов на 128 биткусок.Вы должны быть в состоянии лучше, если вы работаете из кеша.Эта цифра соответствует ожидаемому, если вы связаны скоростью внешней памяти.

3 голосов
/ 22 ноября 2011

Обрабатывать в больших количествах по инструкции, чередовать загрузку / хранение и чередовать использование.В настоящее время эта функция удваивается (сдвигается влево) 56 uint.

void shiftleft56(const unsigned int* input, unsigned int* output)
{
  __asm__ (
  "vldm %0!, {q2-q8}\n\t"
  "vldm %0!, {q9-q15}\n\t"
  "vshl.u32 q0, q2, #1\n\t"
  "vshl.u32 q1, q3, #1\n\t"
  "vshl.u32 q2, q4, #1\n\t"
  "vshl.u32 q3, q5, #1\n\t"
  "vshl.u32 q4, q6, #1\n\t"
  "vshl.u32 q5, q7, #1\n\t"
  "vshl.u32 q6, q8, #1\n\t"
  "vshl.u32 q7, q9, #1\n\t"
  "vstm %1!, {q0-q6}\n\t"
  // "vldm %0!, {q0-q6}\n\t" if you want to overlap...
  "vshl.u32 q8, q10, #1\n\t"
  "vshl.u32 q9, q11, #1\n\t"
  "vshl.u32 q10, q12, #1\n\t"
  "vshl.u32 q11, q13, #1\n\t"
  "vshl.u32 q12, q14, #1\n\t"
  "vshl.u32 q13, q15, #1\n\t"
  // lost cycle here unless you overlap
  "vstm %1!, {q7-q13}\n\t"
  : "=r"(input), "=r"(output) : "0"(input), "1"(output)
  : "q0", "q1", "q2", "q3", "q4", "q5", "q6", "q7",
    "q8", "q9", "q10", "q11", "q12", "q13", "q14", "q15", "memory" );
}

Что важно помнить для оптимизации Neon ... Она имеет два конвейера, один для загрузки / сохранения (с 2 очередями инструкций - один ожидающий иодин запуск - обычно по 3-9 циклов каждый) и один для арифметических операций (с 2 конвейерами команд, один выполняется и один сохраняет результаты).Пока вы удерживаете эти два конвейера занятыми и чередуете свои инструкции, это будет работать очень быстро.Более того, если у вас есть инструкции ARM, пока вы остаетесь в регистрах, вам никогда не придется ждать выполнения NEON, они будут выполняться одновременно (до 8 инструкций в кеше)!Таким образом, вы можете установить некоторую базовую логику цикла в инструкциях ARM, и они будут выполняться одновременно.

Ваш исходный код также использовал только одно значение регистра из 4 (регистр q имеет 4 32-битных значения).3 из них получали операцию удвоения без видимой причины, поэтому вы работали в 4 раза медленнее, чем могли бы.

Что было бы лучше в этом коде, так это выполнить этот цикл, обработав их встроеннымидобавление vldm %0!, {q2-q8} после vstm %1! ... и так далее.Вы также видите, что я жду еще 1 инструкцию перед отправкой ее результатов, поэтому каналы никогда не ждут чего-то другого.Наконец, обратите внимание на !, это означает постинкремент.Таким образом, он читает / записывает значение, а затем автоматически увеличивает указатель из регистра.Я предлагаю вам не использовать этот регистр в коде ARM, чтобы он не зависал в своих собственных конвейерах ... держите ваши регистры отдельно, у вас должна быть избыточная переменная count на стороне ARM.

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

3 голосов
/ 19 апреля 2011

Вопрос довольно расплывчатый, и вы не предоставили много информации, но я постараюсь дать вам несколько советов.

  1. Вы не будете точно знать, что происходит, пока не посмотрите наАссамблея.Используйте -S, Люк!
  2. Вы не указали настройки компилятора.Вы используете оптимизации?Развертывание цикла?
  3. Первая функция использует size, вторая использует SIZE, это намеренно?Они одинаковые?
  4. Какой размер массива вы пытались?Я не ожидаю, что NEON вообще поможет для пары элементов.
  5. Какая разница в скорости?Несколько процентов?Пара порядков?
  6. Вы проверяли, что результаты совпадают?Вы уверены, что код эквивалентен?
  7. Вы используете ту же переменную для промежуточного результата.Попробуйте сохранить результат добавления в другой переменной, которая может помочь (хотя я ожидаю, что компилятор будет умным и выделит другой регистр).Кроме того, вы можете попробовать использовать shift (vshl_n_u32) вместо сложения.

Edit: спасибо за ответы.Я немного осмотрелся и нашел это обсуждение , в котором говорится (выделено мое):

Перемещение данных из NEON в регистры ARM - это Cortex-A8 дорого, поэтому NEONв Cortex-A8 лучше всего использовать для больших блоков работы с небольшим взаимодействием конвейера ARM.

В вашем случае нет преобразования NEON в ARM, а только загружаются и хранятся.Тем не менее, кажется, что сбережения в параллельной работе съедаются не-NEON частями.Я ожидал бы лучших результатов в коде, который делает много вещей в то время как в NEON, например преобразование цветов.

...