Когда я проверяю разницу во времени между сдвигом и умножением в C, нет никакой разницы. Зачем?

28

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

#include <time.h>
#include <stdio.h>

int main() {
    clock_t launch = clock();
    int test = 0x01;
    int runs;

    //simple loop that oscillates between int 1 and int 2
    for (runs = 0; runs < 100000000; runs++) {


    // I first compiled + ran it a few times with this:
    test *= 2;

    // then I recompiled + ran it a few times with:
    test <<= 1;

    // set back to 1 each time
    test >>= 1;
    }

    clock_t done = clock();
    double diff = (done - launch);
    printf("%f\n",diff);
}

Для обеих версий распечатка была примерно 440000, сдавай или снимай 10000. Не было (визуально, по крайней мере) существенной разницы между выходами двух версий. Итак, мой вопрос, есть ли что-то не так с моей методологией? Должна ли вообще быть визуальная разница? Это как-то связано с архитектурой моего компьютера, компилятором или чем-то еще?

NicholasFolk
источник
47
Кто бы тебя ни учил, это было явно ошибочно. Это убеждение не было верным с 1970-х годов для типично используемых компиляторов на типично используемых архитектурах. Хорошо для проверки этого заявления. Я слышал это бессмысленное заявление о JavaScript ради бога.
Эрик Липперт
21
Лучший способ ответить на подобные вопросы - взглянуть на код сборки, который создает компилятор. Компиляторы обычно имеют возможность создать копию языка ассемблера, который они генерируют. Для компиляторов GNU GCC это '-S'.
Чарльз Грант
8
Следует отметить, что после просмотра этого gcc -Sкода код test *= 2фактически компилируется в значение « shll $1, %eax Когда вызывается с gcc -O3 -S», даже цикла нет. Два часовых вызова находятся на одной линии:callq _clock movq %rax, %rbx callq _clock
6
«Меня учили, что сдвиг в двоичном коде гораздо эффективнее, чем умножение на 2 ^ k»; нас учат многим вещам, которые оказываются неправильными (или, по крайней мере, устаревшими). Умный компилятор будет использовать одну и ту же операцию сдвига для обоих.
Джон Боде
9
Всегда, всегда проверяйте сгенерированный ассемблерный код при работе над этим видом оптимизации, чтобы убедиться, что вы измеряете то, что, по вашему мнению, измеряете. Огромное количество вопросов «почему я вижу это время» сводится к тому, что компилятор сводится к полному исключению операций, потому что результаты не используются.
Рассел Борогове

Ответы:

44

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

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

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

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

Thirler
источник
20
Всегда используйте операцию, которая семантически правильна. Если вы манипулировали битовыми масками или помещали маленькие целые числа в большие целые числа, смещение - подходящая операция.
ddyer
2
Будет ли когда-либо (практически говоря) необходимость оптимизировать умножение на оператора сдвига в программном приложении высокого уровня? Кажется, поскольку компилятор уже оптимизирует, полезно иметь эти знания только при программировании на очень низком уровне (по крайней мере, ниже компилятора).
Николас
11
@NicholasFolk Нету. Делай то, что проще всего понять. Если вы пишете сборку напрямую, это может быть полезно ... или если вы пишете оптимизирующий компилятор, опять же, это может быть полезно. Но вне этих двух случаев это хитрость, которая затемняет то, что вы делаете, и заставляет следующего программиста (который является убийцей топора, который знает, где вы живете ) проклинать ваше имя и думать о том, чтобы заняться хобби.
2
@NicholasFolk: Оптимизации на этом уровне почти всегда скрыты или оказанные спорную архитектурой процессора в любом случае. Кого волнует, если вы сэкономите 50 циклов, когда для извлечения аргументов из памяти и их записи требуется более 100? Подобные микрооптимизации имели смысл, когда память работала на (или близко к) скорости процессора, но не так много сегодня.
TMN
2
Потому что я устал видеть эти 10% этой цитаты, и потому, что она бьет по гвоздю в голову здесь: «Нет сомнений, что грааль эффективности ведет к злоупотреблениям. Программисты тратят огромное количество времени на размышления или беспокойство о, скорость некритических частей их программ, и эти попытки эффективности на самом деле имеют сильное негативное влияние при рассмотрении отладки и обслуживания. Мы должны забыть о небольшой эффективности, скажем, в 97% случаев: преждевременная оптимизация является корнем все зло ...
Цао
25

