Действительно ли умножение целых чисел выполняется с той же скоростью, что и сложение на современном процессоре?

Я довольно часто слышу это утверждение, что умножение на современном оборудовании настолько оптимизировано, что на самом деле происходит с той же скоростью, что и сложение. Это правда?

Я никогда не могу получить никакого авторитетного подтверждения. Мое собственное исследование только добавляет вопросов. Тесты скорости обычно показывают данные, которые меня смущают. Вот пример:

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

unsigned int time1000() {
    timeval val;
    gettimeofday(&val, 0);
    val.tv_sec &= 0xffff;
    return val.tv_sec * 1000 + val.tv_usec / 1000;
}

int main() {
    unsigned int sum = 1, T = time1000();
    for (int i = 1; i < 100000000; i++) {
        sum += i + (i+1); sum++;
    }
    printf("%u %u\n", time1000() - T, sum);
    sum = 1;
    T = time1000();
    for (int i = 1; i < 100000000; i++) {
        sum += i * (i+1); sum++;
    }
    printf("%u %u\n", time1000() - T, sum);
}

Приведенный выше код может показать, что умножение выполняется быстрее:

clang++ benchmark.cpp -o benchmark
./benchmark
746 1974919423
708 3830355456

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


person exebook    schedule 17.02.2014    source источник
comment
Также стоит отметить, что ваши счетчики циклов и логика суммирования также потребляют ресурсы сумматора.   -  person Mysticial    schedule 17.02.2014
comment
@Mysticial: Не то чтобы это действительно было связано с этим вопросом, но вы случайно не знаете, есть ли быстрый способ разделить на 11 (быстрее, чем умножение на обратное)? Мне это нужно было несколько дней назад...   -  person user541686    schedule 17.02.2014
comment
@Mehrdad Вы ищете что-то более быстрое, чем целочисленное умножение на инвариантный трюк, который компилятор уже должен уметь делать?   -  person Mysticial    schedule 17.02.2014
comment
Но помните, что хотя умножение и медленнее, на современном оборудовании и сложение, и умножение выполняются настолько быстро, что вам очень редко нужно об этом беспокоиться — посмотрите, как тяжело вам было это продемонстрировать.   -  person Tim Bergel    schedule 17.02.2014
comment
@Mysticial: Да, я ищу что-то более быстрое, чем умножение, то есть битовые сдвиги и тому подобное.   -  person user541686    schedule 17.02.2014
comment
Похоже, что ваш процессор может выполнять сложение и умножение одновременно, но не может выполнять два сложения одновременно. Ваш первый цикл использует сумматор дважды на каждой итерации; ваш второй цикл использует сумматор один раз и один раз множитель. Первый цикл делает два сложения последовательно; ваш второй цикл выполняет их параллельно, поэтому вы, вероятно, видите, что умножение выполняется быстрее, чем два сложения.   -  person Sergey Kalinichenko    schedule 17.02.2014
comment
@TimBergel: это ложь. Я довольно легко и легко заметил разницу всего несколько дней назад и был на самом деле весьма удивлен ею (я сэкономил 1,8 секунды на 9-секундном задании, см. мой ответ).   -  person user541686    schedule 17.02.2014
comment
@Mehrdad: да, я полагаю, что да, и стоит беспокоиться о том, пишете ли вы код 3D-рендеринга или что-то подобное, но я чувствую, что это не слишком важно для обычного программирования, особенно если учесть все другие способы. замедлить свой код....   -  person Tim Bergel    schedule 17.02.2014
comment
@TimBergel: В моей ситуации даже близко не было 3D-рендеринга. Это было довольно типично — все, что я делал, это реализовывал и использовал кучу (по сути, я повторно реализовывал свою собственную версию std::push_heap и т. д.), чтобы я мог обрабатывать элементы в порядке приоритета. У меня закончились идеи, чтобы сделать его быстрее; мне потребовалось много попыток отладки/профилирования, чтобы выяснить причину, и я остался чесать голову, когда увидел инструкцию imul, которую я не ожидал. Я знаю, это удивительно, но моя задача была чертовски типичной, и это вычитание указателя в куче действительно было узким местом.   -  person user541686    schedule 17.02.2014
comment
@Mysticial: Если вам интересно, вопрос о делении на 11 был связан с чем-то похожим: я также пытался реализовать кучу 2-3 (арность чередуется как 2+3+3+3), и, следовательно, я нужно было разделить на 11. Это оказалось слишком медленным из-за умножения, поэтому я отказался от кучи 2-3, но именно поэтому я спросил, знаете ли вы, как быстро разделить на 11.   -  person user541686    schedule 17.02.2014
comment
@Mehrdad Я подсчитал и не думаю, что можно сделать лучше. 11 не может быть представлено меньшим, чем сумма/разность трех степеней двоек. Так что потребуется минимум 2 смены. Самый быстрый сдвиг на Haswell — 0,5 r-пропускной способности. Умножение равно 1,0 r-пропускная способность.   -  person Mysticial    schedule 17.02.2014
comment
@Mystial: один левый сдвиг 3 (умножить на 8), один lea (умножить на 3) и один добавить, верно? Но это для умножения, а не для деления.   -  person Ben Voigt    schedule 17.02.2014
comment
@Mysticial: О, понятно, хорошая мысль. :( Спасибо!   -  person user541686    schedule 17.02.2014
comment
Подождите, на самом деле я думаю, что две инструкции LEA могут это сделать. Первый раз 3, второй раз 8 плюс первый. Дополнительные примеры см. на странице stackoverflow.com/a/20164921/103167.   -  person Ben Voigt    schedule 17.02.2014
comment
@BenVoigt: Это всего лишь небольшие умножения, верно? Не помогает при делении на 11 (хотя интересно).   -  person user541686    schedule 17.02.2014
comment
@Mehrdad: Да, это для небольших умножений. Когда Mysticial сказал 3 степени двойки, это было для 11 или обратно?   -  person Ben Voigt    schedule 17.02.2014
comment
Связанный: electronics.stackexchange.com/q/452181/192433   -  person user1271772    schedule 18.02.2021


Ответы (12)


Умножение двух n-битных чисел на самом деле может быть выполнено за O(log n) глубины схемы, как и сложение.

Сложение за O(log n) выполняется путем деления числа пополам и (рекурсивного) добавления двух частей параллельно, где верхняя половина вычисляется для обоих " 0-carry" и "1-carry". Как только нижняя половина добавлена, перенос проверяется, и его значение используется для выбора между 0-переносом и 1-переносом.

Умножение в глубину O(log n) также выполняется с помощью распараллеливания, где каждая сумма 3 чисел сводится к сумме всего 2 чисел, параллельных, и суммы делается таким же образом, как описано выше.
Я не буду объяснять это здесь, но вы можете найти материалы для чтения по быстрому сложению и умножению, выполнив поиск по "carry-lookahead" и " перенос-сохранение" дополнение.

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

person user541686    schedule 22.07.2014
comment
Я немного слышу то, что вы говорите, однако посмотрите сюда, и вы увидите, что умножение ПО-ПРЕЖНЕМУ зависит от множественных сложений!!! Цифры на слайде 53 получены непосредственно от AMD. Вы можете быть правы, но это также проблема размера кристалла и стоимости реализации по сравнению со статистическим использованием (по сравнению с одним лишь добавлением). cis.upenn.edu/~milom/cis371-Spring08/ лекции/04_integer.pdf - person trumpetlicks; 06.09.2018
comment
@trumpetlicks: Смущен, я сказал, что умножение не зависит от сложений? Я думал, что сам прямо сказал, что умножение зависит от сложений. - person user541686; 06.09.2018
comment
Вы вроде как сделали, но вы также заявили, что теоретический ответ был на 100% неверным, а это не так. Если бы вы не делали ничего, кроме умножения, то вы были бы правы, умножитель CSA выдавал бы результат за каждый такт ЦП, это было бы просто N скрытых тактовых циклов с момента ввода входных данных. Однако, если вы хотите делать умножения, окруженные с обеих сторон другими операциями, тогда каждое отдельное умножение НЕ будет таким же быстрым, как сложение. Сумматор CSA по-прежнему представляет собой НАБОР конвейерных (т. е. нескольких тактов) дополнений. - person trumpetlicks; 07.09.2018
comment
@trumpetlicks: Чт....? О чем ты говоришь? Я даже не использовал слова цикл, часы, конвейер и т. д. в своем ответе. Я также никогда не утверждал, что умножение выполняется так же быстро, как сложение (опять же, я сказал обратное). Я не знаю, какой ответ вы читаете, который является правильным или неправильным по этим вопросам, но это определенно не мой... - person user541686; 07.09.2018

Целочисленное умножение будет медленнее.

таблицы инструкций Agner Fog показывают, что при использовании 32-битных целочисленных регистров ADD/SUB Haswell занимает 0,25– 1 цикл (в зависимости от того, насколько хорошо конвейеризированы ваши инструкции), в то время как MUL занимает 2–4 цикла. С плавающей запятой все наоборот: ADDSS/SUBSS занимает 1–3 цикла, а MULSS — 0,5–5 тактов.

person Cory Nelson    schedule 17.02.2014
comment
Кто вам это сказал? -- Честно говоря, возможно, кто-то, кто перепутал это с FP add/mul. Возможно, понятная ошибка. - person Dolda2000; 17.02.2014
comment
Кроме того, в то время как MUL занимает 2–4 цикла -- Хотя это правда, это всего лишь задержка. MUL, скорее всего, имеет пропускную способность (по крайней мере) одну инструкцию за цикл. По крайней мере, так обстоит дело с Sandy Bridge, и я сомневаюсь, что у Haswell дела обстоят хуже. ADDSS/MULSS также имеют пропускную способность в одну инструкцию за цикл на Sandy Bridge (но задержки 3 и 5 соответственно). - person Dolda2000; 17.02.2014
comment
@ Dolda2000 это то, что Агнер называет пропускной способностью 32-битного MUL/IMUL с использованием только регистров. Интересно, что он указывает 64-битный вариант как 1 цикл, а не как 2 — интересно, это опечатка. - person Cory Nelson; 17.02.2014
comment
@ Кори Нельсон, я проголосовал за вас, но эта ссылка на страницу Агнера Фога ничего не говорит о количестве тактов, не так ли? - person user1271772; 09.08.2019
comment
@user1271772 user1271772 столбец «Задержка» определяет минимальную задержку в тактовых циклах для каждой инструкции. Обратите внимание, что конвейеризация означает, что инструкции могут перекрываться, поэтому это не указывает на пропускную способность. - person Cory Nelson; 09.08.2019
comment
@CoryNelson, я имею в виду, что ссылка, которую вы дали, просто содержит кучу URL-адресов на другие страницы. Кажется, он ничего не говорит о задержке или тактовых циклах! - person user1271772; 09.08.2019
comment
@ user1271772: Таблицы инструкций Agner Fog доступны в формате PDF или электронной таблицы, а микроархив PDF действительно полезен для понимания того, что означают таблицы инструкций. Обычно я ссылаюсь на agner.org/optimize, а не на agner.org/optimize/instruction_tables.pdf, при цитировании данных из него. (Хотя uops.info в наши дни содержит еще более подробные результаты тестов, особенно для инструкций, состоящих из нескольких команд с разной задержкой из разных входы к выходу.) - person Peter Cordes; 26.06.2020
comment
@ Dolda2000: один операнд mul (32x32 => 64-битный) имеет пропускную способность один на 2 цикла на Haswell и стоит 3 мэкв. (2 моп для 64x64 => 128-бит, я думаю, не нужны дополнительные моп, чтобы разделить младшие 64 на две 32-битные половины). Проблема в том, что это не обычный способ умножения. Умножение без расширения выполняется с помощью imul reg,reg/mem или imul reg, reg/mem, immediate, что всегда составляет 1 мкп, пропускная способность 1c в семействе Intel SnB и Zen для любого размера операнда, записывая только один выходной регистр. - person Peter Cordes; 26.06.2020
comment
Интересно, что вы нашли это через год после того, как я написал свои комментарии здесь @PeterCordes. Я оказался тем же парнем, который предложил награду в 200 баллов за умножение длинных целых чисел. Горячий сетевой вопрос на бирже стека моделирования материи! Теперь я также вижу, что вы прокомментировали мой HNQ по электротехнике: electronics.stackexchange.com/q/452181/192433 (что на самом деле возникло из этого вопроса о переполнении стека). - person user1271772; 26.06.2020

Это еще более сложный ответ, чем просто умножение против сложения. На самом деле ответ, скорее всего, НИКОГДА не будет «да». Электронное умножение представляет собой гораздо более сложную схему. Большинство причин этого заключается в том, что умножение — это действие шага умножения, за которым следует шаг сложения. Вспомните, каково было умножать десятичные числа до использования калькулятора.

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

С ТЕХНИЧЕСКОЙ точки зрения, если бы умножение было единственным, что вы делаете (без циклов, подсчета и т. д.), умножение было бы от 2 до (как я видел на PPC-архитектурах) в 35 раз медленнее. Это больше упражнение в понимании вашей архитектуры и электроники.

В дополнение: следует отметить, что МОЖЕТ быть построен процессор, для которого ВСЕ операции, включая умножение, занимают один такт. Что этот процессор должен был бы сделать, так это избавиться от всей конвейерной обработки и замедлить часы, чтобы задержка HW любой схемы OP была меньше или равна задержке, ПРЕДОСТАВЛЯЕМОЙ синхронизацией часов.

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

Картина времени через умножение:

|------------------------------------------------- -| Неконвейерный

|--Шаг 1--|--Шаг 2--|--Шаг 3--|--Шаг 4--|--Шаг 5--| Конвейерный

На приведенной выше диаграмме неконвейерная схема занимает 50 единиц времени. В конвейерной версии мы разделили 50 единиц на 5 шагов, каждый из которых занимает 10 единиц времени, с промежуточным шагом сохранения. ЧРЕЗВЫЧАЙНО важно отметить, что в конвейерном примере каждый из шагов может работать полностью самостоятельно и параллельно. Чтобы операция была завершена, она должна пройти все 5 шагов по порядку, но другая такая же операция с операндами может быть на шаге 2, как и на шагах 1, 3, 4 и 5.

С учетом всего сказанного, этот конвейерный подход позволяет нам непрерывно заполнять оператор каждый такт и получать результат на каждом такте, ЕСЛИ мы можем упорядочить наши операции таким образом, чтобы мы могли выполнять все одну операцию, прежде чем мы переключаемся к другой операции, и все, что мы принимаем в качестве тайминга, — это исходное количество часов, необходимое для получения ПЕРВОЙ операции из конвейера.

Mystical поднимает еще один хороший момент. Также важно смотреть на архитектуру с более системной точки зрения. Это правда, что более новые архитектуры Haswell были созданы для повышения производительности умножения с плавающей запятой в процессоре. По этой причине системный уровень был спроектирован таким образом, чтобы одновременно выполнять несколько умножений по сравнению с добавлением, которое может происходить только один раз за системные часы.

Все это можно резюмировать следующим образом:

  1. Каждая архитектура отличается с точки зрения HW более низкого уровня, а также с точки зрения системы.
  2. ФУНКЦИОНАЛЬНО умножение всегда будет занимать больше времени, чем сложение, потому что оно сочетает в себе истинное умножение с истинным шагом сложения.
  3. Поймите архитектуру, на которой вы пытаетесь запустить свой код, и найдите правильный баланс между удобочитаемостью и получением действительно лучшей производительности от этой архитектуры.
person trumpetlicks    schedule 17.02.2014
comment
Ничего не стоит тот факт, что на Haswell пропускная способность умножения FP в два раза выше, чем у FP add. Это связано с тем, что оба порта 0 и 1 могут использоваться для умножения, но только порт 1 может использоваться для сложения. Тем не менее, вы можете схитрить с добавлением fused-multiply, так как оба порта могут это делать. - person Mysticial; 17.02.2014
comment
@Mysticial - Хороший вопрос, а также еще одна причина, по которой мой ответ носит более общий характер, говоря об архитектуре машины :-) Отличное примечание. - person trumpetlicks; 17.02.2014
comment
@Mysticial - следует отметить, однако, что хотя архитектура поддерживает 2 множителя FP по сравнению с одним добавлением, это не означает, что при рассмотрении одного оператора умножения по сравнению с одним множителем добавления он работает быстрее. Опять же, это архитектура уровня системы HW, а не прямая мера производительности одного добавления по сравнению с одним умножением. - person trumpetlicks; 17.02.2014
comment
Понятно, что умножение медленнее, чем сложение, поэтому почему мы берем удельную стоимость для обоих в анализе алгоритмов? - person Shajeel Afzal; 05.12.2018
comment
Это правда, что более новые архитектуры Haswell были созданы для повышения производительности умножения с плавающей запятой в процессоре. По этой причине системный уровень был спроектирован таким образом, чтобы одновременно выполнять несколько умножений по сравнению с добавлением, которое может происходить только один раз за системные часы. Сколько умножений может выполнять один поток Haswell одновременно и сколько добавлений он может выполнять одновременно? - person user1271772; 09.08.2019
comment
@Mysticial Вы заметили, что ваш комментарий стал этим вопросом? Я также не знаю, была ли у вас возможность подумать над моим вопросом в моем комментарии от августа 2019 г. (без давления, если вы очень заняты, мне просто было очень любопытно). - person user1271772; 29.06.2020

