Подтвердить что ты не робот

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

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

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

#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

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

4b9b3361

Ответ 1

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

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

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

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

Ответ 2

Нет, это не такая же скорость. Кто вам сказал?

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

Ответ 3

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

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

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

Кроме того: следует отметить, что процессор МОЖЕТ быть построен, для которого ВСЕ операции, включая умножение, занимают один такт. Этот процессор должен был бы избавиться от всей конвейерной работы и замедлить тактовую частоту, чтобы задержка HW в любой схеме OP была меньше или равна задержке, предоставляемой тактовой синхронизацией.

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

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

| ------------------------------------------------- - | Non-Конвейерный

| --Step 1-- | --Step 2-- | --Step 3-- | --Step 4-- | --Step 5-- | конвейерный

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

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

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

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

  1. Каждая архитектура отличается с точки зрения HW более низкого уровня, а также с точки зрения системы
  2. ФУНКЦИОНАЛЬНО, умножение всегда будет занимать больше времени, чем сложение, потому что оно объединяет истинное умножение и шаг сложения.
  3. Понимайте архитектуру, на которой вы пытаетесь запустить свой код, и находите правильный баланс между удобочитаемостью и получением действительно лучшей производительности от этой архитектуры.

Ответ 4

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

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

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

Ответ 5

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

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

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

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

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

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

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

Ответ 6

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

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

Ответ 7

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

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

Ответ 8

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

ПРЕДЛОЖЕНИЕ: Когда реализовано в логических строках, используя обычные алгоритмы, целая схема умножения равна O (log N) раз медленнее, чем схема сложения, где N - количество бит в слове.

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

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

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

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