Компилятор распознает константы и при необходимости преобразует умножения в сдвиги.

ddyer
источник
Компилятор распознает константы, являющиеся степенями 2 .... и преобразует их в сдвиги. Не все константы могут быть изменены в смены.
quick_now
4
@quickly_now: Они могут быть преобразованы в комбинации сдвигов и дополнений / вычитаний.
Мердад
2
Классическая ошибка оптимизатора компилятора заключается в преобразовании делений в сдвиги вправо, которые работают для положительных дивидендов, но отключаются на 1 для отрицательных.
ddyer
1
@quickly_now Я полагаю, что термин «где это уместно» охватывает идею о том, что некоторые константы не могут быть переписаны как сдвиги.
Pharap
21

Будет ли смещение быстрее умножения, зависит от архитектуры вашего процессора. Когда-то во времена Pentium и ранее, сдвиг был часто быстрее, чем умножение, в зависимости от количества 1 бит в вашем множителе. Например, если ваш множитель был 320, это 101000000, два бита.

a *= 320;               // Slower
a = (a<<7) + (a<<9);    // Faster

Но если у вас было более двух битов ...

a *= 324;                        // About same speed
a = (a<<2) + (a<<7) + (a<<9);    // About same speed

a *= 340;                                 // Faster
a = (a<<2) + (a<<4) + (a<<7) + (a<<9);    // Slower

На маленьком микроконтроллере, таком как PIC18, с умножением на один цикл, но без сдвига ствола , умножение происходит быстрее, если вы сдвигаетесь более чем на 1 бит

a  *= 2;   // Exactly the same speed
a <<= 1;   // Exactly the same speed

a  *= 4;   // Faster
a <<= 2;   // Slower

Обратите внимание, что это противоположно тому, что было на старых процессорах Intel.

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

a  *= 4;   // 
b  *= 4;   // 

a <<= 2;   // Both lines execute in a single cycle
b <<= 2;   // 
Rocketmagnet
источник
5
+1 «Быстрее ли сдвиг, чем умножение, зависит от архитектуры вашего процессора». Спасибо, что вы немного углубились в историю и показали, что у большинства компьютерных мифов действительно есть логическая основа.
Pharap
11

У вас есть несколько проблем с вашей тестовой программой.

Во-первых, вы на самом деле не используете значение test. В стандарте C нет никакого значения, что значение testимеет значение. Оптимизатор это совершенно бесплатно, чтобы удалить его. Как только его удалили, ваш цикл фактически пуст. Единственным видимым эффектом было бы установить runs = 100000000, ноruns также не используется. Таким образом, оптимизатор может (и должен!) Удалить весь цикл. Легко исправить: также распечатать вычисленное значение. Обратите внимание, что достаточно определенный оптимизатор может оптимизировать цикл (он полностью зависит от констант, известных во время компиляции).

Во-вторых, вы делаете две операции, которые отменяют друг друга. Оптимизатор может заметить это и отменить их . Снова оставив пустую петлю, удалили. Это совершенно сложно исправить. Вы можете переключиться на unsigned int(так что переполнение не является неопределенным поведением), но это, конечно, просто приводит к 0. И простые вещи (например, скажем, test += 1) оптимизатору достаточно легко понять, и это происходит.

Наконец, вы предполагаете, что test *= 2на самом деле будет скомпилировано с умножением. Это очень простая оптимизация; если битовое смещение быстрее, оптимизатор будет использовать его вместо этого. Чтобы обойти это, вам нужно использовать что-то вроде встроенной сборки, специфичной для реализации.

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

Когда я проверил вывод сборки при компиляции вашей программы с gcc -S -O3использованием версии 4.9, оптимизатор фактически просмотрел все простые варианты выше и еще несколько. Во всех случаях он удалял цикл (присваивая константу test), оставались только вызовы clock(), преобразование / вычитание и printf.

