Подтвердить что ты не робот

Какой пример простой функции C, которая быстрее реализована в встроенной сборке?

Мне сложно избивать мой компилятор, используя встроенную сборку.

Какие хорошие, не надуманные примеры функции, с которой компилятор имеет сложное время, действительно, очень быстро и просто? Но это относительно просто сделать с встроенной сборкой.

4b9b3361

Ответ 1

Поскольку это связано с iPhone и ассемблерным кодом, я приведу пример, который будет иметь отношение к iPhone-миру (а не к sm или x86 asm). Если кто-то решит написать код сборки для какого-то реального приложения, то, скорее всего, это будет какая-то цифровая обработка сигнала или манипуляция изображениями. Примеры: преобразование цветового пространства пикселей RGB, кодирование изображений в формат jpeg/png или кодирование звука в mp3, amr или g729 для приложений voip. В случае кодирования звука существует множество подпрограмм, которые невозможно перевести компилятором в эффективный код asm, они просто не имеют эквивалента в C. Примеры широко используемых материалов в обработке звука: насыщенная математика, многократно накапливаемые подпрограммы, умножение матрицы.

Пример насыщенного add: 32-разрядный подписанный int имеет диапазон: 0x8000 0000 <= int32 = lt; = 0x7fff ffff. Если вы добавите два результата, результат может переполняться, но в некоторых случаях это может быть неприемлемым при обработке цифрового сигнала. В принципе, если переполнение потока или недополнение насыщенного add должно возвращать 0x8000 0000 или 0x7fff ffff. Это будет полная функция c, чтобы проверить это. оптимизированная версия насыщенного add может быть:

int saturated_add(int a, int b)
{
    int result = a + b;

    if (((a ^ b) & 0x80000000) == 0)
    {
        if ((result ^ a) & 0x80000000)
        {
            result = (a < 0) ? 0x80000000 : 0x7fffffff;
        }
    }
    return result;
} 

вы также можете выполнить несколько if/else, чтобы проверить переполнение или на x86, вы можете проверить флаг переполнения (что также требует использования asm). iPhone использует armv6 или v7 cpu, которые имеют dsp asm. Таким образом, функция saturated_add с несколькими бранчами (if/else statements) и 2 32-битными константами может быть одной простой инструкцией asm, которая использует только один цикл процессора. Таким образом, просто заставить saturated_add использовать инструкцию asm может сделать весь алгоритм в два-три раза быстрее (и меньше по размеру). Здесь руководство QADD: QADD

другие примеры кода, которые часто выполняются в длинных циклах,

res1 = a + b1*c1;
res2 = a + b2*c2;
res3 = a + b3*c3;

похоже, что здесь ничего нельзя оптимизировать, но на процессоре ARM вы можете использовать определенные команды dsp, которые занимают меньше циклов, чем простое умножение! Это право, a + b * c с конкретными инструкциями может выполняться быстрее, чем просто a * b. Для подобных случаев компиляторы просто не могут понять логику вашего кода и не могут напрямую использовать эти инструкции dsp и почему вам нужно вручную писать asm для оптимизации кода, НО вы должны вручную писать некоторые части кода, которые должны быть оптимизировано. Если вы начнете писать простые циклы вручную, то почти наверняка вы не будете бить компилятор! В Интернете есть несколько хороших документов для встроенной сборки для кодирования еловых фильтров, кодирования/декодирования amr и т.д.

Ответ 2

Если вы не рассматриваете мошенничество с SIMD-операциями, вы можете обычно писать сборку SIMD, которая работает намного лучше, чем ваши способности автогенерации компиляторов (если у нее даже есть авторазвитие!)

Вот очень простой SSE (один из наборов инструкций на SIM-карте x86). Это для сборки Visual С++ в режиме онлайн.

Изменить: вот небольшая пара функций, если вы хотите попробовать сами. Это вычисление n-точечного произведения. Один использует встроенные команды SSE 2 (встроенный синтаксис GCC), другой - очень простой C.

Это очень просто, и я был бы очень удивлен, если бы хороший компилятор не мог векторизовать простой цикл C, но если это не так, вы должны увидеть ускорение в SSE2. Версия SSE 2, вероятно, была бы быстрее, если бы я использовал больше регистров, но я не хочу растягивать свои очень слабые навыки SSE:).

 float dot_asm(float *a, float*b, int n)
{
  float ans = 0;
  int i; 
  // I'm not doing checking for size % 8 != 0 arrays.
  while( n > 0) {
    float tmp[4] __attribute__ ((aligned(16)));

     __asm__ __volatile__(
            "xorps      %%xmm0, %%xmm0\n\t"
            "movups     (%0), %%xmm1\n\t"
            "movups     16(%0), %%xmm2\n\t"
            "movups     (%1), %%xmm3\n\t"
            "movups     16(%1), %%xmm4\n\t"
            "add        $32,%0\n\t"
            "add        $32,%1\n\t"
            "mulps      %%xmm3, %%xmm1\n\t"
            "mulps      %%xmm4, %%xmm2\n\t"
            "addps      %%xmm2, %%xmm1\n\t"
            "addps      %%xmm1, %%xmm0"
            :"+r" (a), "+r" (b)
            :
            :"xmm0", "xmm1", "xmm2", "xmm3", "xmm4");

    __asm__ __volatile__(
        "movaps     %%xmm0, %0"
        : "=m" (tmp)
        : 
        :"xmm0", "memory" );             

   for(i = 0; i < 4; i++) {
      ans += tmp[i];
   }
   n -= 8;
  }
  return ans;
}

