Неоновая оптимизация с использованием встроенных функций

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

Без НЕОНА:

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

С НЕОНОМ:

 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. Это была опечатка, SIZE должен был быть размером; оба одинаковы.
4,5 .Пробовал на массиве из 4000 элементов. Я использовал gettimeofday() до и после вызова функции. NEON=230us,ordinary=155us.
6.Да, я распечатал элементы в каждом случае.
7.Сделал так, никаких улучшений.


person itisravi    schedule 19.04.2011    source источник
comment
Спасибо. Можете ли вы попробовать с оптимизацией (например, -O3)? Кажется, что есть много избыточного кода, который может влиять на тайминги.   -  person Igor Skochinsky    schedule 20.04.2011
comment
Спасибо за ответ. -O3 сократил время с 230 мкс до 95 мкс.   -  person itisravi    schedule 20.04.2011


Ответы (3)


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

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

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

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

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

person Igor Skochinsky    schedule 19.04.2011
comment
Может быть, функция double_elements не идеальный кандидат на «неонизацию»? - person itisravi; 20.04.2011
comment
Спасибо за подсказки :) сам по себе неон должен улучшать производительность. Разве компилятор не должен создавать лучший код за время без включенной оптимизации, особенно для неоновой части? - person itisravi; 20.04.2011
comment
Нет, без указания конкретной оптимизации GCC почти ничего не делает, кроме прямого преобразования исходного кода в ИК в машинный код. Никакой CSE, никакой оптимизации стекового фрейма, никакого устранения мертвого кода и так далее. Если вы хотите, чтобы ваш код был оптимизирован по времени или по другим причинам, вы должны сообщить об этом компилятору. Ознакомьтесь с этой статьей, чтобы узнать немного об этом. - person Igor Skochinsky; 20.04.2011
comment
При выключенных оптимизациях у компилятора есть контракт с отладчиком, который предотвращает даже самые простые оптимизации. Если вы пишете a[i]=0; b *= a[i];, компилятор должен сохранить ноль в a[i], затем пересчитать адрес a[i], прочитать его обратно и выполнить b *=. Потому что у вас могла быть точка останова между ними, и вы изменили i или a[i], прежде чем продолжить. - person greggo; 27.07.2015

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

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]!

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

person Guy Sirton    schedule 13.06.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, которая у вас есть. Сроки могут измениться в будущем, или, возможно, так было не всегда. Это работает для меня, ymmv.

person Michel Donais    schedule 22.11.2011