Оптимизация преобразования цветов Cortex-A8 с использованием NEON

В настоящее время я выполняю процедуру преобразования цвета, чтобы преобразовать YUY2 в NV12. У меня есть функция, которая работает довольно быстро, но не так быстро, как я ожидал, в основном из-за промахов кеша.

void convert_hd(uint8_t *orig, uint8_t *result) {
uint32_t width          = 1280;
uint32_t height         = 720;
uint8_t *lineOdd        = orig;
uint8_t *lineEven       = orig + width*2;
uint8_t *resultYOdd     = result;
uint8_t *resultYEven    = result + width;
uint8_t *resultUV       = result + height*width;
uint32_t totalLoop      = height/2;

while (totalLoop-- > 0) {
  uint32_t lineLoop = 1280/32; // Bytes length: width*2, read by iter 16Bytes

  while(lineLoop-- > 0) {
    __asm__ __volatile__(
        "pld [%[lineOdd]]   \n\t"
        "vld4.8   {d0, d1, d2, d3}, [%[lineOdd],:128]!   \n\t" // d0:Y d1:U0 d2:Y d3:V0
        "pld [%[lineEven]]   \n\t"
        "vld4.8   {d4, d5, d6, d7}, [%[lineOdd],:128]!   \n\t" // d4:Y d5:U1 d6:Y d7:V1
        "vld4.8   {d8, d9, d10, d11}, [%[lineEven],:128]!  \n\t" // d8:Y d9:U0' d10:Y d11:V0'
        "vld4.8   {d12, d13, d14, d15}, [%[lineEven],:128]!  \n\t" // d12:Y d13:U1' d14:Y d15:V1'
        "vhadd.u8   d1, d1, d9    \n\t" // (U0+U0') / 2
        "vhadd.u8   d3, d3, d11    \n\t" // (V0+V0') / 2
        "vhadd.u8   d5, d5, d13    \n\t" // (U1+U1') / 2
        "vhadd.u8   d7, d7, d15    \n\t" // (V1+V1') / 2
        // Save
        "vst2.8 {d0, d2}, [%[resultYOdd],:128]!           \n\t"
        "vst2.8 {d4, d6}, [%[resultYOdd],:128]!           \n\t"
        "vst2.8 {d8, d10}, [%[resultYEven],:128]!          \n\t"
        "vst2.8 {d12, d14}, [%[resultYEven],:128]!          \n\t"
        "vst2.8 {d1, d3}, [%[resultUV],:128]!   \n\t"
        "vst2.8 {d5, d7}, [%[resultUV],:128]!   \n\t"
        : [lineOdd]"+r"(lineOdd), [lineEven]"+r"(lineEven), [resultYOdd]"+r"(resultYOdd), [resultYEven]"+r"(resultYEven), [resultUV]"+r"(resultUV)
        :
        : "memory"
    );
  }
  lineOdd += width*2;
  lineEven += width*2;
  resultYOdd += width;
  resultYEven += width;
}
}

Когда я спрашиваю oprofile, что занимает время, он говорит следующее:

                                           :    220c:   add r2, r0, #2560   ;
                                           :    2210:   add r3, r1, #1280   ;
                                           :    2214:   add ip, r1, #921600 ;
                                           :    2218:   push    {r4, lr}
                                           :    221c:   mov r4, #360    ;
 6  0.1243    10  0.5787     4  0.4561     :    2220:   mov lr, #40 ; 0x28
 9  0.1864     5  0.2894     0       0     :    2224:   pld [r0]
45  0.9321     7  0.4051     3  0.3421     :    2228:   vld4.8  {d0-d3}, [r0 :128]!
51  1.0563     7  0.4051     1  0.1140     :    222c:   pld [r2]
 1  0.0207     1  0.0579     0       0     :    2230:   vld4.8  {d4-d7}, [r0 :128]!
1360 28.1690   770 44.5602   463 52.7936     :    2234: vld4.8  {d8-d11}, [r2 :128]!
 980 20.2983   329 19.0394   254 28.9624     :    2238: vld4.8  {d12-d15}, [r2 :128]!
                                             :    223c: vhadd.u8    d1, d1, d9