Intel с тех пор, как Haswell

  • add производительность 4/такт, задержка 1 цикл. (любой размер операнда)
  • imul производительность 1/такт, задержка 3 такта. (любой размер операнда)

Райзен похож. Семейство Bulldozer имеет гораздо более низкую целочисленную пропускную способность и не полностью конвейерное умножение, в том числе очень медленное для умножения 64-битного размера операнда. См. https://agner.org/optimize/ и другие ссылки в https://stackoverflow.com/tags/x86/info

Но хороший компилятор может автоматически векторизовать ваши циклы. (Пропускная способность и задержка при умножении SIMD-целого числа хуже, чем при добавлении SIMD-целого числа). Или просто проведите через них постоянное распространение, чтобы просто распечатать ответ! Clang действительно знает формулу Гаусса в закрытой форме для sum(i=0..n) и может распознать некоторые циклы, которые это делают.


Вы забыли включить оптимизацию, поэтому оба цикла являются узким местом в ALU + задержка сохранения/перезагрузки хранения sum в памяти между каждым из sum += independent stuff и sum++. См. Почему clang производит неэффективные asm с -O0 (для этой простой суммы с плавающей запятой)? чтобы узнать больше о том, насколько плох результирующий asm и почему это так. clang++ по умолчанию равно -O0 (режим отладки: храните переменные в памяти, где отладчик может изменять их между любыми операторами C++).

