Действительно ли быстрее использовать say (i << 3) + (i << 1) для умножения на 10, чем напрямую использовать i * 10?
Это может быть или не быть на вашей машине - если вам все равно, измерьте в реальных условиях использования.
Тематическое исследование - от 486 до Core i7
Сравнительный анализ очень сложно сделать осмысленно, но мы можем взглянуть на несколько фактов. Из http://www.penguin.cz/~literakl/intel/s.html#SAL и http://www.penguin.cz/~literakl/intel/i.html#IMUL мы получаем представление о тактовых циклах x86. необходим для арифметического сдвига и умножения. Скажем, мы придерживаемся "486" (самый новый из перечисленных), 32-битных регистров и немедленных, IMUL занимает 13-42 цикла и IDIV 44. Каждая лицензия SAL занимает 2 и добавляя 1, так что даже с несколькими из них вместе выглядят поверхностно как победитель.
В эти дни с ядром i7:
(из http://software.intel.com/en-us/forums/showthread.php?t=61481 )
Задержка составляет 1 цикл для целочисленного сложения и 3 цикла для целочисленного умножения . Вы можете найти задержки и значения в Приложении C «Справочного руководства по оптимизации архитектур Intel® 64 и IA-32», которое находится по адресу http://www.intel.com/products/processor/manuals/ .
(из какого-то интеллара)
Используя SSE, Core i7 может выдавать одновременные инструкции сложения и умножения, что приводит к максимальной скорости 8 операций с плавающей запятой (FLOP) за такт
Это дает вам представление о том, как далеко все зашло. Оптимизация мелочи - как сдвиг битов по сравнению с*
- к которым серьезно относились даже в 90-е годы, сейчас просто устарели. Сдвиг битов все еще быстрее, но для не-степени двух муль / дел к тому времени, когда вы делаете все свои смены и добавляете результаты, это снова медленнее. Затем, больше инструкций означает больше ошибок кэша, больше потенциальных проблем в конвейерной обработке, более широкое использование временных регистров может означать большее сохранение и восстановление содержимого регистра из стека ... это быстро становится слишком сложным, чтобы количественно определить все воздействия, но они преимущественно отрицательный.
функциональность в исходном коде против реализации
В более общем плане, ваш вопрос помечен C и C ++. Как языки 3-го поколения, они специально разработаны, чтобы скрыть детали базового набора команд ЦП. Чтобы удовлетворить свои языковые стандарты, они должны поддерживать операции умножения и сдвига (и многие другие), даже если базовое оборудование этого не делает . В таких случаях они должны синтезировать требуемый результат, используя множество других инструкций. Точно так же они должны обеспечивать программную поддержку для операций с плавающей запятой, если в процессоре этого нет, а FPU нет. Все современные процессоры поддерживают*
и<<
, так что это может показаться абсурдным теоретическим и историческим, но важно то, что свобода выбора реализации идет в обоих направлениях: даже если у процессора есть инструкция, которая реализует операцию, запрашиваемую в исходном коде в общем случае, компилятор может свободно выберите что-то еще, что он предпочитает, потому что это лучше для конкретного случая, с которым сталкивается компилятор.
Примеры (с гипотетическим языком ассемблера)
source literal approach optimised approach
#define N 0
int x; .word x xor registerA, registerA
x *= N; move x -> registerA
move x -> registerB
A = B * immediate(0)
store registerA -> x
...............do something more with x...............
Инструкции, такие как exclusive или ( xor
), не имеют отношения к исходному коду, но xoring что-либо само по себе очищает все биты, поэтому его можно использовать для установки чего-либо на 0. Исходный код, который подразумевает адреса памяти, может не повлечь за собой никакого использования.
Такого рода хаки использовались до тех пор, пока компьютеры были рядом. В первые годы существования 3GL, чтобы обеспечить освоение разработчиками, выход компилятора должен был удовлетворять существующему хардкорному оптимизирующему руку разработчику на ассемблере. сообщество, которое произвело код, не было медленнее, более многословно или иначе хуже. Компиляторы быстро переняли много замечательных оптимизаций - они стали лучшим централизованным хранилищем, чем любой отдельный программист на языке ассемблера, хотя всегда есть шанс, что они пропустят определенную оптимизацию, которая оказывается критической в конкретном случае - люди могут иногда преуменьшить это и нащупать что-то лучшее, в то время как компиляторы просто делают, как им было сказано, пока кто-то не поделится этим опытом с ними.
Таким образом, даже если переключение и добавление по-прежнему происходит быстрее на каком-то конкретном оборудовании, тогда разработчик компилятора, вероятно, сработал именно тогда, когда это безопасно и выгодно.
Ремонтопригодность
Если ваше аппаратное обеспечение изменится, вы можете перекомпилировать его, и он будет смотреть на целевой ЦП и делать еще один лучший выбор, в то время как вы вряд ли когда-нибудь захотите пересмотреть свои «оптимизации» или перечислить, какие среды компиляции должны использовать умножение, а какие - сдвигаться. Подумайте обо всех «оптимизациях» со сдвигом битов со сдвигом в два, написанных более 10 лет назад, которые теперь замедляют код, в котором они работают, так как он работает на современных процессорах ...!
К счастью, хорошие компиляторы, такие как GCC, обычно могут заменить серию битовых сдвигов и арифметику прямым умножением, когда включена любая оптимизация (т.е. ...main(...) { return (argc << 4) + (argc << 2) + argc; }
-> imull $21, 8(%ebp), %eax
), поэтому перекомпиляция может помочь даже без исправления кода, но это не гарантируется.
Странный код со сдвигом битов, реализующий умножение или деление, гораздо менее выразителен, чем вы пытались достичь концептуально, поэтому другие разработчики будут смущены этим, а сбитый с толку программист с большей вероятностью введет ошибки или удалит что-то важное в попытке восстановить кажущееся здравомыслие. Если вы делаете неочевидные вещи, когда они действительно ощутимо полезны, а затем хорошо документируете их (но в любом случае не документируете другие интуитивные вещи), все будут счастливее.
Общие решения против частичных решений
Если у вас есть некоторые дополнительные знания, такие как то, что вы int
действительно будете хранить только значения x
, y
и z
, возможно, вы сможете выработать некоторые инструкции, которые работают с этими значениями, и получить результат быстрее, чем когда компилятор не имеет это понимание и нуждается в реализации, которая работает для всех int
ценностей. Например, рассмотрим ваш вопрос:
Умножение и деление могут быть достигнуты с помощью битовых операторов ...
Вы иллюстрируете умножение, но как насчет деления?
int x;
x >> 1; // divide by 2?
Согласно стандарту C ++ 5.8:
-3- Значение E1 >> E2 - это биты E2, сдвинутые вправо E1. Если E1 имеет тип без знака или E1 имеет тип со знаком и неотрицательное значение, значение результата является неотъемлемой частью отношения E1, деленного на величину 2, возведенную в степень E2. Если E1 имеет тип со знаком и отрицательное значение, результирующее значение определяется реализацией.
Таким образом, ваш битовый сдвиг имеет результат, определенный реализацией, когда x
он отрицательный: он может не работать одинаково на разных машинах. Но /
работает гораздо более предсказуемо. (Это также может быть не вполне согласованным, поскольку разные машины могут иметь разные представления отрицательных чисел и, следовательно, разные диапазоны, даже если в представлении присутствует одинаковое количество битов.)
Вы можете сказать: «Мне все равно ... это int
хранит возраст сотрудника, он никогда не может быть отрицательным». Если у вас есть такая особая способность проникновения в суть, тогда да - ваша >>
безопасная оптимизация может быть передана компилятором, если вы явно не сделаете это в своем коде. Но это рискованно и редко полезно, так как большую часть времени у вас не будет такого понимания, и другие программисты, работающие над тем же кодом, не будут знать, что вы поставили на карту некоторые необычные ожидания данных, которые вы '' Я буду обрабатывать ... то, что кажется им абсолютно безопасным, может иметь неприятные последствия из-за вашей "оптимизации"
Есть ли какие-либо входные данные, которые не могут быть умножены или разделены таким образом?
Да ... как упомянуто выше, отрицательные числа имеют поведение, определяемое реализацией, когда они "разделены" сдвигом битов.