1000 20.7125   170  9.8380   104 11.8586     :    2240: vhadd.u8    d3, d3, d11
                                             :    2244: vhadd.u8    d5, d5, d13
   5  0.1036     2  0.1157     2  0.2281     :    2248: vhadd.u8    d7, d7, d15
                                             :    224c: vst2.8  {d0,d2}, [r1 :128]!
1125 23.3016   293 16.9560    15  1.7104     :    2250: vst2.8  {d4,d6}, [r1 :128]!
  34  0.7042    41  2.3727     0       0     :    2254: vst2.8  {d8,d10}, [r3 :128]!
  74  1.5327     8  0.4630     0       0     :    2258: vst2.8  {d12,d14}, [r3 :128]!
  60  1.2428    39  2.2569     6  0.6842     :    225c: vst2.8  {d1,d3}, [ip :128]!
  53  1.0978    24  1.3889    14  1.5964     :    2260: vst2.8  {d5,d7}, [ip :128]!
                                             :    2264: subs    lr, lr, #1
   0       0     0       0     1  0.1140     :    2268: bne 2224 <convert_hd+0x18>
  11  0.2278    14  0.8102    10  1.1403     :    226c: subs    r4, r4, #1
                                             :    2270: add r0, r0, #2560   ;
                                             :    2274: add r2, r2, #2560   ;
   2  0.0414     6  0.3472     0       0     :    2278: add r1, r1, #1280   ;
                                             :    227c: add r3, r3, #1280   ;
   2  0.0414     1  0.0579     0       0     :    2280: bne 2220 <convert_hd+0x14>
                                             :    2284: pop {r4, pc}
  • первые два столбца - количество циклов (абсолютное и относительное)
  • два следующих - промахи кеша L1 (абсолютный и относительный)
  • последние - промахи кэша L2 (абсолютные и относительные)

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

Спасибо !


