Получение высокой части 64-битного целочисленного умножения
В С++ скажите, что:
uint64_t i;
uint64_t j;
то i * j
даст uint64_t
, который имеет в качестве значения нижнюю часть умножения между i
и j
, т.е. (i * j) mod 2^64
.
Теперь, что, если бы я хотел высшую часть умножения? Я знаю, что существует инструкция по сборке, чтобы сделать что-то подобное при использовании 32-битных целых чисел, но я вообще не знаком с сборкой, поэтому я надеялся на помощь.
Каков наиболее эффективный способ сделать что-то вроде:
uint64_t k = mulhi(i, j);
Ответы
Ответ 1
Если вы используете gcc и версию, поддерживающую 128-битные номера (попробуйте использовать __uint128_t), чем выполнение умножения 128 и извлечение верхних 64 бит, вероятно, будет наиболее эффективным способом получения результата.
Если ваш компилятор не поддерживает 128-битные номера, тогда ответ Якка правильный. Однако это может быть слишком кратким для общего потребления. В частности, фактическая реализация должна быть осторожна с переполнением 64-битных целых чисел.
Простым и портативным решением, которое он предлагает, является разбиение каждого из a и b на 2 32-битных числа, а затем умножение этих 32-разрядных чисел с использованием операции умножения на 64 бит. Если мы напишем:
uint64_t a_lo = (uint32_t)a;
uint64_t a_hi = a >> 32;
uint64_t b_lo = (uint32_t)b;
uint64_t b_hi = b >> 32;
то очевидно, что:
a = (a_hi << 32) + a_lo;
b = (b_hi << 32) + b_lo;
и
a * b = ((a_hi << 32) + a_lo) * ((b_hi << 32) + b_lo)
= ((a_hi * b_hi) << 64) +
((a_hi * b_lo) << 32) +
((b_hi * a_lo) << 32) +
a_lo * b_lo
если вычисление выполняется с использованием 128-разрядной (или более) арифметической.
Но эта проблема требует, чтобы мы выполняли все вычисления с использованием 64-разрядной арифметики, поэтому нам нужно беспокоиться о переполнении.
Так как a_hi, a_lo, b_hi и b_lo - все 32-разрядные номера без знака, их продукт будет соответствовать беззнаковому 64-битовому номеру без переполнения. Однако промежуточные результаты вышеуказанного расчета не будут.
Следующий код реализует mulhi (a, b), когда математика должна выполняться по модулю 2 ^ 64:
uint64_t a_lo = (uint32_t)a;
uint64_t a_hi = a >> 32;
uint64_t b_lo = (uint32_t)b;
uint64_t b_hi = b >> 32;
uint64_t a_x_b_hi = a_hi * b_hi;
uint64_t a_x_b_mid = a_hi * b_lo;
uint64_t b_x_a_mid = b_hi * a_lo;
uint64_t a_x_b_lo = a_lo * b_lo;
uint64_t carry_bit = ((uint64_t)(uint32_t)a_x_b_mid +
(uint64_t)(uint32_t)b_x_a_mid +
(a_x_b_lo >> 32) ) >> 32;
uint64_t multhi = a_x_b_hi +
(a_x_b_mid >> 32) + (b_x_a_mid >> 32) +
carry_bit;
return multhi;
Как указывает Якк, если вы не возражаете против того, чтобы +1 в верхних 64 битах, вы можете опустить вычисление бит переноса.
Ответ 2
Это версия с проверкой, которую я придумал сегодня вечером, которая обеспечивает полный 128-битный продукт. При проверке он кажется проще, чем большинство других решений в Интернете (например, в библиотеке Botan и других ответах здесь), потому что он использует преимущества того, как MIDDLE PART не переполняется, как объясняется в комментариях кода.
Для контекста я написал его для этого проекта github: https://github.com/catid/fp61
//------------------------------------------------------------------------------
// Portability Macros
// Compiler-specific force inline keyword
#ifdef _MSC_VER
# define FP61_FORCE_INLINE inline __forceinline
#else
# define FP61_FORCE_INLINE inline __attribute__((always_inline))
#endif
//------------------------------------------------------------------------------
// Portable 64x64->128 Multiply
// CAT_MUL128: r{hi,lo} = x * y
// Returns low part of product, and high part is set in r_hi
FP61_FORCE_INLINE uint64_t Emulate64x64to128(
uint64_t& r_hi,
const uint64_t x,
const uint64_t y)
{
const uint64_t x0 = (uint32_t)x, x1 = x >> 32;
const uint64_t y0 = (uint32_t)y, y1 = y >> 32;
const uint64_t p11 = x1 * y1, p01 = x0 * y1;
const uint64_t p10 = x1 * y0, p00 = x0 * y0;
/*
This is implementing schoolbook multiplication:
x1 x0
X y1 y0
-------------
00 LOW PART
-------------
00
10 10 MIDDLE PART
+ 01
-------------
01
+ 11 11 HIGH PART
-------------
*/
// 64-bit product + two 32-bit values
const uint64_t middle = p10 + (p00 >> 32) + (uint32_t)p01;
/*
Proof that 64-bit products can accumulate two more 32-bit values
without overflowing:
Max 32-bit value is 2^32 - 1.
PSum = (2^32-1) * (2^32-1) + (2^32-1) + (2^32-1)
= 2^64 - 2^32 - 2^32 + 1 + 2^32 - 1 + 2^32 - 1
= 2^64 - 1
Therefore it cannot overflow regardless of input.
*/
// 64-bit product + two 32-bit values
r_hi = p11 + (middle >> 32) + (p01 >> 32);
// Add LOW PART and lower half of MIDDLE PART
return (middle << 32) | (uint32_t)p00;
}
#if defined(_MSC_VER) && defined(_WIN64)
// Visual Studio 64-bit
# include <intrin.h>
# pragma intrinsic(_umul128)
# define CAT_MUL128(r_hi, r_lo, x, y) \
r_lo = _umul128(x, y, &(r_hi));
#elif defined(__SIZEOF_INT128__)
// Compiler supporting 128-bit values (GCC/Clang)
# define CAT_MUL128(r_hi, r_lo, x, y) \
{ \
unsigned __int128 w = (unsigned __int128)x * y; \
r_lo = (uint64_t)w; \
r_hi = (uint64_t)(w >> 64); \
}
#else
// Emulate 64x64->128-bit multiply with 64x64->64 operations
# define CAT_MUL128(r_hi, r_lo, x, y) \
r_lo = Emulate64x64to128(r_hi, x, y);
#endif // End CAT_MUL128
Ответ 3
Длительное умножение должно соответствовать производительности.
Разделить a*b
на (hia+loa)*(hib+lob)
. Это дает 4 32-битных умножения плюс некоторые сдвиги. Делайте их в 64 бит и выполняйте перенос вручную, и вы получите большую часть.
Обратите внимание, что аппроксимация высокой части может быть выполнена с меньшим количеством умножений - точная в пределах 2 ^ 33 или около того с 1 умножением и внутри 1 с 3 умножениями.
Я не думаю, что есть переносная альтернатива.
Ответ 4
TL: DR с GCC для 64-битного ISA: (a * (unsigned __int128)b) >> 64
прекрасно компилируется в одну инструкцию полного умножения или умножения на половину. Нет необходимости возиться с встроенным ассемблером.
К сожалению, текущие компиляторы не оптимизируют @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 does not 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 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 version here so it can inline.
#endif
Для x86-64, AArch64 и PowerPC64 (и других) это компилируется в одну инструкцию mul
и пару mov
для работы с соглашением о вызовах (которое следует оптимизировать после этих строк) ,
Из проводника компилятора Godbolt (с источником + asm для x86-64, PowerPC64 и AArch64):
# 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
(или с помощью clang -march=haswell
для включения BMI2: mov rdx, rsi
/mulx rax, rcx, rdi
для непосредственного помещения верхней половины в RAX. gcc тупой и все еще использует дополнительный mov
.)
Для AArch64 (с gcc unsigned __int128
или MSVC с __umulh
):
test_var:
umulh x0, x0, x1
ret
С постоянной мощностью 2 во время компиляции мы обычно получаем ожидаемое смещение вправо, чтобы получить несколько старших бит. Но gcc забавно использует shld
(см. ссылку на Godbolt).
К сожалению, современные компиляторы не оптимизируют @craigster0 красивую портативную версию. Вы получаете 8x shr r64,32
, 4x imul r64,r64
и кучу инструкций add
/mov
для x86-64. то есть он компилируется во множество 32х32 => 64-битных умножений и распаковывает результаты. Поэтому, если вы хотите что-то, что использует преимущества 64-битных процессоров, вам нужно несколько #ifdef
.
Команда полного умножения mul 64
- это 2 мопа на процессорах Intel, но с задержкой всего в 3 цикла, как и в imul r64,r64
, который дает только 64-битный результат. Таким образом, версия __int128
/встроенная в 5–10 раз дешевле по задержке и пропускной способности (влияние на окружающий код) в современной x86-64, чем в переносной версии, благодаря быстрому предположению, основанному на http://agner.org/optimize/.
Проверьте это в проводнике компилятора Godbolt по приведенной выше ссылке.
Однако gcc полностью оптимизирует эту функцию при умножении на 16: вы получаете один сдвиг вправо, более эффективный, чем при умножении unsigned __int128
.
Ответ 5
Вот ассм для версии ARMv8 или Aarch64:
// High (p1) and low (p0) product
uint64_t p0, p1;
// multiplicand and multiplier
uint64_t a = ..., b = ...;
p0 = a*b; asm ("umulh %0,%1,%2" : "=r"(p1) : "r"(a), "r"(b));
А вот асм для старых DEC-компиляторов:
p0 = a*b; p1 = asm("umulh %a0, %a1, %v0", a, b);
Если у вас x86 BMI2 и вы хотите использовать mulxq
:
asm ("mulxq %3, %0, %1" : "=r"(p0), "=r"(p1) : "d"(a), "r"(b));
И общий x86 умножить, используя mulq
:
asm ("mulq %3" : "=a"(p0), "=d"(p1) : "a"(a), "g"(b) : "cc");