Задержка переадресации хранилища на современных системах x86, таких как семейство Sandybridge (включая Haswell и Skylake), составляет от 3 до 5 циклов, в зависимости от времени перезагрузки. Таким образом, с ALU add с задержкой в ​​1 цикл вы также видите около двух шагов задержки в 6 циклов на критическом пути для этого цикла. (Много, чтобы скрыть все хранилище / перезагрузку и расчет на основе i, а также обновление счетчика циклов).

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


Современные процессоры x86 имеют пропускную способность, умножающую 1/такт, поэтому даже с оптимизацией вы не увидите узкого места в пропускной способности. Или на семействе Bulldozer, не полностью конвейерном с пропускной способностью 1 на 2 часа.

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

Хотя lea позволяет очень эффективно копировать и добавлять и выполнять i + i + 1 с помощью одной инструкции. Хотя на самом деле хороший компилятор увидит, что цикл использует только 2*i и оптимизирует для увеличения на 2. То есть уменьшение силы для повторного добавления на 2 вместо необходимости сдвига внутри цикла.

И, конечно же, с оптимизацией дополнительные sum++ могут просто складываться в sum += stuff, где stuff уже включает константу. Не так с умножением.

person Peter Cordes    schedule 10.08.2019

Я пришел в эту тему, чтобы получить представление о том, что современные процессоры делают в отношении целочисленной математики и количества циклов, необходимых для их выполнения. Я работал над проблемой ускорения операций умножения и деления 32-битных целых чисел на процессоре 65c816 в 1990-х годах. Используя описанный ниже метод, я смог утроить скорость стандартных математических библиотек, доступных в то время в компиляторах ORCA/M.

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

