Pull to refresh

Comments 17

А что произойдет, если мы случайно сложим больше, чем 2^13 чисел, произойдет наихудший сценарий и переносы переполнятся? Это же как-то контролируется?

Результат будет неверным, только и всего.

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

Алгоритм Карацубы – это про умножение. Ну и думаю, что там сложение простое: оптимизация из статьи – для суммирования кучи чисел... По сути, вынос части работы из цикла суммирования.

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

Для Python, в основных вариантах использования, это всё теряется на фоне _PyLong_New (PyObject_Malloc), поэтому, для 64-бит архитектур, CPython хранит 30-бит цифры (limb) в uint32_t/int32_t.

Ну, а насчёт, недожато, то да, тема автоматической нормализации не раскрыта, но это объективно, 256-бит числа взяты же с потолка, для примера.

В скалярном коде, возможно, потребуется ещё одно, шестое, сложение сложение и один условный переход, а возможно лучше иметь пять условных переходов с достаточно низкой вероятностью перехода, кто знает?

В векторном коде, неопределённость ещё выше.

Мы нормализуем число, начиная справа, определяя, сколько «десяток» в каждом разряде, вычитая эти «десятки» и перенося их в следующий разряд. 672415 и 736606 действительно дают в сумме 1409021, так что система работает!

Получается, проблему переноса разряда просто перенесли в другое место ( извините за тавтологию),и 17 ассемблерных инструкций вместо adc работают быстрее?

Получается, проблему переноса разряда просто перенесли в другое место ( извините за тавтологию),и 17 ассемблерных инструкций вместо adc работают быстрее?

во первых, это концепт: на большом количестве слагаемых это будет выгоднее, чем adc-цепочка из-за instruction-level parallelism, либо если использовать SIMD, то можно использовать векторизацию

во вторых, команд исполнится на ALU не 17, а 13, т.к. mov-команды здесь исполнятся блоком переименования регистров за 0 тактов: https://agner.org/optimize/microarchitecture.pdf разделы "Register allocation and renaming" различных архитектур

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

А почему тут сериализация раз в несколько тысяч сложений? В этом коде во время перенорсов вычисление каждого следующего разряда завист от предыдущего. Вот цепочка add B,T mov T,B SHR 51 T - тут зависимость по данным.

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

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

А ещё в AVX-512 есть 2 интересные инструкции: VPMADD52LUQ и VPMADD52HUQ, которые выполняют сложение с умножением над 52-битными целыми числами :)

Правильно ли я понял, что
vpaddq %xmm5, %xmm6, %xmm5 - это AVX
vpaddq %ymm4, %ymm0, %ymm0 - это AVX2

?

В данном случае да, потому что это операции над целыми числами.

Но если заменить vpaddq, например, на vaddps, то и там, и там будет AVX.

Во-первых, в большинстве популярных CPU x86 adc просто выполняется медленнее, чем обычная add. Так как у adc есть третье входящее значение (флаг переноса), эта команда сложнее, чем add

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

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

  1. На интелах до broadwell микрооперации, отправляющиеся на ALU, принимали не более чем 2 входа, поэтому adc (требующий значения двух входных регистров и значение флага переноса) декодировался в 2 микрооперации, а add - в 1.

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

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

Sign up to leave a comment.

Articles