Имеют разные оптимизации (простой, SSE, AVX) в одном и том же исполняемом файле с C/С++

Я разрабатываю оптимизацию для своих 3D-вычислений, и теперь у меня есть:

  • a "plain" с использованием стандартных библиотек языка C,
  • a SSE оптимизированная версия, которая компилируется с использованием препроцессора #define USE_SSE,
  • a AVX оптимизированная версия, которая компилируется с использованием препроцессора #define USE_AVX

Можно ли переключаться между тремя версиями без необходимости компилировать разные исполняемые файлы (например, иметь разные файлы библиотек и загружать "правый" один динамически, не знаю, являются ли для них функции inline "правильными" )? Я бы также подумал о том, чтобы использовать этот вид коммутатора в программном обеспечении.

3 ответа

Для этого есть несколько решений.

Один из них основан на С++, где вы создадите несколько классов - как правило, вы реализуете класс интерфейса и используете функцию factory, чтобы дать вам объект правильного класса.

например.

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 как ваш "простой" класс, вы можете сделать поэтапное уточнение и реализовать только те части, которые вы на самом деле находите, выгодны и полагаются на базовый класс для реализации функций, в которых производительность не высококритичный.

Изменить: Вы говорите о inline, и я думаю, что вы смотрите на неправильный уровень функции, если это так. Вам нужны довольно большие функции, которые что-то делают с довольно небольшим количеством данных. В противном случае все ваши усилия будут потрачены на подготовку данных в нужном формате, а затем выполнение нескольких инструкций расчета и последующее помещение данных обратно в память.

Я также рассмотрю, как вы храните свои данные. Вы храните наборы массива с X, Y, Z, W, или вы храните много X, много Y, много Z и много W в отдельных массивах [при условии, что мы делаем 3D-вычисления]? В зависимости от того, как работает ваш расчет, вы можете обнаружить, что выполнение одного или другого способа даст вам наилучшую выгоду.

Я сделал честный SSE и 3DNow! оптимизация несколько лет назад, и "трюк" часто больше связан с тем, как вы храните данные, чтобы вы могли легко захватить "комплект" правильного типа данных за один раз. Если у вас есть данные, которые хранятся неправильно, вы будете тратить много времени на "swizzling data" (перемещение данных с одного способа хранения на другой).


Один из способов - реализовать три библиотеки, соответствующие одному и тому же интерфейсу. С динамическими библиотеками вы можете просто поменять файл библиотеки, и исполняемый файл будет использовать все, что он найдет. Например, в Windows вы можете скомпилировать три библиотеки DLL:

  • PlainImpl.dll
  • SSEImpl.dll
  • AVXImpl.dll

И затем создайте исполняемую ссылку для Impl.dll. Теперь просто поместите одну из трех конкретных DLL в тот же каталог, что и .exe, переименуйте его в Impl.dll, и он будет использовать эту версию. Тот же принцип должен в основном применяться к UNIX-подобной ОС.

Следующим шагом будет программная загрузка библиотек, которая, вероятно, является наиболее гибкой, но она специфична для ОС и требует некоторой дополнительной работы (например, открытие библиотеки, получение указателей на функции и т.д.).

Edit: Но, конечно, вы могли бы просто реализовать эту функцию три раза и выбрать ее во время выполнения, в зависимости от настроек параметров/настроек файла и т.д., как это было в других ответах.


Конечно, это возможно.

Лучший способ сделать это - иметь функции, которые выполняют всю работу, и выбирать среди них во время выполнения. Это будет работать, но не оптимально:

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. Затем выберите во время выполнения, который нужно запустить.

Для бонусных очков в сборке "debug" выполните вычисления как с SSE, так и с равниной, и утвердите, что результаты достаточно близки, чтобы дать уверенность. Напишите обычную версию, а не скорость, но для правильности; затем используйте его результаты, чтобы убедиться, что ваши умные оптимизированные версии получают правильный ответ.

Легендарный Джон Кармак рекомендует последний подход; он называет это "параллельными реализациями". Прочитайте его эссе об этом.

Поэтому я рекомендую сначала написать обычную версию. Затем вернитесь назад и начните перезаписывать части своего приложения, используя ускорение SSE или AVX, и убедитесь, что ускоренные версии дают правильные ответы. (И иногда у простой версии может быть ошибка, которую ускоренная версия не имеет. Наличие двух версий и их сравнение помогают сделать ошибки в любой версии.)

licensed under cc by-sa 3.0 with attribution.