На процессоре 65c816 не было инструкций умножения или деления. Mult и Div были выполнены со сдвигами и добавлениями.
Чтобы выполнить 16-битное добавление, вы должны сделать следующее:

LDA $0000 - loaded a value into the Accumulator (5 cycles)
ADC $0002 - add with carry  (5 cycles)
STA $0004 - store the value in the Accumulator back to memory (5 cycles)
15 cycles total for an add

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

Традиционный способ выполнения умножения - это сдвиг и сложение всего значения одного числа. Каждый раз, когда перенос становится единицей, поскольку он смещается влево, это означает, что вам нужно снова добавить значение. Это требовало проверки каждого бита и сдвига результата.

Я заменил это таблицей поиска из 256 элементов, чтобы не нужно было проверять биты переноса. Также можно было определить переполнение перед выполнением умножения, чтобы не терять время. (На современном процессоре это можно сделать параллельно, но я не знаю, делают ли они это аппаратно). Учитывая два 32-битных числа и предварительно экранированное переполнение, один из множителей всегда равен 16 битам или меньше, поэтому для выполнения всего 32-битного умножения достаточно один или два раза выполнить 8-битное умножение. Результатом этого стало умножение в 3 раза быстрее.

скорость 16-битного умножения варьировалась от 12 до примерно 37 тактов.

