Я разрабатываю некоторое научное программное обеспечение для своего университета. Это пишется в C++ в Windows (VS2008). Алгоритм должен вычислить некоторые значения для большого количества матричных пар, то есть, в ядре находится итерация цикла по матрицам, собирая некоторые данные, например:
sumA = sumAsq = sumB = sumBsq = diffsum = diffsumsq = return = 0;
for (int y=0; y < height; ++y)
{
for (int x=0; x < width; ++x)
{
valA = matrixA(x,y);
valB = matrixB(x,y);
sumA+=valA;
sumAsq+=valA*valA;
sumB+=valB;
sumBsq+=valB*valB;
diffsum+=valA-valB;
diffsumsq+=(valA-valB)*(valA-valB);
}
}
return = sumA + sumB / sumAsq + sumBsq * diffsum * diffsumsq
Эта стандартная программа является выполняемыми миллионами времен для другого matrixA, matrixB пары. Моя проблема состоит в том, что эта программа является чрезвычайно медленной, скомпилирована в режиме Release со всей активированной оптимизацией. Используя "pause-when-busy-and-inspect" метод отладчика, я установил, что программа находится в этом цикле фактически каждый раз, даже при том, что, как Вы могли бы ожидать, эта стандартная программа окружается целым набором ответвлений управления и условий. Что озадачивает меня, большинство - то, что во время его выполнения в двухпроцессорной основанной на xeon системе, программа использует одно из этих 4 ядер (не удивительно, это является однопоточным на данный момент), но только приблизительно до 25% его предела, и с относительно большими колебаниями, где я ожидал бы устойчивую, 100%-ю загрузку, пока программа не завершается.
Текущая версия является на самом деле переписыванием, созданный с оптимизацией производительности в памяти. Я был опустошен, когда я узнал, что это на самом деле медленнее, чем оригинал. Предыдущая версия использовала матрицы Повышения, которые я заменил матрицами OpenCV, установив их, чтобы быть более чем в 10 раз быстрее в сравнении времени выполнения умножения два 1000x100 матрицы. Я получаю доступ к матрице путем ручного разыменования необработанного указателя на его данные, которые я надеялся, получит меня некоторая производительность. Я сделал стандартную программу вычисления мультилинией #define макрос, чтобы осуществить его встраивание и избежать вызовов функции и возвратов. Я улучшил математику позади вычислений так, чтобы окончательное значение было вычислено в единственной передаче через матрицы (старая версия требует двух передач). Я ожидал получать огромные усиления, и все же противоположное верно. Я нигде не около эффективности моей старой программы, не говоря уже о коммерческом программном обеспечении для конкретного приложения.
Я задавался вопросом, имело ли это, возможно, некоторое отношение к матричным данным, являющимся 8-разрядными символами, я однажды видел, что доступ к плаваниям был на самом деле медленнее, чем к, удваивается в моей старой программе, возможно, символы еще медленнее, так как процессор получает данные в 32-разрядных блоках (этот Xeon, вероятно, захватывает даже 64 бита). Я также полагал, что превращение матриц в векторы избежало конструкции цикла в цикле, а также некоторой формы векторизации, как, например, вычисление данных для 4 (меньше?еще?) последовательные ячейки матрицы на единственном повторении цикла. Какие-либо другие идеи?
Править: фактический код в новой, основанной на OpenCV версии:
const char *Aptr, *Bptr;
double sumA = 0, sumB = 0, sumAsq = 0, sumBsq = 0, diffsum = 0, diffsumsq = 0;
char Aval, Bval;
for (int y=0; y < height; ++y)
{
Aptr = (char*)(AMatrix.imageData + AMatrix.widthStep * y);
Bptr = (char*)(BMatrix.imageData + BMatrix.widthStep * y);
for (int x=0; x < width; ++x)
{
Aval = Aptr[x];
Bval = Bptr[x];
sumA+=Aval;
sumB+=Bval;
sumAsq+=Aval*Aval;
sumBsq+=Bval*Bval;
diffsum+=Aval-Bval;
diffsumsq+=(Aval-Bval)*(Aval-Bval);
}
}
Если вы используете технику «паузы», она должна сказать вам больше, чем просто то, что вы находитесь в этом цикле. Он должен сказать вам, где находится петля.
Никогда не угадай, если просто узнаешь. Тем не менее, вот мое предположение :-) Вы делаете все суммирование в переменных с плавающей запятой, но получаете исходные числа как целые символы, верно? Тогда вы можете ожидать, что преобразование из int в double займет некоторое время, и если да, то большую часть времени вы будете видеть, что ваши паузы происходят в этих инструкциях. Так что в основном мне интересно, почему вы не делаете все это в целочисленной арифметике.
Вы говорите, что коэффициент использования никогда не превышает 25%. Может ли это быть из-за того, что используется только одно из 4 ядер?
Вы говорите, что загрузка часто падает ниже 25%. Это говорит о том, что, возможно, поток блокирует выполнение файлового ввода-вывода. Если это так, ваши паузы должны уловить это в действии и подтвердить это. Если это так, вы могли бы ускорить ввод-вывод, используя большие блоки или, возможно, открывая / закрывая реже.Обратите внимание, что улучшения вашего внутреннего цикла сократят время, затрачиваемое на этот цикл, но не сократят время, затрачиваемое на ввод-вывод, поэтому процент времени, затрачиваемого на ввод-вывод, увеличится (вызывая очевидное снижение использования) , пока вы не уменьшите его размер.
Использование на самом деле не очень полезное число. На самом деле это всего лишь индикатор разделения ЦП / ввода-вывода, и он вообще не говорит вам, слишком много вы делаете из того или другого.
Как сказал @renick, избавьтесь от вычисления адресов. Вы должны быть в состоянии пройти через этот цикл на уровне языка ассемблера и увидеть, что он не делает ничего больше, чем если бы вы надели шляпу «гуру» и написали сборку самостоятельно.
В любом случае векторизация может быть большим выигрышем.
Разные мысли:
diffsum
внутри цикла? Похоже, что вы могли бы сделать diffsum = sumA-sumB
один раз вне цикла, но могут быть числовые соображения, которые не позволят вам сделать это. Вам следует попытаться избавиться от циклов и вместо этого попробовать векторизовать операции. Используя такую библиотеку, как Eigen , ваш код будет выглядеть примерно так:
Eigen::MatrixXd matrixA(height, width);
Eigen::MatrixXd matrixB(height, width);
double sumA = matrixA.sum();
double sumAsq = matrixA.cwise().square().sum();
double sumB = matrixB.sum();
double sumBsq = matrixB.cwise().square().sum();
Eigen::MatrixXd diff = matrixA - matrixB;
double diffSum = diff.sum();
double diffSumSq = diff.cwise().square().sum();
return sumA + sumB / sumAsq + sumBsq * diffSum * diffSumSq;
«25% от своего предела и с относительно большими колебаниями, при которых я ожидал бы устойчивой, 100% -ной нагрузки, пока программа не завершится».
Вы упомянули, что функция окружена целым набором условий и управляющих ветвей, поэтому Я предполагаю, что это приводит к тому, что конвейер процессора очищается, а не используется эффективно. Попробуйте переписать свое программное обеспечение, чтобы оно не требовало большого количества ветвлений.
Я бы также рекомендовал использовать одну из математических библиотек, например Eigen , ATLAS или GSL
Ваш внутренний цикл вызывает функции! Независимо от того, насколько они тривиальны, вы платите большой штраф. Вы должны попытаться линеаризовать обращения к матрице (по сути, сделать их одномерными), чтобы вы могли обращаться к ним с помощью простого разыменования указателей
vala = *matrixA++;
valb = *matrixB++;
и, поскольку вы выполняете простые сложения и вычитания, посмотрите на SSE/SSE2 и т.д. в зависимости от возможностей вашего целевого процессора и вашей арифметики (целочисленной, с плавающей точкой и т.д.).
EDIT: MMX SSE2 intrinsics - это функции, которые отображаются один к одному с SIMD инструкциями CPU. Посмотрите эти страницы microsoft, чтобы начать, и дополнительно я советую заглянуть на сайт Intel для IA-32/ Intel64 руководства программиста или аналогичные руководства от AMD.
Я также очень рекомендую эту книгу по оптимизации для архитектур Intel. Она объяснит все скрытые возможности вашего процессора и компилятора.
Можете ли вы проверить ассемблерный код, генерируемый этим циклом? Если вы используете только 25% процессора, возможно, этот цикл связан с памятью. Там около восьми локальных переменных, и я полагаю, что компилятор не отображает их все в регистры, поэтому в каждом цикле выполняется много операций с памятью. Одно из соображений - написать этот цикл на ассемблере.
Почему вы проходите по матрице столбец за столбцом? Матрицы будут храниться в памяти строка за строкой, поэтому, если вы обращаетесь ко всему столбцу во внутреннем цикле, вы, вероятно, запрашиваете дополнительную загрузку памяти на разные уровни памяти (кеши и т. Д.).
Если бы я был на вашем месте, я бы попытался выяснить, что именно вызывает разницу в производительности между старым и новым кодом. Возможно, матрицы boost используют какое-то кэширование или ленивую/нетерпеливую оценку.
Сохранять матрицы с такими же параметрами вне цикла? Я думаю, что тебе нужно немного сэкономить.
TLDR: сначала оптимизируйте алгоритм умножения матриц, затем следите за количеством темпоралов, затем оптимизируйте внутренние распределения матриц.
Длинный ответ:
Я думаю, что наиболее важным моментом является оптимизация умножения матриц. Умножение матриц для самого интуитивного алгоритма - это O(n^3) (что огромно даже для маленьких матриц).
Для примера, для умножения матрицы 2x2 у вас есть 16 операций умножения ("мо"). Для умножения матрицы 3x3 у вас есть 27 mo, а для 4x4 - 64 mo.
Я не уверен, как это реализовано в вашем случае, но если это интуитивный алгоритм (как тройной for
цикл), изменение этого алгоритма на матричное умножение с использованием LU-разложенных матриц должно резко увеличить вашу производительность.
Это связано с тем, что, получив разложенные матрицы, вы можете сильно оптимизировать алгоритм умножения (нет смысла перемножать строки и столбцы для нулевых элементов).
Далее, рассмотрите возможность использования кэшированных значений вместо повторения операций сложения в diffsumsq
:
старый код:
diffsum+=valA-valB; // compute difference once
diffsumsq+=(valA-valB)*(valA-valB); // compute it two more times, then multiply
новый код:
diff = valA-valB; // compute difference once
diffsum+= diff; // use computed difference
diffsumsq+=diff*diff; // just multiply
Второй вариант в три раза быстрее на вычислении разности (два for
цикла - операции x* y выполняются только один раз вместо трех).
Далее можно следить за количеством временных объектов: каждая бинарная операция создает временный объект (что означает выделение в памяти еще одной матрицы x*y и копирование значений). Например, выражение:
diffsumsq+=(valA-valB)*(valA-valB);
создает временный объект для разности в первой парантезе, затем еще один для разности во второй, затем еще один для произведения.
В моем примере лучше написать
diff = valA;
diff -= valB;
вместо
diff = valA - valB;
так как таким образом вы избежите выделения временного, которое присваивается diff (во втором варианте).
Еще одна вещь, за которой нужно следить, это потребление памяти: поскольку эта функция выполняется часто, вы можете либо предварительно выделить память, либо объединить используемые матрицы и повторно использовать память вместо создания новых объектов.
Я уверен, что есть и другие вещи, за которыми нужно следить.
Edit: Как вы можете умножать матрицы? Нужно, чтобы они сопоставлялись по столбцам x строкам. То есть, количество столбцов в valA должно быть равно количеству строк в valB (если я правильно помню свои умножения матриц).
Еще один момент:
Я сделал процедуру вычисления в виде многострочный макрос #define, чтобы обеспечить ее инлайнинг и чтобы избежать вызовов и возвратов функций.
Для оптимизации кода на C++ макросы не нужны. Чтобы избежать вызовов и возвратов функций, используйте встроенные
функции. Макросы имеют свои проблемы.
Вам также следует попробовать, если у вас нет возможности многопоточности цикла с помощью простой настройки, такой как OpenMP. использование ЦП на 25% звучит как четырехъядерный процессор, на котором работает один рабочий поток.