Оптимизиране на преобразуването на цветовете 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, *резултатът е подравнен с размера на cacheline, редувайте зареждания и съхранява, докато правите това, направете време и намерете сладкото оформление, след което поставете някои 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µs (в сравнение с 6000µs). Това съотношение (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µs и получавам около 1800µs.   -  person jmh    schedule 07.02.2014
comment
@auselen Току-що се опитах да използвам memcpy между оригинала и резултата и отнема по-малко от 10µs... Имам голяма граница на напредък!   -  person jmh    schedule 07.02.2014


Отговори (1)


Дължината на реда на кеша е фиксирана на осем думи (32 байта). В допълнение към pld, което имате в момента, имате нужда от pld[lineEven+cacheLine]. Пропуските са vld4.8 {d8-d11}, което е втората половина на lineEven. pld ще извлече само ред от кеша. Освен това трябва да промените позицията pld. Поставете един в началото и друг преди vhadd, може би със следващата цел на паметта. След това ALU и паметта са активни паралелно.

Освен това вмъкнете 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 ще бъдат заредени преди други и ще бъдат готови за използване. В противен случай вашето ALU (vhadd) ще блокира изчакването за зареждане на данните.

Може също така да пожелаете да напълнете цикъла с pld[lineOdd] и pld[lineEven], преди нещата да започнат.

person artless noise    schedule 06.02.2014
comment
Разстоянието за предварително зареждане няма нищо общо с дължината на линията на кеша; става въпрос за това колко време трябва на системата да прочете паметта, така че да е готова, когато вашият код се нуждае от нея. - person BitBank; 06.02.2014
comment
PLD за Cortex-A8 и PLD за Cortex- A5; и двамата говорят за кешове и кеш линии. Освен това данните, представени от ОП, подкрепят факта, че липсва второто полувреме. Вижте също Шаблон за копиране на Linux с неговите PLD стойности. - person artless noise; 07.02.2014
comment
Вярно – трябва да бъдат издадени множество инструкции за предварително зареждане, за да се зареди обработваното количество. Предполагам, че изтълкувах погрешно отговора ви като намекващ, че трябва да е дължината на реда в кеша. - 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() за cortex-A8, който ограничава PLD до размер L2. Предполагам, че L1 miss не е толкова скъп. Забележете, че извличат предварително много предварително. Мисля, че крачката на вашето HD видео (5*256) причинява случайни пропуски на L2. - person artless noise; 08.02.2014