Целочисленное деление или умножение с плавающей запятой?

Если нужно вычислить долю заданного значения int, скажем:

int j = 78;
int i = 5* j / 4;

Это быстрее, чем делать:

int i = 1.25*j; // ?

Если да, то можно ли использовать коэффициент преобразования, чтобы решить, какой из них использовать, например, сколько int делений можно выполнить за одно и то же время при одном float умножении?

Редактировать: я думаю, комментарии ясно дают понять, что математика с плавающей запятой будет медленнее, но вопрос в том, насколько? Если мне нужно заменить каждое float умножение на N int делений, для чего N это больше не будет стоить того?


person nbubis    schedule 27.09.2013    source источник
comment
Вы сравнивали каждый из них?   -  person Mysticial    schedule 28.09.2013
comment
Сколько из этих чисел являются динамическими?   -  person Kerrek SB    schedule 28.09.2013
comment
@KerrekSB имеет в виду компилятор, оптимизирующий все это для вас. Следовательно, почему бенчмаркинг важен.   -  person Adam    schedule 28.09.2013
comment
@KerrekSB - все числа динамические. Пример — это всего лишь пример, на самом деле они будут динамическими.   -  person nbubis    schedule 28.09.2013
comment
Я бы сказал, что если у вас есть три целых числа a, b и c и вы хотите вычислить a * b / c, вам следует написать int compute(int a, int b, int c) { return a * b / c; }. Я сомневаюсь, что вы могли бы сделать лучше.   -  person Kerrek SB    schedule 28.09.2013
comment
Это кажется преждевременной оптимизацией, также обратите внимание, что литерал 1.25 на самом деле является double, а не float.   -  person LihO    schedule 28.09.2013
comment
Две альтернативы, которые вы показали, могут дать разные результаты. Сначала выбирайте на основе желаемого результата, а затем на основе производительности.   -  person Oswald    schedule 28.09.2013
comment
Является ли правильным округлением значения результата возможной проблемой?   -  person Jongware    schedule 28.09.2013
comment
Я тоже не понимаю, чего вы надеетесь достичь. Если все три входа являются динамическими, то вы можете в лучшем случае вычислить множитель с плавающей запятой как static_cast<double>(a) / static_cast<double>(c), тогда вам нужно преобразовать b в число с плавающей запятой, умножить, а затем округлить в обратном направлении, используя округление, предписанное C++, все и любое из которых вряд ли будет быть быстрее, чем две целочисленные операции.   -  person Kerrek SB    schedule 28.09.2013
comment
вы также можете выполнять целочисленные сдвиги и сложения, а не умножать (зависит от вашего оборудования). При использовании C и компилятора возникают накладные расходы на преобразование и т. Д., И у вас не будет фиксированной точки. но ответ недетерминирован, вам нужно просто проверить его...   -  person old_timer    schedule 28.09.2013
comment
Вы уверены, что именно эта операция является узким местом в производительности вашей программы? (Держу пари, что это не так.) Измерьте это, а затем оптимизируйте узкое место.   -  person pts    schedule 28.09.2013
comment
@pts - когда это единственная операция, и вы повторяете ее 10^7 раз, это узкое место.   -  person nbubis    schedule 28.09.2013
comment
Ваша предпосылка на самом деле несколько ошибочна здесь. Невозможно, чтобы все числа были динамическими, если вы можете переключаться между 5/4 и 1.25. Если бы числитель и знаменатель не были константами времени компиляции, как узнать, что нужно умножать на 1.25? (без фактического разделения, которого вы пытаетесь избежать)   -  person Mysticial    schedule 28.09.2013
comment
Базовая арифметика настолько не важна для производительности, что я не знаю, почему вы беспокоитесь об ее оптимизации. Вы собираетесь запускать этот код в тостере?   -  person Havenard    schedule 28.09.2013
comment
Просто чтобы прояснить ситуацию, умножение выполняется на уровне ЦП за одну операцию, а деление — одна из самых ресурсоемких базовых операций ЦП. Является ли случай, когда вы используете несколько операций вместо одной, включая деление, быстрее, даже если вы используете только целые числа? Я искренне сомневаюсь.   -  person Havenard    schedule 28.09.2013


Ответы (3)


Вы сказали, что все значения являются динамическими, что имеет значение. Для конкретных значений 5 * j / 4 целочисленные операции будут невероятно быстрыми, потому что в худшем случае компилятор оптимизирует их до двух сдвигов и одного сложения, плюс некоторые возни, чтобы справиться с возможностью того, что j отрицательное. Если ЦП может работать лучше (целочисленное умножение за один цикл или что-то еще), то компилятор обычно знает об этом. Ограничения возможностей компиляторов по оптимизации такого рода вещей в основном возникают, когда вы компилируете для большого семейства процессоров (например, генерируете код ARM с наименьшим общим знаменателем), где компилятор на самом деле мало что знает о оборудования и, следовательно, не всегда может сделать правильный выбор.

Я предполагаю, что если a и b фиксированы на какое-то время (но неизвестны во время компиляции), то возможно, что вычисление k = double(a) / b один раз, а затем int(k * x) для множества различных значений x, может быть быстрее, чем вычисление a * x / b для множества различных значений x. Я бы не стал на это рассчитывать.

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

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

person Steve Jessop    schedule 27.09.2013

Невозможно ответить на этот вопрос вне контекста. Кроме того, 5*j/4 обычно не дает того же результата, что и (int) (1.25*j), из-за свойств целочисленной арифметики и арифметики с плавающей запятой, включая округление и переполнение.

Если ваша программа выполняет в основном операции с целыми числами, то преобразование j в число с плавающей запятой, умножение на 1,25 и обратное преобразование в целое число могут быть бесплатными, поскольку в нем используются единицы с плавающей запятой, которые не задействованы иным образом.

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

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

Кроме того, разница в производительности между 5*j/4 и (int) (1.25*j) чаще всего слишком мала, чтобы быть заметной в программе, если только она или подобные операции не повторяются очень много раз. (И если это так, то векторизация кода может дать огромные преимущества, то есть использование возможностей одной инструкции и нескольких данных [SIMD] многих современных процессоров для одновременного выполнения нескольких операций.)

person Eric Postpischil    schedule 27.09.2013

В вашем случае 5*j/4 будет намного быстрее, чем 1.25*j, потому что делением по степени 2 можно легко управлять с помощью сдвига вправо, а 5*j можно выполнить одной инструкцией на многих архитектурах, таких как LEA на x86 или ADD со сдвигом на ARM. . Большинству других потребуется не более 2 инструкций, таких как j + (j >> 2), но в этом случае это все же, вероятно, быстрее, чем умножение с плавающей запятой. Более того, при выполнении int i = 1.25*j вам потребуется 2 конверсии из int в double и обратно, а также 2 междоменных перемещения данных, которые обычно обходятся очень дорого.

В других случаях, когда дробь не может быть представлена ​​в двоичном формате с плавающей запятой (например, 3*j/10), тогда использование int умножение/деление будет более правильным (поскольку 0,3 не совсем 0,3 в плавающей запятой), и скорее всего быстрее (поскольку компилятор может оптимизировать деление на константу, преобразовав его в умножение)


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

Важным отличием является то, что 5*j/4 будет переполняться, если j слишком велико, а 1.25*j — нет.

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

Смотрите также

person phuclv    schedule 28.09.2013