C против vDSP против NEON - Как NEON может быть таким же медленным, как C?
Как может NEON быть таким же медленным, как С?
Я пытался построить быструю функцию гистограммы, которая бы объединяла входящие значения в диапазоны, присваивая им значение - то есть пороговое значение диапазона, к которому они ближе всего. Это то, что будет применяться к изображениям, поэтому оно должно быть быстрым (предположим, массив изображений 640x480, так что 300 000 элементов) . Числа диапазона гистограммы кратны (0,25,50,75,100) . Входные данные будут с плавающей запятой, а конечные выходы, очевидно, будут целыми числами.
Я протестировал следующие версии на xCode, открыв новый пустой проект (без делегата приложения) и просто используя файл main.m. Я удалил все связанные библиотеки, за исключением ускорения.
Вот реализация C: более ранняя версия была в большом количестве, если бы тогда, но вот окончательная оптимизированная логика. это заняло 11 с и 300 мс.
int main(int argc, char *argv[])
{
NSLog(@"starting");
int sizeOfArray=300000;
float* inputArray=(float*) malloc(sizeof(float)*sizeOfArray);
int* outputArray=(int*) malloc(sizeof(int)*sizeOfArray);
for (int i=0; i<sizeOfArray; ++i)
{
inputArray[i]=88.5;
}
//Assume range is [0,25,50,75,100]
int lcd=25;
for (int j=0; j<1000; ++j)// just to get some good time interval
{
for (int i=0; i<sizeOfArray; ++i)
{
//a 60.5 would give a 50. An 88.5 would give 100
outputArray[i]=roundf(inputArray[i]/lcd)*lcd;
}
}
NSLog(@"done");
}
Вот реализация vDSP. Даже с некоторыми утомительными смещениями к целому числу вперед и назад, это заняло всего 6 секунд! улучшение почти на 50%!
//vDSP implementation
int main(int argc, char *argv[])
{
NSLog(@"starting");
int sizeOfArray=300000;
float* inputArray=(float*) malloc(sizeof(float)*sizeOfArray);
float* outputArrayF=(float*) malloc(sizeof(float)*sizeOfArray);//vDSP requires matching of input output
int* outputArray=(int*) malloc(sizeof(int)*sizeOfArray); //rounded value to the nearest integere
float* finalOutputArrayF=(float*) malloc(sizeof(float)*sizeOfArray);
int* finalOutputArray=(int*) malloc(sizeof(int)*sizeOfArray); //to compare apples to apples scenarios output
for (int i=0; i<sizeOfArray; ++i)
{
inputArray[i]=37.0; //this will produce an final number of 25. On the other hand 37.5 would produce 50.
}
for (int j=0; j<1000; ++j)// just to get some good time interval
{
//Assume range is [0,25,50,75,100]
float lcd=25.0f;
//divide by lcd
vDSP_vsdiv(inputArray, 1, &lcd, outputArrayF, 1,sizeOfArray);
//Round to nearest integer
vDSP_vfixr32(outputArrayF, 1,outputArray, 1, sizeOfArray);
// MUST convert int to float (cannot just cast) then multiply by scalar - This step has the effect of rounding the number to the nearest lcd.
vDSP_vflt32(outputArray, 1, outputArrayF, 1, sizeOfArray);
vDSP_vsmul(outputArrayF, 1, &lcd, finalOutputArrayF, 1, sizeOfArray);
vDSP_vfix32(finalOutputArrayF, 1, finalOutputArray, 1, sizeOfArray);
}
NSLog(@"done");
}
Вот неоновая реализация. Это мой первый, так что играйте хорошо! это было медленнее, чем vDSP, и заняло 9 секунд и 300 мс, что для меня не имело смысла. Либо vDSP лучше оптимизирован, чем NEON, или я делаю что-то не так.
//NEON implementation
int main(int argc, char *argv[])
{
NSLog(@"starting");
int sizeOfArray=300000;
float* inputArray=(float*) malloc(sizeof(float)*sizeOfArray);
float* finalOutputArrayF=(float*) malloc(sizeof(float)*sizeOfArray);
for (int i=0; i<sizeOfArray; ++i)
{
inputArray[i]=37.0; //this will produce an final number of 25. On the other hand 37.5 would produce 50.
}
for (int j=0; j<1000; ++j)// just to get some good time interval
{
float32x4_t c0,c1,c2,c3;
float32x4_t e0,e1,e2,e3;
float32x4_t f0,f1,f2,f3;
//ranges of histogram buckets
float32x4_t buckets0=vdupq_n_f32(0);
float32x4_t buckets1=vdupq_n_f32(25);
float32x4_t buckets2=vdupq_n_f32(50);
float32x4_t buckets3=vdupq_n_f32(75);
float32x4_t buckets4=vdupq_n_f32(100);
//midpoints of ranges
float32x4_t thresholds1=vdupq_n_f32(12.5);
float32x4_t thresholds2=vdupq_n_f32(37.5);
float32x4_t thresholds3=vdupq_n_f32(62.5);
float32x4_t thresholds4=vdupq_n_f32(87.5);
for (int i=0; i<sizeOfArray;i+=16)
{
c0= vld1q_f32(&inputArray[i]);//load
c1= vld1q_f32(&inputArray[i+4]);//load
c2= vld1q_f32(&inputArray[i+8]);//load
c3= vld1q_f32(&inputArray[i+12]);//load
f0=buckets0;
f1=buckets0;
f2=buckets0;
f3=buckets0;
//register0
e0=vcgtq_f32(c0,thresholds1);
f0=vbslq_f32(e0, buckets1, f0);
e0=vcgtq_f32(c0,thresholds2);
f0=vbslq_f32(e0, buckets2, f0);
e0=vcgtq_f32(c0,thresholds3);
f0=vbslq_f32(e0, buckets3, f0);
e0=vcgtq_f32(c0,thresholds4);
f0=vbslq_f32(e0, buckets4, f0);
//register1
e1=vcgtq_f32(c1,thresholds1);
f1=vbslq_f32(e1, buckets1, f1);
e1=vcgtq_f32(c1,thresholds2);
f1=vbslq_f32(e1, buckets2, f1);
e1=vcgtq_f32(c1,thresholds3);
f1=vbslq_f32(e1, buckets3, f1);
e1=vcgtq_f32(c1,thresholds4);
f1=vbslq_f32(e1, buckets4, f1);
//register2
e2=vcgtq_f32(c2,thresholds1);
f2=vbslq_f32(e2, buckets1, f2);
e2=vcgtq_f32(c2,thresholds2);
f2=vbslq_f32(e2, buckets2, f2);
e2=vcgtq_f32(c2,thresholds3);
f2=vbslq_f32(e2, buckets3, f2);
e2=vcgtq_f32(c2,thresholds4);
f2=vbslq_f32(e2, buckets4, f2);
//register3
e3=vcgtq_f32(c3,thresholds1);
f3=vbslq_f32(e3, buckets1, f3);
e3=vcgtq_f32(c3,thresholds2);
f3=vbslq_f32(e3, buckets2, f3);
e3=vcgtq_f32(c3,thresholds3);
f3=vbslq_f32(e3, buckets3, f3);
e3=vcgtq_f32(c3,thresholds4);
f3=vbslq_f32(e3, buckets4, f3);
vst1q_f32(&finalOutputArrayF[i], f0);
vst1q_f32(&finalOutputArrayF[i+4], f1);
vst1q_f32(&finalOutputArrayF[i+8], f2);
vst1q_f32(&finalOutputArrayF[i+12], f3);
}
}
NSLog(@"done");
}
PS: это мой первый бенчмаркинг в этом масштабе, поэтому я постарался сделать его простым (большие циклы, постоянный код установки, использование NSlog для печати времени начала / окончания, только ускорение связанного фреймворка) . Если какое-либо из этих предположений значительно влияет на результат, пожалуйста, критикуйте.
Спасибо
3 ответа
Во-первых, это не "НЕОН" как таковой. Это неотъемлемая часть. Почти невозможно получить хорошую производительность NEON, используя встроенные функции в clang или gcc. Если вы считаете, что вам нужны встроенные функции, вы должны написать от руки ассемблер.
vDSP не "лучше оптимизирован", чем NEON. vDSP на iOS использует процессор NEON. Использование NEON в vDSP намного лучше оптимизировано, чем использование NEON.
Я еще не изучил ваш встроенный код, но наиболее вероятная (на самом деле почти наверняка) причина проблемы заключается в том, что вы создаете состояния ожидания. Писать на ассемблере (а встроенные функции - это просто ассемблер, написанный на сварочных перчатках), это не то же самое, что писать на С. Вы не сравниваете то же самое. Вам нужен новый способ мышления. При сборке вы можете делать больше, чем одну вещь за раз (потому что у вас разные логические единицы), но вы абсолютно должны планировать вещи таким образом, чтобы все эти вещи могли работать параллельно. Хорошая сборка сохраняет все эти трубопроводы заполненными. Если вы можете прочитать свой код, и он имеет смысл, это, вероятно, дерьмовый ассемблерный код. Если вы никогда не повторяете себя, это, вероятно, код сборки дерьма. Вам нужно тщательно обдумать, что входит в какой регистр и сколько там циклов, пока вам не разрешат его прочитать.
Если бы это было так же просто, как транслитерация C, то компилятор сделал бы это для вас. В тот момент, когда вы говорите: "Я напишу это в NEON", вы говорите: "Я думаю, что я могу написать лучше NEON, чем компилятор", потому что компилятор тоже его использует. Тем не менее, часто можно написать лучше NEON, чем компилятор (особенно gcc и clang).
Если вы готовы погрузиться в этот мир (и это довольно крутой мир), у вас впереди немного чтения. Вот несколько мест, которые я рекомендую:
- http://www.coranac.com/tonc/text/asm.htm (Вы действительно хотите провести некоторое время с этим)
- http://hilbert-space.de/ (Весь сайт. Он перестал писать слишком рано.)
- На ваш конкретный вопрос он объясняет его здесь: http://hilbert-space.de/?p=22
- Еще немного по вашему конкретному вопросу: Arm Neon Intrinsics vs hand Assembly
- http://wanderingcoder.net/2010/07/19/ought-arm/
- http://wanderingcoder.net/2010/06/02/intro-neon/
- Вы также можете быть заинтересованы в:
ВСЕ, ЧТО СКАЗАЛ... Всегда всегда всегда начинайте с пересмотра вашего алгоритма. Часто ответ не в том, как заставить ваш цикл вычислять быстро, а в том, как не вызывать цикл так часто.
ARM NEON имеет 32 регистра, ширину 64 бита (двойное представление как 16 регистров, ширина 128 бит). Ваша неоновая реализация уже использует по крайней мере 18 128-битной ширины, поэтому компилятор будет генерировать код для перемещения их назад и вперед из стека, и это не хорошо - слишком большой доступ к дополнительной памяти.
Если вы планируете поиграть со сборкой, я считаю, что лучше всего использовать инструмент для вывода инструкций в объектные файлы. Один называется objdump
в линуксе я считаю это называется otool
в мире Apple. Таким образом, вы можете увидеть, как выглядит полученный машинный код и что компилятор сделал с вашими функциями.
Ниже приведена часть дампа вашей неоновой реализации из gcc (-O3) 4.7.1. Вы можете заметить загрузку четырех регистра через vldmia sp, {d8-d9}
,
1a6: ff24 cee8 vcgt.f32 q6, q10, q12
1aa: ff64 4ec8 vcgt.f32 q10, q10, q4
1ae: ff2e a1dc vbit q5, q15, q6
1b2: ff22 ceea vcgt.f32 q6, q9, q13
1b6: ff5c 41da vbsl q10, q14, q5
1ba: ff20 aeea vcgt.f32 q5, q8, q13
1be: f942 4a8d vst1.32 {d20-d21}, [r2]!
1c2: ec9d 8b04 vldmia sp, {d8-d9}
1c6: ff62 4ee8 vcgt.f32 q10, q9, q12
1ca: f942 6a8f vst1.32 {d22-d23}, [r2]
Конечно, все зависит от компилятора, лучший компилятор может избежать этой ситуации, более четко используя доступные регистры.
Таким образом, в конце концов вы попадаете в зависимость от компилятора, если вы не используете сборку (встроенную, автономную) или должны постоянно проверять вывод компилятора, пока не получите то, что хотите от него.
В качестве дополнения к ответу Роба, написание NEON само по себе является искусством (кстати, спасибо за добавление моих постов Wandering Coder) и ответом auselen (что вы действительно используете слишком много регистров в любой момент времени, что приводит к разливу), я должен добавить, что ваш алгоритм присваивания является более общим, чем два других: он допускает произвольные диапазоны, а не только кратные, поэтому вы пытаетесь сравнить вещи, которые не сравнимы. Всегда сравнивайте апельсины с апельсинами; за исключением того, что было бы справедливо сравнивать пользовательский алгоритм, более специфичный, чем стандартный, если вам нужны только специфические особенности пользовательского. Так что это еще один способ, с помощью которого алгоритм NEON может быть таким же медленным, как и алгоритм C: если это не тот же алгоритм.
Что касается ваших потребностей в гистограмме, используйте то, что вы создали с помощью vDSP, и только в том случае, если производительность не подходит для вашего приложения, только тогда изучите оптимизацию другим способом; способы сделать это, помимо использования инструкций NEON, будут включать в себя предотвращение слишком большого перемещения памяти (вероятно, узкого места в реализации vDSP) и увеличение счетчиков для каждого сегмента при просмотре пикселей вместо того, чтобы этот промежуточный вывод был сделан по принуждению ценности. Эффективный код DSP касается не только самих вычислений, но и того, как наиболее эффективно использовать пропускную способность памяти и так далее. Тем более на мобильных устройствах: ввод-вывод памяти, даже для кешей, требует больше энергии, чем операции внутри процессора, поэтому обе шины ввода-вывода, как правило, работают с меньшей частотой тактовой частоты процессора. Таким образом, у вас не так много пропускной способности памяти, чтобы играть, и вы должны разумно использовать пропускную способность памяти, которая у вас есть, так как любое ее использование требует энергии.