multiply by 2  (0000 0010)
LDA $0000 - loaded a value into the Accumulator  (5 cycles).
ASL  - shift left (2 cycles).
STA $0004 - store the value in the Accumulator back to memory (5 cycles).
12 cycles plus call overhead.
multiply by (0101 1010)
LDA $0000 - loaded a value into the Accumulator  (5 cycles) 
ASL  - shift left (2 cycles) 
ASL  - shift left (2 cycles) 
ADC $0000 - add with carry for next bit (5 cycles) 
ASL  - shift left (2 cycles) 
ADC $0000 - add with carry for next bit (5 cycles) 
ASL  - shift left (2 cycles) 
ASL  - shift left (2 cycles) 
ADC $0000 - add with carry for next bit (5 cycles) 
ASL  - shift left (2 cycles) 
STA $0004 - store the value in the Accumulator back to memory (5 cycles)
37 cycles plus call overhead

Поскольку шина данных AppleIIgs, для которой это было написано, имела ширину всего 8 бит, для загрузки 16-битных значений требовалось 5 циклов для загрузки из памяти, один дополнительный для указателя и один дополнительный цикл для второго байта.

Инструкция LDA (1 цикл, так как это 8-битное значение) $0000 (для загрузки 16-битного значения требуется два цикла) ячейка памяти (для загрузки требуется два цикла из-за 8-битной шины данных)

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