float dot_c(float *a, float *b, int n) {

  float ans = 0;
  int i;
  for(i = 0;i < n; i++) {
    ans += a[i]*b[i];
  }
  return ans;
}

Ответ 3

Если вы не являетесь сборщиком-гуру, шансы на избиение компилятора очень низки.

Фрагмент из приведенной выше ссылки,

Например, бит-ориентированный "XOR % EAX,% EAX" была самый быстрый способ установить регистр в ноль в ранних поколениях x86, но большинство кода генерируется компиляторы и компиляторы сгенерированная инструкция XOR. Таким образом, ИА дизайнеры решили переместить часто встречающийся компилятор сгенерированные инструкции до фронта логики комбинационного декодирования делая буквальный "MOVL $0,% EAX" команда выполняется быстрее, чем Инструкция XOR.

Ответ 4

Я реализовал простую взаимную корреляцию, используя общую реализацию "Слита С". И ТОГДА, когда это заняло больше времени, чем время, которое у меня было доступно, я прибегал к явному распараллеливанию алгоритма и использованию встроенного процессора, чтобы заставить конкретные инструкции использоваться в вычислениях. В этом конкретном случае время вычисления сократилось s > 30 мс до чуть более 4 мс. У меня было 15-секундное окно для завершения обработки до следующего сбора данных.

Это была оптимизация типа SIMD на процессоре VLWI. Для этого требуется всего около 4 встроенных процессоров, которые в основном являются инструкциями на языке ассемблера, которые дают внешний вид вызова функции в исходном коде. Вы можете сделать то же самое с встроенной сборкой, но управление синтаксисом и регистрами немного лучше с внутренними функциями процессора.

Кроме того, если размер имеет значение, ассемблер является королем. Я пошел в школу с парнем, который написал полноэкранный текстовый редактор менее чем за 512 байт.

Ответ 5

У меня есть алгоритм контрольной суммы, который требует, чтобы слова были повернуты на определенное количество бит. Чтобы реализовать его, у меня есть этот макрос:

//rotate word n right by b bits
#define ROR16(n,b) (((n)>>(b))|(((n)<<(16-(b)))&0xFFFF))

//... and inside the inner loop: 
sum ^= ROR16(val, pos);

Расширение релиза VisualStudio расширяется до этого: (val находится в ax, pos находится в dx, sum находится в bx)

mov         ecx,10h 
sub         ecx,edx 
mov         ebp,eax 
shl         ebp,cl 
mov         cx,dx 
sar         ax,cl 
add         esi,2 
or          bp,ax 
xor         bx,bp 

Более эффективная эквивалентная сборка вручную:

 mov       cl,dx
 ror       ax,cl
 xor       bx,ax

Я не понял, как исправить команду ror из чистого кода "c". Однако... Во время написания этого я помнил встроенные функции компилятора. Я могу сгенерировать второй набор инструкций с помощью:

sum ^= _rotr16(val,pos);

Итак, мой ответ: даже если вы считаете, что можете побить чистый компилятор c, проверьте встроенные функции, прежде чем приступать к встроенной сборке.

Ответ 6

Если вы хотите делать такие вещи, как операции SIMD, вы можете побить компилятор. Это потребует хорошего знания архитектуры и набора инструкций.

Ответ 7

Моя лучшая победа над компилятором была в простой процедуре memcpy... Я пропустил множество базовых настроек (например, мне не нужно было много фрейма стека, поэтому я сохраняю несколько циклов там), и сделал несколько довольно волосатых вещей.

Это было около 6 лет назад, с некоторым проприетарным компилятором неизвестного качества. Мне придется выкопать код, который у меня был, и попробовать его против GCC; Я не знаю, что это может ускориться, но я не исключаю этого.

В конце концов, хотя мой memcpy был в среднем примерно в 15 раз быстрее, чем тот, что был в нашей библиотеке C, я просто сохранил его в заднем кармане, если мне это нужно. Это была игрушка для меня, чтобы играть с сборкой PPC, и ускорение скорости не было необходимым в нашем приложении.