derobert
источник
1
Также обратите внимание, что оптимизатор может (и будет) оптимизировать операции над константами (даже в цикле), как показано в sqrt c # vs sqrt c ++, где оптимизатор смог заменить цикл, суммирующий значение, с фактической суммой. Чтобы победить эту оптимизацию, вам нужно использовать что-то определенное во время выполнения (например, аргумент командной строки).
@MichaelT Да. Вот что я имел в виду под «Заметим, что достаточно определенный оптимизатор все еще может оптимизировать цикл (он полностью зависит от констант, известных во время компиляции)».
Дероберт
Я понимаю, что вы говорите, но я не думаю, что компилятор удаляет весь цикл. Вы можете легко проверить эту теорию, просто увеличив число итераций. Вы увидите, что увеличение количества итераций делает программу дольше. Если бы петля была полностью удалена, это было бы не так.
DollarAkshay
@AkshayLAradhya Я не могу сказать, что делает ваш компилятор, но я еще раз подтвердил, что gcc -O3(теперь с 7.3) все еще полностью удаляет цикл. (Обязательно переключайтесь на long вместо int, если требуется, иначе он оптимизирует его в бесконечный цикл из-за переполнения).
Дероберт
8

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

Результирующая относительная среда выполнения сдвига и умножения не имеет ничего общего с 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, попытка использовать эту эквивалентность ради производительности, вероятно, бесполезная задача. Однако даже если мы принудительно использовали инструкции умножения, а затем не увидели никакой разницы во времени выполнения, это объясняется, в частности, характером метрики стоимости, которую мы использовали, а не потому, что нет разницы в стоимости. Сквозное время выполнения - это одна метрика, и если она единственная, о которой мы заботимся, все хорошо. Но это не значит, что все различия в стоимости между умножением и сдвигом просто исчезли. И я думаю, что это, конечно, не очень хорошая идея - передавать эту идею спрашивающему, косвенно или иным образом, который, очевидно, только начинает понимать факторы, влияющие на время выполнения и стоимость современного кода. Инжиниринг всегда связан с компромиссами. Расследование и объяснение того, какие компромиссы сделали современные процессоры для демонстрации времени выполнения, которое мы, как пользователи, в конечном итоге видим, могут дать более дифференцированный ответ. И я думаю, что более дифференцированный ответ, чем «это просто больше не соответствует действительности», оправдан, если мы хотим, чтобы меньше инженеров проверяли микрооптимизированный код, уничтожая читабельность, потому что требуется более общее понимание природы таких «оптимизаций» для определить его различные, разнообразные воплощения, чем просто ссылаться на некоторые конкретные случаи как устаревшие.

user2880576
источник
6

То, что вы видите, является эффектом оптимизатора.

Задача оптимизаторов состоит в том, чтобы сделать полученный скомпилированный код меньшим или более быстрым (но редко одновременно в одно и то же время ... но, как и многие другие вещи ... ЭТО ЗАВИСИМО от того, что это за код).

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

Итак ... если наивный компилятор сгенерирует вызов библиотеки для операции * 2, то, конечно, он будет работать медленнее, чем битовый сдвиг *.

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

Просто ради интереса я только сегодня смотрел на сгенерированный ассемблер для некоторых операций, таких как * 5 ... на самом деле не на это, а на другие вещи, и по пути я заметил, что компилятор превратил * 5 в:

  • сдвиг
  • сдвиг
  • добавить оригинальный номер

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

Искусство оптимизаторов компиляторов - это отдельная тема, наполненная магией, и действительно правильно понятая примерно 6 людьми на всей планете :)

quickly_now
источник
3

Попробуйте синхронизировать это с:

for (runs = 0; runs < 100000000; runs++) {
      ;
}

Компилятор должен распознавать, что значение testпосле каждой итерации цикла остается неизменным, а окончательное значение testне используется, и полностью исключать цикл.

Рассел Борогове
источник
2

Умножение представляет собой комбинацию сдвигов и дополнений.

В случае, который вы упомянули, я не думаю, что имеет значение, оптимизирует ли это компилятор или нет - «умножить xна два» можно реализовать так:

  • Сдвиньте биты xодного места влево.
  • Добавить xв x.

