Я думаю, что для спрашивающего было бы более полезно иметь более дифференцированный ответ, потому что я вижу несколько неисследованных предположений в вопросах и в некоторых ответах или комментариях.
Результирующая относительная среда выполнения сдвига и умножения не имеет ничего общего с C. Когда я говорю «С», я имею в виду не экземпляр конкретной реализации, такой как та или иная версия GCC, а язык. Я не собираюсь воспринимать это как абсурд, но для примера приведу крайний пример: вы могли бы реализовать полностью совместимый со стандартами компилятор C и умножить его на час, а на сдвиг - миллисекунды - или наоборот. Я не знаю каких-либо таких ограничений производительности в C или C ++.
Вы можете не заботиться об этой детали в аргументации. Возможно, вы намеревались просто проверить относительную производительность выполнения сдвигов по сравнению с умножением, и вы выбрали C, потому что он обычно воспринимается как язык программирования низкого уровня, поэтому можно ожидать, что его исходный код будет переводиться в соответствующие инструкции более напрямую. Такие вопросы очень распространены, и я думаю, что хороший ответ должен указать на то, что даже в C ваш исходный код не переводится в инструкции так же прямо, как вы можете думать в данном случае. Я дал вам некоторые возможные результаты компиляции ниже.
Здесь комментарии, которые ставят под сомнение полезность замены этой эквивалентности в реальном программном обеспечении. Вы можете увидеть некоторые в комментариях к вашему вопросу, например, от Эрика Липперта. Это соответствует реакции, которую вы обычно получаете от более опытных инженеров в ответ на такую оптимизацию. Если вы используете двоичные сдвиги в производственном коде в качестве общего средства умножения и деления, люди, скорее всего, будут сжиматься в вашем коде и иметь некоторую эмоциональную реакцию («Я слышал, что это бессмысленное утверждение о JavaScript ради бога») это может не иметь смысла для начинающих программистов, если они не будут лучше понимать причины этих реакций.
Эти причины, в первую очередь, представляют собой сочетание пониженной читаемости и бесполезности такой оптимизации, как вы, возможно, уже поняли, сравнивая их относительную производительность. Однако я не думаю, что у людей была бы такая сильная реакция, если бы замена сдвига на умножение была единственным примером такой оптимизации. Подобные ваши вопросы часто возникают в разных формах и в разных контекстах. Я думаю, что более старшие инженеры на самом деле реагируют так сильно, по крайней мере, я иногда, на то, что существует потенциал для гораздо более широкого спектра вреда, когда люди широко применяют такие микрооптимизации по всей базе кода. Если вы работаете в такой компании, как Microsoft, на большой базе кода, вы потратите много времени на чтение исходного кода других инженеров или на попытки найти в нем определенный код. Это может быть даже ваш собственный код, который вы будете пытаться осмыслить через несколько лет, особенно в самые неподходящие времена, например, когда вам нужно исправить производственный сбой после вызова, поступившего на пейджер. В пятницу вечером мы собираемся отправиться на веселую вечеринку с друзьями ... Если вы потратите столько времени на чтение кода, вы по достоинству оцените его как можно более читабельный. Представьте себе, что вы читаете ваш любимый роман, но издатель решил выпустить новое издание, в котором они используют abbrv. все, что вам нужно, это svs spc. Это похоже на реакцию других инженеров на ваш код, если вы посыпаете их такой оптимизацией. Как указывали другие ответы, лучше четко указать, что вы имеете в виду,
Тем не менее, даже в таких условиях вы можете решить вопрос об интервью, в котором вы должны знать эту или другую эквивалентность. Знание их неплохо, и хороший инженер знал бы об арифметическом эффекте двоичного сдвига. Обратите внимание, что я не сказал, что это делает хорошего инженера, но что хороший инженер знал бы, по моему мнению. В частности, вы все еще можете найти какого-нибудь менеджера, обычно в конце цикла собеседования, который широко улыбнется вам в ожидании удовольствия, чтобы раскрыть вам этот хитрый инженерный «трюк» в вопросе кодирования и доказать, что он / она Раньше тоже был или является одним из опытных инженеров, а не «просто» менеджер. В таких ситуациях просто попытайтесь выглядеть впечатленным и поблагодарите его / ее за полезное интервью.
Почему вы не видели разницу в скорости в C? Наиболее вероятный ответ заключается в том, что оба результата привели к одному и тому же ассемблерному коду:
int shift(int i) { return i << 2; }
int multiply(int i) { return i * 2; }
Можно как скомпилировать в
shift(int):
lea eax, [0+rdi*4]
ret
На GCC без оптимизаций, т. Е. Используя флаг "-O0", вы можете получить это:
shift(int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov eax, DWORD PTR [rbp-4]
sal eax, 2
pop rbp
ret
multiply(int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov eax, DWORD PTR [rbp-4]
add eax, eax
pop rbp
ret
Как видите, передача «-O0» в GCC не означает, что он не будет достаточно умным в отношении того, какой код он создает. В частности, обратите внимание, что даже в этом случае компилятор избегал использования инструкции умножения. Вы можете повторить тот же эксперимент со сдвигами на другие числа и даже умножением на числа, которые не являются степенями двух. Скорее всего, на вашей платформе вы увидите комбинацию сдвигов и дополнений, но не умножения. Для компилятора кажется небольшим совпадением то, что он явно избегает использования умножений во всех тех случаях, если умножения и сдвиги действительно имеют одинаковую стоимость, не так ли? Но я не хочу давать предположения для доказательства, поэтому давайте двигаться дальше.
Вы можете повторить тест с помощью приведенного выше кода и посмотреть, заметите ли вы разницу в скорости сейчас. Даже тогда вы тестируете не сдвиг против умножения, как вы можете видеть по отсутствию умножения, но код, который был сгенерирован GCC с определенным набором флагов для операций C сдвига и умножения в конкретном случае . Итак, в другом тесте вы можете отредактировать код сборки вручную и вместо этого использовать в коде команду «imul» для метода «multiply».
Если вы хотите победить некоторые из этих умений компилятора, вы можете определить более общий метод сдвига и умножения и в итоге получите что-то вроде этого:
int shift(int i, int j) { return i << j; }
int multiply(int i, int j) { return i * j; }
Который может дать следующий код сборки:
shift(int, int):
mov eax, edi
mov ecx, esi
sal eax, cl
ret
multiply(int, int):
mov eax, edi
imul eax, esi
ret
Здесь, наконец, мы имеем, даже на самом высоком уровне оптимизации GCC 4.9, выражение в инструкциях по сборке, которое вы, возможно, ожидали, когда изначально устанавливали свой тест. Я думаю, что сам по себе может быть важным уроком оптимизации производительности. Мы можем видеть разницу, которую он сделал, чтобы заменить переменные на конкретные константы в нашем коде, с точки зрения умений, которые компилятор может применить. Микро-оптимизации, такие как смещение-умножение, - это некоторые очень низкоуровневые оптимизации, которые компилятор обычно может легко выполнить сам. Другие оптимизации, которые оказывают гораздо большее влияние на производительность, требуют понимания замысла кода.это часто не доступно компилятору или может быть предположено только некоторой эвристикой. Это то место, куда вы, как инженер-программист, приходите, и это, как правило, обычно не подразумевает замену умножения сдвигами. Это включает в себя такие факторы, как избегание избыточного вызова службы, которая производит ввод-вывод и может блокировать процесс. Если вы идете на свой жесткий диск или, не дай бог, в удаленную базу данных за некоторыми дополнительными данными, которые вы могли бы получить из того, что у вас уже есть в памяти, время, которое вы тратите на ожидание, перевешивает выполнение миллиона инструкций. Теперь, я думаю, что мы немного отошли от вашего первоначального вопроса, но я думаю, указав это на вопросника, особенно если мы предполагаем, что кто-то, кто только начинает понимать перевод и выполнение кода,
Итак, какой из них будет быстрее? Я думаю, что это хороший подход, который вы выбрали для проверки разницы в производительности. В общем, легко быть удивленным производительностью некоторых изменений кода во время выполнения. Современные технологии используют множество методов, и взаимодействие между программным обеспечением также может быть сложным. Даже если вам нужно получить положительные результаты производительности для определенного изменения в одной ситуации, я думаю, что опасно делать вывод, что изменения такого типа всегда будут приносить преимущества производительности. Я думаю, что запускать такие тесты один раз опасно: «Хорошо, теперь я знаю, какой из них быстрее!» а затем без разбора применить ту же оптимизацию к производственному коду без повторения ваших измерений.
Так что, если сдвиг быстрее, чем умножение? Есть, конечно, признаки того, почему это было бы правдой. GCC, как вы можете видеть выше, кажется (даже без оптимизации) считает, что избегать прямого умножения в пользу других инструкций - хорошая идея. Intel 64 и IA-32 Архитектуры Optimization Справочное руководство даст вам представление об относительной стоимости команд процессора. Другой ресурс, более сфокусированный на задержке инструкций и пропускной способности, - http://www.agner.org/optimize/instruction_tables.pdf., Обратите внимание, что они не являются хорошим предиктором абсолютного времени выполнения, но выполняют инструкции относительно друг друга. В тесном цикле, когда ваш тест имитирует, показатель «пропускная способность» должен быть наиболее актуальным. Это количество циклов, к которым исполнительный блок обычно привязывается при выполнении данной инструкции.
Так что, если сдвиг не быстрее, чем умножение? Как я уже говорил выше, современные архитектуры могут быть довольно сложными, а такие вещи, как предсказание ветвлений, кэширование, конвейеризация и параллельное выполнение, могут затруднить прогнозирование относительной производительности двух логически эквивалентных фрагментов кода за раз. Я действительно хочу подчеркнуть это, потому что именно здесь я недоволен большинством ответов на подобные вопросы, а также тем, что многие люди прямо говорят, что это просто неправда (больше), что смещение происходит быстрее, чем умножение.
Нет, насколько мне известно, мы не изобрели какой-то секретный инженерный соус в 1970-х или когда-либо, чтобы внезапно аннулировать разницу в стоимости единицы умножения и сдвига битов. Общее умножение с точки зрения логических элементов и, разумеется, с точки зрения логических операций, все еще сложнее, чем смещение со сдвигом бочки во многих сценариях на многих архитектурах. То, как это приводит к общему времени выполнения на настольном компьютере, может быть немного непрозрачным. Я не знаю наверняка, как они реализованы в конкретных процессорах, но вот объяснение умножения: действительно ли целочисленное умножение - такая же скорость, как сложение на современном CPU
В то время как здесь объяснение барреля Shifter . Документы, на которые я ссылался в предыдущем параграфе, дают другое представление об относительной стоимости операций через посредство инструкций процессора. Похоже, что у инженеров Intel часто возникают подобные вопросы: на форумах Intel для разработчиков часовые циклы для целочисленного умножения и добавления в процессор Core 2 Duo
Да, в большинстве реальных сценариев, и почти наверняка в JavaScript, попытка использовать эту эквивалентность ради производительности, вероятно, бесполезная задача. Однако даже если мы принудительно использовали инструкции умножения, а затем не увидели никакой разницы во времени выполнения, это объясняется, в частности, характером метрики стоимости, которую мы использовали, а не потому, что нет разницы в стоимости. Сквозное время выполнения - это одна метрика, и если она единственная, о которой мы заботимся, все хорошо. Но это не значит, что все различия в стоимости между умножением и сдвигом просто исчезли. И я думаю, что это, конечно, не очень хорошая идея - передавать эту идею спрашивающему, косвенно или иным образом, который, очевидно, только начинает понимать факторы, влияющие на время выполнения и стоимость современного кода. Инжиниринг всегда связан с компромиссами. Расследование и объяснение того, какие компромиссы сделали современные процессоры для демонстрации времени выполнения, которое мы, как пользователи, в конечном итоге видим, могут дать более дифференцированный ответ. И я думаю, что более дифференцированный ответ, чем «это просто больше не соответствует действительности», оправдан, если мы хотим, чтобы меньше инженеров проверяли микрооптимизированный код, уничтожая читабельность, потому что требуется более общее понимание природы таких «оптимизаций» для определить его различные, разнообразные воплощения, чем просто ссылаться на некоторые конкретные случаи как устаревшие.