TL: DR с GCC для 64-битного ISA: (a * (unsigned __int128)b) >> 64
прекрасно компилируется в одну инструкцию полного умножения или умножения с высокой половиной. Не нужно возиться со встроенным asm.
К сожалению, текущие компиляторы не оптимизируют красивую портативную версию @craigster0, поэтому, если вы хотите использовать преимущества 64-битных процессоров, вы не можете использовать ее, кроме как в качестве запасного варианта. для целей, для которых у вас нет #ifdef
. (Я не вижу универсального способа его оптимизации; вам нужен 128-битный тип или встроенный.)
GNU C (gcc, clang или ICC) имеет unsigned __int128
на большинстве 64-битных платформ. (Или в более старых версиях __uint128_t
). Однако GCC не реализует этот тип на 32-битных платформах.
Это простой и эффективный способ заставить компилятор выдать 64-битную инструкцию полного умножения и сохранить старшую половину. (GCC знает, что приведение uint64_t к 128-битному целому числу по-прежнему имеет верхнюю половину, равную нулю, поэтому вы не получите 128-битное умножение с использованием трех 64-битных умножений.)
MSVC также имеет __umulh
внутреннюю функцию для 64-битного умножения старшей половины, но, опять же, он доступен только на 64-битных платформах (и, в частности, x86-64 и AArch64. В документации также упоминается IPF (IA-64), имеющий _umul128
, но у меня нет MSVC для Itanium (в любом случае, вероятно, это не актуально. )
#define HAVE_FAST_mul64 1
#ifdef __SIZEOF_INT128__ // GNU C
static inline
uint64_t mulhi64(uint64_t a, uint64_t b) {
unsigned __int128 prod = a * (unsigned __int128)b;
return prod >> 64;
}
#elif defined(_M_X64) || defined(_M_ARM64) // MSVC
// MSVC for x86-64 or AArch64
// possibly also || defined(_M_IA64) || defined(_WIN64)
// but the docs only guarantee x86-64! Don't use *just* _WIN64; it doesn't include AArch64 Android / Linux
// https://docs.microsoft.com/en-gb/cpp/intrinsics/umulh
#include <intrin.h>
#define mulhi64 __umulh
#elif defined(_M_IA64) // || defined(_M_ARM) // MSVC again
// https://docs.microsoft.com/en-gb/cpp/intrinsics/umul128
// incorrectly say that _umul128 is available for ARM
// which would be weird because there's no single insn on AArch32
#include <intrin.h>
static inline
uint64_t mulhi64(uint64_t a, uint64_t b) {
unsigned __int64 HighProduct;
(void)_umul128(a, b, &HighProduct);
return HighProduct;
}
#else
# undef HAVE_FAST_mul64
uint64_t mulhi64(uint64_t a, uint64_t b); // non-inline prototype
// or you might want to define @craigster0's version here so it can inline.
#endif
(или с clang -march=haswell
, чтобы включить BMI2: mov rdx, rsi
/ mulx rax, rcx, rdi
, чтобы напрямую поместить верхнюю половину в RAX. gcc тупой и по-прежнему использует дополнительный mov
.)
# x86-64 gcc7.3. clang and ICC are the same. (x86-64 System V calling convention)
# MSVC makes basically the same function, but with different regs for x64 __fastcall
mov rax, rsi
mul rdi # RDX:RAX = RAX * RDI
mov rax, rdx
ret
Для AArch64 (с gcc unsigned __int128
или MSVC с __umulh
):
С постоянной степенью времени компиляции умножителя 2 мы обычно получаем ожидаемый сдвиг вправо, чтобы захватить несколько старших битов. Но gcc забавно использует shld
(см. Ссылку Godbolt).
test_var:
umulh x0, x0, x1
ret
К сожалению, нынешние компиляторы не оптимизируют красивую портативную версию @craigster0. Вы получаете 8x shr r64,32
, 4x imul r64,r64
и кучу _21 _ / _ 22_ инструкций для x86-64. то есть он компилируется во множество 32x32 => 64-битных умножений и распаковок результатов. Так что, если вы хотите что-то, что использует преимущества 64-битных процессоров, вам понадобится #ifdef
s.
Инструкция полного умножения mul 64
составляет 2 мопа на процессорах Intel, но все же задержка всего 3 цикла, такая же, как imul r64,r64
, которая дает только 64-битный результат. Таким образом, __int128
/ внутренняя версия в 5-10 раз дешевле по задержкам и пропускной способности (влияние на окружающий код) на современных x86-64, чем портативная версия, судя по быстрому предположению, основанному на http://agner.org/optimize/.
Проверьте это в проводнике компилятора Godbolt по указанной выше ссылке.
Однако gcc полностью оптимизирует эту функцию при умножении на 16: вы получаете один сдвиг вправо, что более эффективно, чем при умножении unsigned __int128
.
Это проверенная мною версия, которую я придумал сегодня вечером, обеспечивает полный 128-битный продукт. При осмотре это кажется проще, чем большинство других решений в Интернете (например, в библиотеке Botan и других ответах здесь), потому что оно использует то, как СРЕДНЯЯ ЧАСТЬ не переполняется, как описано в комментариях к коду.
person
Peter Cordes
schedule
21.06.2018