Заставить компилятор не оптимизировать операторы без побочных эффектов - PullRequest
10 голосов
/ 20 июля 2009

Я читал несколько старых книг по игровому программированию, и, как некоторые из вас, возможно, знали, в те дни, как правило, было быстрее делать битовые хаки, чем делать вещи стандартным способом.(Преобразование float в int, бит знака маски, обратное преобразование в абсолютное значение, например, вместо простого вызова fabs())

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

Но я все же хочу сделать сравнение, просто ради любопытства.Поэтому я хочу убедиться, что при профиле я не получаю искаженные результаты.Поэтому я хотел бы убедиться, что компилятор не оптимизирует операторы, не имеющие побочных эффектов, такие как:

void float_to_int(float f)
{
    int i = static_cast<int>(f); // has no side-effects
}

Есть ли способ сделать это?Насколько я могу судить, выполнение чего-то вроде i += 10 по-прежнему не будет иметь побочных эффектов и, следовательно, не решит проблему.

Единственное, о чем я могу думать, это иметь глобальную переменную int dummy;, а после приведения сделать что-то вроде dummy += i, поэтому используется значение i.Но я чувствую, что эта фиктивная операция будет мешать желаемым результатам.

Я использую Visual Studio 2008 / G ++ (3.4.4).

Редактировать

Чтобы уточнить, я бы хотел, чтобы все оптимизации были максимизированы, чтобы получить хорошие результаты профиля.Проблема заключается в том, что при этом будут оптимизированы операторы без побочных эффектов, и, следовательно, ситуация.

Править еще раз

Чтобы еще раз уточнить, прочитайте это: Я не пытаюсь микрооптимизировать это в каком-то производственном коде.

Мы все знаем, что старые трюки больше не очень полезны, мне просто любопытно как они бесполезны.Просто любопытство.Конечно, жизнь могла бы продолжаться, если бы я не знал, как эти старые хаки работают против современных процессоров, но это никогда не помешает узнать.

Так что скажите мне, что эти приемы больше не нужны, перестаньте пытатьсяОптимизировать бла-бла "- это ответ, полностью упускающий суть.Я знаю они бесполезны, я ими не пользуюсь.

Преждевременное цитирование Кнута - корень всех раздражений.

Ответы [ 10 ]

8 голосов
/ 20 июля 2009

Назначение переменной volatile никогда не будет оптимизировано, так что это может дать вам желаемый результат:

static volatile int i = 0;

void float_to_int(float f)
{
    i = static_cast<int>(f); // has no side-effects
}
6 голосов
/ 20 июля 2009

В этом случае я предлагаю вам сделать так, чтобы функция возвращала целое значение:

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;
}

Который также должен быть внешним.

Разница во времени должна дать вам представление о расходах на то, что вы пытаетесь измерить.

6 голосов
/ 20 июля 2009

Поэтому я хочу убедиться, что при профилировании я не получаю искаженных результатов. Поэтому я хотел бы убедиться, что компилятор не оптимизирует операторы

Вы по определению искажаете результаты.

Вот как решить проблему с попыткой профилировать «фиктивный» код, который вы написали просто для тестирования: Для профилирования сохраните свои результаты в глобальном / статическом массиве и напечатайте один член массива в вывод в конце программы. Компилятор не сможет оптимизировать out любых вычислений, которые помещают значения в массив, но вы все равно получите любые другие оптимизации, которые он может вставить, чтобы сделать код быстрым.

4 голосов
/ 20 июля 2009

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

int float_to_int(float f)
{
    return static_cast<int>(f); // has no side-effects
}

Как и предполагали другие, вам нужно будет проверить вывод ассемблера, чтобы убедиться, что этот подход действительно работает.

3 голосов
/ 20 июля 2009

Что всегда работало на всех компиляторах, которые я использовал до сих пор:

extern volatile int writeMe = 0;

void float_to_int(float f)
{    
  writeMe = static_cast<int>(f); 
}

обратите внимание, что это искажает результаты, методы boith должны записывать в writeMe.

volatile сообщает компилятору «значение может быть получено без вашего уведомления», поэтому компилятор не может пропустить вычисление и отбросить результат. Чтобы заблокировать распространение входных констант, вам также может понадобиться запустить их через внешнюю переменную:

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);
}

Тем не менее, все оптимизации между чтением и записью могут быть применены «с полной силой». Чтение и запись в глобальную переменную часто являются хорошими «ограждениями» при проверке сгенерированной сборки.

Без extern компилятор может заметить, что ссылка на переменную никогда не берется, и, таким образом, определить, что она не может быть изменчивой. Технически, с Link Time Code Generation этого может быть недостаточно, но я не нашел компилятор , который агрессивен. (Для компилятора, который действительно удаляет доступ, ссылку необходимо передать функции в DLL, загруженной во время выполнения)

2 голосов
/ 20 июля 2009

Вам просто нужно перейти к той части, где вы чему-то научились, и прочитать опубликованное Руководство по оптимизации процессора Intel .

В них довольно четко говорится, что приведение между float и int является действительно плохой идеей, потому что требует сохранения из регистра int в память с последующей загрузкой в ​​регистр float. Эти операции вызывают пузырь в трубопроводе и тратят много драгоценных циклов.

2 голосов
/ 20 июля 2009

вызов функции влечет за собой немало накладных расходов, поэтому я все равно удалил бы это.

добавление манекена + = i; это не проблема, если вы сохраняете этот же бит кода в альтернативном профиле. (Так что код, с которым вы сравниваете его).

Последнее, но не менее важное: сгенерируйте asm-код. Даже если вы не можете кодировать в asm, сгенерированный код, как правило, понятен, так как он будет иметь метки и закомментированный C-код. Итак, вы знаете (сортировка), что происходит и какие биты сохраняются.

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;
 } 

предположительно тоже очень быстро. Возможно, вы захотите профилировать это тоже. (хотя это вряд ли переносимый код)

1 голос
/ 20 июля 2009

Вернуть значение?

int float_to_int(float f)
{
    return static_cast<int>(f); // has no side-effects
}

и затем на сайте вызова вы можете суммировать все возвращаемые значения и распечатать результат после завершения теста. Обычный способ сделать это - каким-то образом убедиться, что вы зависите от результата.

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

0 голосов
/ 20 июля 2009

GCC 4 выполняет много микрооптимизаций, чего GCC 3.4 никогда не делал. GCC4 включает векторизатор дерева, который, как оказалось, отлично справляется с работой , используя SSE и MMX. Он также использует библиотеки GMP и MPFR, чтобы помочь оптимизировать вызовы таких вещей, как sin(), fabs() и т. Д., А также оптимизировать такие вызовы для своих FPU, SSE или 3D Now! эквиваленты.

Я знаю, что компилятор Intel также чрезвычайно хорош в подобных оптимизациях.

Мое предложение - не беспокоиться о таких микрооптимизациях - на относительно новом оборудовании (построенном за последние 5 или 6 лет) они почти полностью спорят.

Редактировать: На последних процессорах инструкция FPU fabs намного быстрее, чем приведение к int и битовой маске, а инструкция fsin, как правило, будет быстрее, чем предварительный расчет таблицы или экстраполяция ряда Тейлора. , Многие из оптимизаций, которые вы найдете, например, в «Уловках гуру программирования игр», полностью спорны и, как указано в другом ответе, потенциально могут быть медленнее, чем инструкции на FPU и в SSE.

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

Ознакомьтесь со всеми руководствами по программированию для процессоров AMD и Intel.

0 голосов
/ 20 июля 2009

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...