Чтобы выполнить полное 32-битное умножение, вам нужно будет сделать это дважды и сложить результаты вместе, чтобы получить окончательный ответ. Современные процессоры должны иметь возможность выполнять эти два задания параллельно и добавлять результаты к ответу. В сочетании с предварительной проверкой переполнения, выполняемой параллельно, это минимизирует время, необходимое для выполнения умножения.

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

С уважением, Кен

person Kenneth C Richardson    schedule 18.11.2019
comment
Любая оценка современного процессора (например, Intel i7), сколько циклов может занять ADD или MUL? - person adieux; 27.02.2020

Умножение требует последнего шага добавления как минимум того же размера числа; так что это займет больше времени, чем дополнение. В десятичном виде:

    123
    112
   ----
   +246  ----
   123      | matrix generation  
  123    ----
  -----
  13776 <---------------- Addition

То же самое относится и к двоичному коду, но с более сложной редукцией матрицы.

Тем не менее, причины, по которым они могут занимать одинаковое количество времени:

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

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

Единственный способ запустить этот тест строго — это запустить его в сборке и без операционной системы — иначе будет слишком много переменных.

person user3684405    schedule 28.05.2014

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

Если бы мы могли часы выше, мы могли бы определить. сделать ADD быстрее, вероятно, на несколько порядков.