person jmh    schedule 06.02.2014    source источник
comment
Одна вещь, которую можно улучшить, — это использование предварительной загрузки кеша. Используя PLD [R2], вы говорите системе предварительно загрузить память, из которой вы собираетесь немедленно читать. Что вам нужно сделать, так это указать ему предварительно загружать данные перед тем, где вы читаете, чтобы они были готовы, когда они вам понадобятся (например, PLD [R2, #0x200]).   -  person BitBank    schedule 06.02.2014
comment
@BitBank Я тоже пробовал это, но все же у меня много промахов кеша на r2 (lineEven).   -  person jmh    schedule 06.02.2014
comment
Невозможно избежать промахов кеша, потому что процессор намного быстрее памяти. Оптимизируя расстояние предварительного натяга как для R0, так и для R2, ​​вы можете свести к минимуму промахи. Поэкспериментируйте с разными расстояниями, пока не найдете лучшее. Обычно это может ускорить процесс на 20-25%.   -  person BitBank    schedule 06.02.2014
comment
Что-то еще, что ускорит процесс, — это развернуть цикл еще раз. Вы используете только 1/2 регистров NEON.   -  person BitBank    schedule 06.02.2014
comment
@BitBank, с другой стороны, может быть, у него только 16D регистров, как в VFPv3-D16?   -  person auselen    schedule 07.02.2014
comment
Вы не можете избежать промахов кеша. Вы можете улучшить коэффициент промахов кеша. Что описывает то, чего вы пытаетесь достичь. Если вы действительно в этом заинтересованы, начните с удаления PLD, превратите цикл в один, убедитесь, что *origin, *result выровнены по размеру кэш-линии, чередуйте загрузки и сохранения, делая так, чтобы определить время и найти приятный макет, затем поместите несколько PLD. PLD всегда должны быть очень впереди того, что вы делаете в настоящее время, но не слишком далеко. Если бы это был не Cortex-A8, я бы сказал, что полностью отказался от этого, поскольку его трудно понять при общем / широком использовании на разных ядрах.   -  person auselen    schedule 07.02.2014
comment
@auselen Спасибо за ваши советы. Я хочу иметь возможность превратить цикл в один, но как я могу этого добиться? Потому что разворачивание довольно тяжелое (цикл сейчас вызывается 40 раз). Я могу использовать до 32 регистров d, поэтому я могу вызвать цикл 20 раз... *origin и *result выровнены по 128 битам. На Cortex-A8 я вижу строки кэша размером 64 байта L1 и для L2 Вы думаете, что я должен выровнять источник и результат по 64 байта?   -  person jmh    schedule 07.02.2014
comment
@jmh Я имел в виду, что у вас есть два цикла while, и на самом деле это может не помочь, но для этого следует проверить дизассемблирование (не так сложно) - не разворачивайте без необходимости. Вероятно, выравнивание по 64 байтам помогает, но лучше сделать шаг назад и проверить, насколько хороша пропускная способность вашей памяти, возможно, создав memcpy с помощью vld/vst. Затем сравните его со своими ожиданиями. Также не забывайте, что вы не выполняете линейную операцию копирования с вашим алгоритмом, а слегка разбросаны.   -  person auselen    schedule 07.02.2014
comment
Я сказал, что он выровнен по 128 битам, но он выровнен по 128 байтам. Вот сгенерированный ассемблерный код pastebin.com/gdrnZhBA   -  person jmh    schedule 07.02.2014
comment
@auselen Если мое изображение 640x360, а не 1280x720, с использованием того же алгоритма требуется 600 мкс (по сравнению с 6000 мкс). Это соотношение (в 10 раз быстрее) критично, так как размер изображения делится только на 4...   -  person jmh    schedule 07.02.2014
comment
@jmh не можете ли вы разделить свой алгоритм для обработки 1280x720, например 4 640x360?   -  person auselen    schedule 07.02.2014
comment
@auselen, я реализую это прямо сейчас;)   -  person jmh    schedule 07.02.2014
comment
@auselen Я попытался использовать алгоритм с изображением 1280x720 в качестве входных данных и вызвал функцию, которая обрабатывает 640x360, только один раз. И я вижу очень большую разницу, у меня должно быть около 600 мкс, а у меня получается около 1800 мкс.   -  person jmh    schedule 07.02.2014
comment
@auselen Я только что попытался использовать memcpy между исходным кодом и результатом, и это занимает менее 10 мкс ... У меня большой запас хода!   -  person jmh    schedule 07.02.2014


Ответы (1)


Длина строки кэша фиксирована и составляет восемь слов (32 байта). В дополнение к pld, который у вас есть сейчас, вам нужно pld[lineEven+cacheLine]. Промахи: vld4.8 {d8-d11}, то есть вторая половина lineEven. pld будет извлекать только строку кэша. Кроме того, вы должны изменить позицию pld. Поместите один в начало, а другой перед vhadd, возможно, со следующей целью памяти. Затем у вас параллельно активны АЛУ и блоки памяти.

Кроме того, чередуйте vst2.8 {d0, d2} с vhadd; Похоже, что большинство данных - это передача памяти. vhadd будет блокировать зависимости данных, такие как d9, которые вы можете или не можете загружать из pld, но не планируете.

Я не так хорошо знаком с NEON, но нижеследующее является попыткой следовать тому, что я сказал.

__asm__ __volatile__(
    "pld [%[lineOdd], #32]\n\t" // 2nd part of odd.
    "vld4.8   {d0, d1, d2, d3}, [%[lineOdd],:128]!\n\t"
    "pld [%[lineEven], #32]\n\t" // 2nd part of even.
    "vld4.8   {d8, d9, d10, d11}, [%[lineEven],:128]!\n\t"
    "vld4.8   {d4, d5, d6, d7}, [%[lineOdd],:128]!\n\t"
    "vld4.8   {d12, d13, d14, d15}, [%[lineEven],:128]!\n\t" 
    "vhadd.u8   d1, d1, d9\n\t"
    // First in memory pipe, so write early.
    "vst2.8 {d0, d2}, [%[resultYOdd],:128]!\n\t"  
    "vhadd.u8   d3, d3, d11\n\t"
    "vst2.8 {d8, d10}, [%[resultYEven],:128]!\n\t"
    "vhadd.u8   d5, d5, d13\n\t"
    "vst2.8 {d4, d6}, [%[resultYOdd],:128]!           \n\t"
    "vhadd.u8   d7, d7, d15\n\t"
    "vst2.8 {d12, d14}, [%[resultYEven],:128]!          \n\t"
    "pld [%[lineOdd]]\n\t"   // 1st part of odd.
    "vst2.8 {d1, d3}, [%[resultUV],:128]!   \n\t"
    "pld [%[lineEven]]\n\t"  // 1st part of even.
    "vst2.8 {d5, d7}, [%[resultUV],:128]!   \n\t"
    : [lineOdd]"+r"(lineOdd), [lineEven]"+r"(lineEven),
      [resultYOdd]"+r"(resultYOdd), [resultYEven]"+r"(resultYEven),
      [resultUV]"+r"(resultUV)
    :
    : "memory"
);

В чем я могу ошибаться, так это в скорости операций NEON; Я понятия не имею, насколько широки ваши регистры (64/128), поэтому может понадобиться больше PLD и т. д. Лучше чередовать операции сохранения с добавлениями. В частности, некоторые dX будут загружены раньше других и будут готовы к использованию. В противном случае ваш АЛУ (vhadd) заблокируется в ожидании загрузки данных.

Вы также можете заполнить цикл с помощью pld[lineOdd] и pld[lineEven], прежде чем все начнется.

person artless noise    schedule 06.02.2014
comment
Расстояние предварительной загрузки не имеет ничего общего с длиной строки кэша; речь идет о том, сколько времени системе нужно для чтения памяти, чтобы она была готова, когда ваш код в ней нуждается. - person BitBank; 06.02.2014
comment
True — необходимо выполнить несколько инструкций предварительной загрузки, чтобы загрузить обрабатываемую сумму. Думаю, я неправильно истолковал ваш ответ как подразумевающий, что он должен быть длиной строки кэша. - person BitBank; 07.02.2014
comment
Я только что попробовал ваш код, @artlessnoise, и результат профилирования здесь: pastebin.com/T79H2Sfm Как вы можете видите, это не очень хорошо (да и по времени тоже нехорошо). - person jmh; 07.02.2014
comment
@jmh Переместите vld4.8 {d4-d7} и vld4.8 {d12-d15} вниз к vhadd.u8 d5, d5, d13; это промахи кеша, и до тех пор вам не нужны данные. Я не говорил, что мой код будет лучше; вам определенно нужно четыре pld, а не только два, иначе у вас всегда будут промахи кеша. Вы также можете жонглировать vst и vld и/или попробовать переместить инструкции pld. - person artless noise; 07.02.2014
comment
Вот результат (если я правильно понял ваше требование): pastebin.com/6MmLrU1S - person jmh; 07.02.2014
comment
Это не требования, а просто предложения. Похоже, дела пошли лучше. Вы также можете просто добавить pld [%[lineOdd], #32] и pld [%[lineEven], #32] в начале исходного кода. Это одна из проблем, которые я предложил. Это уменьшит количество промахов кэша L2. Похоже, что последняя версия также уменьшила промахи кэша L2. Является ли алгоритм оптимальным или нет, я не знаю. Я обращался чтобы узнать идеи и избежать промахов кеша.... Ваш код никогда не может быть лучше, чем memcpy(). Т.е. ваша пропускная способность памяти SDRAM; это лучшее, что вы можете ожидать. - person artless noise; 07.02.2014
comment
Кажется намного лучше? 1200 циклов против 4000 у внутреннего контура. Это правильно? - person artless noise; 07.02.2014
comment
Если ваша память, копируемая за цикл, составляет 128 байт, а ваш кеш — 64, то вам нужно изменить на pld[%[lineOdd], #64] и т. д. Это зависит от размера строки вашего кеша и объема памяти, которую вы копируете за цикл. Хотя из данных вашего профиля кажется, что у вас 32-байтные строки кэша. - person artless noise; 07.02.2014
comment
Справочник ARM по memcpy() для кортекса-A8, который ограничивает PLD размером L2. Я думаю, что промах L1 не так уж и дорог. Обратите внимание, что они выполняют предварительную выборку заблаговременно. Я думаю, что шаг вашего HD-видео (5 * 256) иногда вызывает промахи L2. - person artless noise; 08.02.2014