NEON ASM-код работает намного медленнее, чем C-код? - PullRequest
4 голосов
/ 17 мая 2011

Я пытаюсь реализовать оптимизацию Гаусса-Ньютона для конкретной проблемы на iPhone ARM с использованием NEON.Первая функция ниже - моя оригинальная функция C.Второй - это неоновый асм-код, который я написал.Я запускал каждый из них 100 000 раз, а версия NEON занимает в 7-8 раз дольше, чем версия C.Я думаю, что загрузка (vld1.32) - это то, что занимает большую часть времени.Я экспериментировал с удалением некоторых инструкций.

У кого-нибудь есть понимание этой проблемы?Спасибо!

template<class T>
inline void GaussNewtonOperationJtr8x8(T Jtr[8], const T J[8], T residual)
{
    Jtr[0] -= J[0]*residual;
    Jtr[1] -= J[1]*residual;
    Jtr[2] -= J[2]*residual;
    Jtr[3] -= J[3]*residual;
    Jtr[4] -= J[4]*residual;
    Jtr[5] -= J[5]*residual;
    Jtr[6] -= J[6]*residual;
    Jtr[7] -= J[7]*residual;    
}

inline void GaussNewtonOperationJtr8x8_NEON(NFloat Jtr[8], const NFloat J[8], NFloat residual)
{
    __asm__ volatile (
                      // load Jtr into registers
                      "vld1.32   {d0-d3}, [%0]\n\t"
                      // load J into registers
                      "vld1.32   {d4-d7}, [%1]\n\t"
                      // load residual in register
                      "vmov.f32  s16, %2\n\t"
                      // Jtr -= J*residual
                      "vmls.f32  q0, q2, d8[0]\n\t"
                      "vmls.f32  q1, q3, d8[0]\n\t"
                      // store result
                      "vst1.32   {d0-d3}, [%0]\n\t"
                      // output
                      :
                      // input
                      : "r"(Jtr), "r"(J), "r"(residual)
                      // registers
                      : "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "d10", "d11", "d12", "d13", "d14"
                      );
}

Ответы [ 4 ]

5 голосов
/ 04 ноября 2011
  1. Не используйте d8-d15.Они должны быть сохранены в стек перед использованием.И восстановлен после.Компилятор поместит инструкции, делающие это, тратя впустую ценные циклы.
  2. Загрузка J до Jtr.Jtr ожидается на более поздней стадии конвейера, чем J.
  3. Используйте VLDMIA / VSTMIA вместо VLD / VST.VLDMIA / VSTMIA работает быстрее и имеет преимущества для конвейера.
  4. Используйте векторно-векторное умножение вместо векторно-скалярного умножения.
  5. Если вы создаете зацикленную версию, поместите pld в начале и развернитецикл так, что 64 байта считываются из каждого указателя за одну итерацию.

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

Молодец.

{

__asm__ volatile (
    // load residual in register
    "vdup.32  q12, %2\n\t"
    // load J into registers
    "vldmia   %1, {q10-q11}\n\t"
     // load Jtr into registers
    "vldmia   %0, {q8-q9}\n\t"
    // Jtr -= J*residual
    "vmls.f32  q8, q10, q12\n\t"
    "vmls.f32  q9, q11, q12\n\t"
    // store result
    "vstmia   %0, {q8-q9}\n\t"
    // output
    :
    // input
    : "r"(Jtr), "r"(J), "r"(residual)
    // registers
    : "q8", "q9", "q10", "q11", "q12"
);
3 голосов
/ 18 мая 2011

Вы переключаетесь между инструкциями NEON и VFP.Существует штраф за это как на Cortex-A8, так и на A9.Избавьтесь от этой инструкции VFP vmov.f32, а также убедитесь, что этот код не встроен в места, где используется код VFP, за исключением случаев, когда существует длинный прогон такого кода для оправдания переключения контекста конвейера.

3 голосов
/ 17 мая 2011

Сам компилятор оптимизирует сборку, генерируемую кодом Си. Он просто не переводит один код в другой.

То, что вы пытаетесь сделать, это сделать лучшую оптимизацию, чем компилятор (оу). Знаете ли вы, по крайней мере, что такое ассемблерный код, который генерирует компилятор для кода C выше? Ну, вы должны, если вы хотите, чтобы ваш ассемблерный код был лучше.

EDIT:

В этой теме есть отличная дискуссия о таких вещах: Почему ARM NEON не быстрее простого C ++?

1 голос
/ 30 мая 2011

Ваша версия C ++ на самом деле использует float? Я не могу сказать, потому что вы только дали шаблон и не показали, какой экземпляр вы использовали. Очень странно, что NEON будет значительно медленнее, чем VFP на Cortex-A8 для этого кода, но для u32s я мог видеть, что это может сработать таким образом.

Я не знаю, что такое ABI, но могут быть некоторые издержки при передаче остатка (то есть, что делает компилятор, чтобы поместить его в этот регистр% 2). Попробуйте вместо этого использовать указатель и использовать vld1 для одноэлементного - вы можете загрузить только один float в NEON таким образом.

Вы получите лучшую производительность от массивов, если будете использовать выровненные по 16 байтов нагрузки и хранилища, но вам, возможно, придется поиграть в некоторые игры, чтобы заставить входы работать таким образом. К сожалению, вы никогда не получите действительно большую производительность из-за этого, потому что вы не избежите большей части задержки инструкции vmls, которая является длительной (из-за цепочки умножения NEON и добавления конвейеров конец в конец). Это хуже из-за того, что зависимая инструкция является хранилищем, которое нужно вводить на ранней стадии в конвейере NEON. В идеале вы сможете выполнять несколько таких операций одновременно и чередовать несколько экземпляров вместе - столько, сколько сможете вписать в регистры.

...