Заставить компилятор не оптимизировать операторы без побочных эффектов
Я читал некоторые старые книги по программированию игр, и, как некоторые из вас, возможно, знают, в те дни было обычно быстрее делать бит-хаки, чем делать что-то стандартным способом. (Преобразование float
to int
, бит знака маски, преобразовывает назад для абсолютного значения, вместо как раз вызывать fabs()
, например)
в настоящее время почти всегда лучше просто использовать стандартные математические функции библиотеки, так как эти крошечные вещи вряд ли являются причиной большинства узких мест.
но Я все еще хочу сделать сравнение, просто из любопытства. Поэтому я хочу убедиться, что когда я профилирую, я не получаю искаженных результатов. Таким образом, я хотел бы убедиться, что компилятор не оптимизирует операторы, которые не имеют побочных эффектов, таких как:
void float_to_int(float f)
{
int i = static_cast<int>(f); // has no side-effects
}
есть ли способ сделать это? Насколько я могу судить, делает что-то вроде i += 10
все еще не будет иметь никакого побочного эффекта и как таковой не решит проблему.
единственное, о чем я могу думать, это наличие глобальной переменной, int dummy;
, а после броска делаем что-то вроде dummy += i
, поэтому значение это. Но я чувствую, что эта фиктивная операция будет мешать результатам, которые я хочу.
я использую Visual Studio 2008 / G++ (3.4.4).
редактировать
чтобы уточнить, я хотел бы, чтобы все оптимизации были максимальными, чтобы получить хорошие результаты профиля. Проблема в том, что при этом заявления без побочных эффектов будут оптимизированы, отсюда и ситуация.
изменить Опять
чтобы уточнить еще раз, прочитайте это:я не пытаюсь микро-оптимизировать это в каком-то производственном коде.
мы все знаем, что старые трюки больше не очень полезны, мне просто любопытно как они не полезны. Просто любопытство. Конечно, жизнь могла бы продолжаться и без меня, зная, как эти старые хаки работают против современных CPU, но это никогда не повредит знать.
знаю они не полезны, я их не использую.преждевременное цитирование кнута-корень всех неприятностей.
10 ответов
компиляторам, к сожалению, разрешено оптимизировать столько, сколько им нравится, даже без явных переключателей, если код ведет себя так, как будто оптимизация не происходит. Однако вы часто можете обмануть их, чтобы не делать этого, если вы укажете, что значение может быть использовано позже, поэтому я бы изменил ваш код на:
int float_to_int(float f)
{
return static_cast<int>(f); // has no side-effects
}
Как предлагали другие, вам нужно будет изучить выходные данные assemnler, чтобы проверить, что этот подход действительно работает.
назначение volatile
переменная shold никогда не оптимизируется, поэтому это может дать вам желаемый результат:
static volatile int i = 0;
void float_to_int(float f)
{
i = static_cast<int>(f); // has no side-effects
}
В этом случае я предлагаю вам сделать функция возвращает целочисленное значение:
int float_to_int(float f)
{
return static_cast<int>(f);
}
ваш вызывающий код может затем использовать его с printf, чтобы гарантировать, что он не оптимизирует его. Также убедитесь, что float_to_int находится в отдельном блоке компиляции, поэтому компилятор не может играть никаких трюков.
extern int float_to_int(float f)
int sum = 0;
// start timing here
for (int i = 0; i < 1000000; i++)
{
sum += float_to_int(1.0f);
}
// end timing here
printf("sum=%d\n", sum);
Теперь сравните это с пустой функции, как:
int take_float_return_int(float /* f */)
{
return 1;
}
, который также должен быть внешним.
разница во времени должна дать вам представление о расходы на то, что вы пытаетесь измерить.
поэтому я хочу убедиться, что когда я профиль, я не получаю искаженные результаты. Таким образом, я хотел бы убедиться, что компилятор не оптимизирует операторы out
вы по определению искажает результаты.
вот как исправить проблему попытки профилировать "фиктивный" код, который вы написали только для тестирования: для профилирования сохраните результаты в глобальный / статический массив и распечатайте один член массива на выходе в конце программы. Компилятор не сможет оптимизировать out любые вычисления, которые помещали значения в массив, но вы все равно получите любые другие оптимизации, которые он может ввести, чтобы сделать код быстрым.
что всегда работало на всех компиляторах, которые я использовал до сих пор:
extern volatile int writeMe = 0;
void float_to_int(float f)
{
writeMe = static_cast<int>(f);
}
обратите внимание, что это искажает результаты, методы boith должны писать в writeMe
.
volatile
сообщает компилятору "значение может быть доступно без вашего уведомления", поэтому компилятор не может пропустить расчет и удалить результат. Чтобы propagiation блок ввода констант, возможно, придется запускать их через Экстерн летучие, тоже:
extern volatile float readMe = 0;
extern volatile int writeMe = 0;
void float_to_int(float f)
{
writeMe = static_cast<int>(f);
}
int main()
{
readMe = 17;
float_to_int(readMe);
}
тем не менее, все оптимизации между read и запись может быть применена "с полной силой". Чтение и запись в глобальную переменную часто хорошо "fenceposts" при осмотре созданную сборку.
без extern
компилятор может заметить, что ссылка на переменную никогда не берется, и, таким образом, определить, что она не может быть изменчивой. Технически, с генерацией кода времени ссылки, этого может быть недостаточно, но я не нашел компилятор это агресивные. (Для компилятора, который действительно удаляет доступ, ссылка должна быть передана функции в DLL, загруженной во время выполнения)
вам просто нужно перейти к той части, где вы узнаете что-то и прочитать опубликованное руководство по оптимизации процессора Intel.
Они довольно четко заявляют, что литье между float и int-действительно плохая идея, потому что для этого требуется хранилище из регистра int в память с последующей загрузкой в регистр float. Эти операции вызывают пузырь в трубопроводе и тратят много драгоценных циклов.
вызов функции вызывает довольно много накладных расходов, поэтому я бы все равно удалил это.
добавление манекена += i; не проблема, если вы держите этот же бит кода в альтернативном профиле. (Итак, код, с которым вы сравниваете его).
последнее, но не менее важное: генерировать код asm. Даже если вы не можете кодировать в asm, сгенерированный код обычно понятен, поскольку он будет иметь метки и прокомментированный код C. Итак, вы знаете (sortoff), что происходит, и что биты хранятся.
R
p.s. нашел и это:
inline float pslNegFabs32f(float x){
__asm{
fld x //Push 'x' into st(0) of FPU stack
fabs
fchs //change sign
fstp x //Pop from st(0) of FPU stack
}
return x;
}
предположительно также очень быстро. Возможно, вы захотите сделать профиль этого тоже. (хотя это вряд ли портативный код)
возвращают значение?
int float_to_int(float f)
{
return static_cast<int>(f); // has no side-effects
}
а потом на сайте вызова, вы можете суммировать все возвращаемые значения, и распечатать результат, когда тест сделала. Обычный способ сделать это-каким-то образом убедиться, что вы зависите от результата.
вместо этого вы можете использовать глобальную переменную, но, похоже, это создаст больше пропусков кэша. Обычно, просто возвращая значение вызывающему абоненту (и убедившись, что вызывающий абонент действительно что-то с ним делает), делает трюк.
микро-бенчмарк вокруг этого утверждения не будет репрезентативным для использования этого подхода в подлинном scenerio; окружающие инструкции и их влияние на конвейер и Кэш, как правило, так же важны, как и любое данное утверждение само по себе.
GCC 4 делает много микро-оптимизаций сейчас, что GCC 3.4 никогда не делал. GCC4 включает в себя векторизатор дерева, который оказывается делать очень хорошая работа по использованию SSE и MMX. Он также использует библиотеки GMP и MPFR для оптимизации вызовов таких вещей, как sin()
, fabs()
, etc., а также оптимизация таких звонков на их FPU, SSE или 3D сейчас! эквиваленты.
Я знаю, что компилятор Intel также очень хорош в этих видах процессы оптимизации.
мое предложение-не беспокоиться о такой микро-оптимизации - на относительно новом оборудовании (все, что было построено за последние 5 или 6 лет), они почти полностью спорны.
Edit: в последних процессорах FPU fabs
инструкция намного быстрее, чем приведение к int
и бит Маска, и fsin
инструкция обычно будет быстрее, чем предварительное вычисление таблицы или экстраполяция серии Тейлора. Много оптимизаций, которые вы найдете в например, "трюки гуру игрового программирования" полностью спорны и, как указано в другом ответе, потенциально могут быть медленнее, чем инструкции на FPU и в SSE.
все это связано с тем, что новые процессоры конвейеризованы - инструкции декодируются и отправляются в быстрые вычислительные единицы. Инструкции больше не выполняются с точки зрения тактовых циклов и более чувствительны к пропускам кэша и зависимостям между инструкциями.
Проверьте AMD и Intel руководства по программированию процессора для всех деталей.