Это все основные атомарные операции; один не быстрее, чем другой.

Измените его на «умножить xна четыре» (или любое 2^k, k>1другое), и оно будет немного другим:

  • Сдвиньте биты из xдвух мест влево.
  • Добавить xк xи назвать его y, добавить yк y.

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

Попробуйте последнее (или любое 2^k, k>1) с соответствующими параметрами, чтобы вы не могли оптимизировать их, чтобы они были одинаковыми при реализации. Вы должны найти сдвиг быстрее, принимая O(1)по сравнению с повторным добавлением в O(k).

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

OJFord
источник
1
Что такое «базовая атомная операция»? Разве нельзя утверждать, что в смене операция может применяться к каждому биту параллельно, в то время как, кроме того, самые левые биты зависят от других битов?
Берги
2
@ Берджи: я предполагаю, что он имеет в виду, что как shift, так и add являются одними машинными инструкциями Вам нужно взглянуть на документацию по набору команд, чтобы увидеть количество циклов для каждого, но да, добавление часто является многоцикловой операцией, тогда как сдвиг обычно выполняется за один цикл.
TMN
Да, это может быть так, но умножение - это тоже отдельная машинная инструкция (хотя, конечно, может потребоваться больше циклов)
Берги
@ Берги, это тоже зависит от арки. Какую дугу вы думаете о том, что сдвиг происходит за меньшее количество циклов, чем 32-битное сложение (или x-бит, если применимо)?
OJFord
Я не знаю какой-либо конкретной архитектуры, нет (и мои курсы компьютерной инженерии исчезли), вероятно, обе инструкции занимают менее одного цикла. Я, вероятно, думал с точки зрения микрокода или даже логических элементов, где сдвиг, вероятно, будет дешевле.
Берги
1

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

Однако следует отметить, что деление потенциально отрицательных знаковых значений не эквивалентно смещению вправо. Выражение как (x+8)>>4не эквивалентно (x+8)/16. Первые в 99% компиляторов будут отображать значения от -24 до -9 до -1, от -8 до +7 до 0 и от +8 до +23 до 1 [округление чисел почти симметрично до нуля]. Последний будет отображать от -39 до -24 до -1, от -23 до +7 до 0 и от +8 до +23 до +1 [крайне асимметрично, и, вероятно, не то, что предполагалось]. Обратите внимание, что даже если значения не должны быть отрицательными, использование >>4, скорее всего, даст более быстрый код, чем /16если компилятор не сможет доказать, что значения не могут быть отрицательными.

Supercat
источник
0

Еще немного информации, которую я только что проверил.

На x86_64 код операции MUL имеет задержку 10 циклов и пропускную способность 1/2 цикла. MOV, ADD и SHL имеют задержку 1 цикл с пропускной способностью 2,5, 2,5 и 1,7 цикла.

Умножение на 15 потребует минимум 3 операции SHL и 3 операции ADD и, возможно, пару MOV.

https://gmplib.org/~tege/x86-timing.pdf

Рич Ремер
источник
0

Ваша методология ошибочна. Ваша проверка приращения цикла и сама проверка состояния занимают так много времени.

  • Попробуйте запустить пустой цикл и измерить время (назовите его base).
  • Теперь добавьте 1 смену и измерьте время (назовите это s1).
  • Затем добавьте 10 операций смены и измерьте время (назовите это s2)

Если все идет правильно, base-s2должно быть в 10 раз больше base-s1. Иначе что-то еще вступает в игру здесь.

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

int main(){

    int test = 2;
    clock_t launch = clock();

    test << 6;
    test << 6;
    test << 6;
    test << 6;
    //.... 1 million times
    test << 6;

    clock_t done = clock();
    printf("Time taken : %d\n", done - launch);
    return 0;
}

И вот у вас есть ваш результат

1 миллион сменных операций менее чем за 1 миллисекунду? ,

Я сделал то же самое для умножения на 64 и получил тот же результат. Так что, вероятно, компилятор полностью игнорирует операцию, поскольку другие упоминали, что значение test никогда не изменяется.

Оператор сдвига Результат

DollarAkshay
источник