Действительно ли быстрее использовать say (i << 3) + (i << 1) для умножения на 10, чем использовать i * 10 напрямую? </p>
Это может быть или не быть на вашем компьютере - если вам все равно, измерьте в реальных условиях использования.
Пример использования - от 486 до ядра 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/.
(от какого-то блоба Intel)
Используя SSE, Core i7 может выдавать команды одновременного сложения и умножения, что приводит к пиковой частоте 8 операций с плавающей запятой (FLOP) за такт
Это дает вам представление о том, как далеко все зашло. Оптимистические мелочи - например, сдвиг битов по сравнению с *
- к которым серьезно относились даже в 90-е годы, сейчас просто устарели. Сдвиг битов все еще быстрее, но для не-степени двух муль / дел к тому времени, когда вы делаете все свои смены и добавляете результаты, это снова медленнее. Затем, больше инструкций означает больше ошибок кэша, больше потенциальных проблем в конвейерной обработке, более широкое использование временных регистров может означать большее сохранение и восстановление содержимого регистра из стека ... это быстро становится слишком сложным, чтобы количественно определить все воздействия, но они преимущественно отрицательный.
функциональность в исходном коде и реализация
В более общем плане, ваш вопрос помечен C и C ++. Как языки третьего поколения, они специально разработаны, чтобы скрыть детали базового набора команд ЦП. Чтобы удовлетворить свои языковые стандарты, они должны поддерживать операции умножения и сдвига (и многие другие) , даже если базовое оборудование не . В таких случаях они должны синтезировать требуемый результат, используя множество других инструкций. Точно так же они должны обеспечивать программную поддержку для операций с плавающей запятой, если в процессоре этого нет, а 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
) не имеют отношения к исходному коду, но если что-то само по себе очищает все биты, то это можно использовать для установки чего-либо в 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
хранит возраст работника, он никогда не может быть отрицательным".Если у вас есть такая особая способность проникновения в суть, тогда да - ваша >>
безопасная оптимизация может быть передана компилятором, если вы явно не сделаете это в своем коде.Но, это рискованно и редко полезно, так как большую часть времени у вас не будет такого понимания, и другие программисты, работающие над тем же кодом, не будут знать, что вы поставили на что-то необычноеожидания данных, которые вы будете обрабатывать ... то, что кажется совершенно безопасным изменением для них, может иметь неприятные последствия из-за вашей "оптимизации".
Есть ли какие-либо данные, которые нельзя умножитьили делится таким образом?
Да ... как уже упоминалось выше, отрицательные числа имеют поведение, определяемое реализацией, когда "делятся" путем сдвига битов.