Я разрабатываю оптимизации для своих 3D-расчетов, и теперь у меня есть:
plain
«версия с использованием стандартных библиотек языка C,SSE
оптимизированная версия, которая компилируется с использованием препроцессора #define USE_SSE
,AVX
оптимизированная версия, которая компилируется с использованием препроцессора #define USE_AVX
Можно ли переключаться между 3 версиями, не компилируя разные исполняемые файлы (например, имея разные библиотечные файлы и динамически загружая «правильную», не знаю, если inline
функции «правильные» для этого)?
Я бы также подумал о возможностях такого переключения в программном обеспечении.
Есть несколько решений для этого.
Один основан на C ++, где вы создаете несколько классов — как правило, вы реализуете интерфейсный класс и используете фабричную функцию, чтобы получить объект правильного класса.
например
class Matrix
{
virtual void Multiply(Matrix &result, Matrix& a, Matrix &b) = 0;
...
};
class MatrixPlain : public Matrix
{
void Multiply(Matrix &result, Matrix& a, Matrix &b);
};void MatrixPlain::Multiply(...)
{
... implementation goes here...
}
class MatrixSSE: public Matrix
{
void Multiply(Matrix &result, Matrix& a, Matrix &b);
}
void MatrixSSE::Multiply(...)
{
... implementation goes here...
}
... same thing for AVX...
Matrix* factory()
{
switch(type_of_math)
{
case PlainMath:
return new MatrixPlain;
case SSEMath:
return new MatrixSSE;
case AVXMath:
return new MatrixAVX;
default:
cerr << "Error, unknown type of math..." << endl;
return NULL;
}
}
Или, как предложено выше, вы можете использовать общие библиотеки, которые имеют общий интерфейс, и динамически загружать правильную библиотеку.
Конечно, если вы реализуете базовый класс Matrix в качестве своего «простого» класса, вы можете выполнять пошаговое уточнение и реализовывать только те части, которые вы на самом деле считаете полезными, и полагаться на базовый класс для реализации функций, в которых производительность не очень критична.
Редактировать:
Вы говорите о встроенном, и я думаю, что вы смотрите на неправильный уровень функции, если это так. Вам нужны довольно большие функции, которые что-то делают с небольшим объемом данных. В противном случае все ваши усилия будут потрачены на подготовку данных в правильном формате, а затем на выполнение нескольких инструкций по вычислению, а затем на возврат данных в память.
Я также хотел бы рассмотреть, как вы храните свои данные. Вы храните наборы массивов с X, Y, Z, W, или вы храните много X, много Y, много Z и много W в отдельных массивах [при условии, что мы делаем трехмерные вычисления]? В зависимости от того, как работает ваш расчет, вы можете обнаружить, что выполнение того или иного способа принесет вам максимальную пользу.
Я сделал немало SSE и 3DNow! оптимизация несколько лет назад, и «хитрость» часто заключается в том, как вы храните данные, чтобы вы могли легко получить «связку» нужного вида данных за один раз. Если данные хранятся неверно, вы будете тратить много времени на «извращение данных» (перемещение данных с одного способа хранения на другой).
Одним из способов является реализация трех библиотек, соответствующих одному интерфейсу. С динамическими библиотеками вы можете просто поменять файл библиотеки, и исполняемый файл будет использовать все, что найдет. Например, в Windows вы можете скомпилировать три DLL:
А потом сделайте исполняемую ссылку на Impl.dll
, Теперь просто поместите одну из трех конкретных DLL в тот же каталог, что и .exe
переименуйте его в Impl.dll
и он будет использовать эту версию. Этот же принцип в основном должен быть применим в UNIX-подобных ОС.
Следующим шагом будет загрузка библиотек программным способом, что, вероятно, является наиболее гибким, но это зависит от ОС и требует дополнительной работы (например, открытие библиотеки, получение указателей на функции и т. Д.)
Редактировать: Но, конечно, вы можете просто реализовать эту функцию три раза и выбрать одну во время выполнения, в зависимости от настроек некоторых параметров / файла конфигурации и т. Д., Как указано в других ответах.
Конечно, это возможно.
Лучший способ сделать это — иметь функции, которые выполняют всю работу, и выбирать их во время выполнения. Это будет работать, но не оптимально:
typedef enum
{
calc_type_invalid = 0,
calc_type_plain,
calc_type_sse,
calc_type_avx,
calc_type_max // not a valid value
} calc_type;
void do_my_calculation(float const *input, float *output, size_t len, calc_type ct)
{
float f;
size_t i;
for (i = 0; i < len; ++i)
{
switch (ct)
{
case calc_type_plain:
// plain calculation here
break;
case calc_type_sse:
// SSE calculation here
break;
case calc_type_avx:
// AVX calculation here
break;
default:
fprintf(stderr, "internal error, unexpected calc_type %d", ct);
exit(1);
break
}
}
}
При каждом прохождении цикла код выполняет switch
заявление, которое просто накладные расходы. Действительно умный компилятор может теоретически исправить это для вас, но лучше исправить это самостоятельно.
Вместо этого напишите три отдельные функции: одну для простого, одну для SSE и одну для AVX. Затем решите во время выполнения, какой из них запустить.
Для получения бонусных баллов в «отладочной» сборке выполните вычисления как с SSE, так и с равниной, и утверждайте, что результаты достаточно близки, чтобы придать уверенность. Напишите простую версию не для скорости, а для правильности; затем используйте его результаты, чтобы убедиться, что ваши умные оптимизированные версии получают правильный ответ.
Легендарный Джон Кармак рекомендует последний подход; он называет это «параллельными реализациями». Читать его эссе об этом.
Поэтому я рекомендую вам сначала написать простую версию. Затем вернитесь и начните переписывать части своего приложения, используя ускорение SSE или AVX, и убедитесь, что ускоренные версии дают правильные ответы. (И иногда в простой версии может быть ошибка, которой нет в ускоренной версии. Наличие двух версий и их сравнение помогает выявить ошибки в любой версии.)