person mathreadler    schedule 09.02.2017

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

Другим ЦП требуется три или четыре цикла для выполнения умножения, что немного медленнее, чем сложение. Но это далеко не то снижение производительности, с которым вам пришлось столкнуться десять лет назад (тогда 32-битное умножение могло занять тридцать с чем-то циклов на некоторых процессорах).

Итак, да, в настоящее время умножение относится к тому же классу скорости, но нет, оно все еще не так быстро, как сложение на всех процессорах.

person cmaster - reinstate monica    schedule 09.05.2014
comment
Это не в том же классе скорости, потому что часы достигли предела скорости. Это означает, что ЦП простаивает гораздо больше времени в течение одного тактового цикла, если вы выполняете ADD, потому что, поскольку транзисторы уменьшились, но максимальная тактовая частота осталась прежней, поэтому сигнал мог пройти больше вентилей за один такт. - person mathreadler; 10.02.2017
comment
@mathreadler Сколько времени требуется аппаратным вентилям для получения правильного результата, совершенно не имеет значения для программного обеспечения: результат не может вступить в силу до начала следующего тактового цикла. Кроме того, вы можете поспорить с разработчиками микросхем, что они не создадут сумматоры, которые значительно быстрее, чем они должны быть для выполнения требований предполагаемой тактовой частоты: это означало бы использование драгоценного недвижимого имущества микросхемы для бессмысленно сложных генераторов с опережением переноса. Эти схемы необходимы, потому что даже сегодня невозможно передать 64-битный перенос за один цикл. - person cmaster - reinstate monica; 10.02.2017

Даже в ARM (известном своей высокой эффективностью и небольшим, чистым дизайном) целочисленное умножение занимает 3-7 циклов, а целочисленное сложение занимает 1 цикл.

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

Причина, по которой это хорошо работает на ARM, заключается в том, что ARM имеет механизм сдвига ствола, который позволяет многим инструкциям сдвигать или поворачивать один из своих аргументов на 1-31 бит без каких-либо затрат, т. е. x = a + b и x = a + (b << s) занимают одинаковое количество времени.

Допустим, используя эту функцию процессора, вы хотите вычислить a * 15. Затем, начиная с 15 = 1111 (base 2), следующий псевдокод (переведенный на сборку ARM) будет реализовывать умножение:

a_times_3 = a + (a << 1)                  // a * (0011 (base 2))
a_times_15 = a_times_3 + (a_times_3 << 2) // a * (0011 (base 2) + 1100 (base 2))

Точно так же вы можете умножить на 13 = 1101 (base 2), используя любой из следующих способов:

a_times_5 = a + (a << 2)
a_times_13 = a_times_5 + (a << 3)
a_times_3 = a + (a << 1)
a_times_15 = a_times_3 + (a_times_3 << 2)
a_times_13 = a_times_15 - (a << 1)

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

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

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

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

person Luke Hutchison    schedule 05.09.2020

Здесь есть много хороших ответов на ваш основной вопрос, но я просто хотел указать, что ваш код не является хорошим способом измерения производительности операции. Во-первых, современные процессоры постоянно настраивают частоты, поэтому вы должны использовать rdtsc для подсчета фактического количества циклов, а не прошедших микросекунд. Но что еще более важно, в вашем коде есть искусственные цепочки зависимостей, ненужная управляющая логика и итераторы, которые превратят вашу меру в странную смесь задержки и пропускной способности, а также некоторые константы, добавленные без всякой причины. Чтобы действительно измерить пропускную способность, вы должны значительно развернуть цикл, а также добавить несколько частичных сумм параллельно (больше сумм, чем шагов в конвейерах add/mul cpu).

person paperclip optimizer    schedule 07.06.2021

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

Я понял это сам, задав этот вопрос всего несколько дней назад здесь.

person user541686    schedule 17.02.2014
comment
С другой стороны, у вас было 64-битное умножение, используемое для эмуляции 32-битного деления. - person Ben Voigt; 17.02.2014
comment
@BenVoigt: Подождите, есть ли более 32-битное 32-битное умножение, чем то, которое использовал компилятор? - person user541686; 17.02.2014
comment
Есть умножения, дающие 32-битные результаты, но я не уверен ни в одном, которое берет произвольные 32-битные операнды и дает 32-битный результат. - person Ben Voigt; 17.02.2014

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

Утверждение: при реализации в логических элементах с использованием обычных алгоритмов схема целочисленного умножения работает в O(log N) раз медленнее, чем схема сложения, где N — количество битов в слове.

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

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

Чтобы умножить A на B, нам сначала нужно умножить каждый бит A на каждый бит B. Каждое побитовое умножение — это просто вентиль И. Необходимо выполнить N^2 побитовых умножения, следовательно, N^2 логических элементов И, но все они могут выполняться параллельно для глубины схемы, равной 1. Это решает фазу умножения школьного алгоритма, оставляя только фазу сложения.

На этапе сложения мы можем комбинировать частичные произведения, используя перевернутую двоичную древовидную схему, чтобы выполнять многие сложения параллельно. Дерево будет иметь глубину (log N) узлов, и в каждом узле мы будем складывать вместе два числа с O (N) битами. Это означает, что каждый узел может быть реализован с помощью сумматора глубины O(N), что дает общую глубину схемы O(N log N). КЭД.

person Brian Johnson    schedule 09.05.2014
comment
-1, изложенная здесь реализация уж больно наивна — это не обычный алгоритм. - person Nick Bastin; 24.07.2014
comment
en.wikipedia.org/wiki/Binary_multiplier#Implementations упоминает некоторые современные варианты реализации, такие как Например, en.wikipedia.org/wiki/Wallace_tree. И нет, вы не используете дополнение O(N) ripple-carry в современном ALU x86-64! Это будет 64 задержки полного сумматора в критическом пути для целого числа add за 1 цикл. en.wikipedia.org/wiki/Carry-lookahead_adder — это один из способов решить эту проблему. или есть также выбор переноса. Забавный факт: Pentium 4 действительно использовал 16-битные сумматоры с пульсирующим переносом на очень высокой тактовой частоте. - person Peter Cordes; 10.08.2019