作者:墨镜小靖 | 来源:互联网 | 2023-05-19 12:55
Iamlookingforanefficient(optionallystandard,elegantandeasytoimplement)solutiontomulti
I am looking for an efficient (optionally standard, elegant and easy to implement) solution to multiply relatively large numbers,and store the result into one or several integers :
我正在寻找一种高效的(可选的、优雅的、易于实现的)解决方案,将相对较大的数字相乘,并将结果存储为一个或几个整数:
Let say I have two 64 bits integers declared like this :
假设我有两个64位整数,像这样声明:
uint64_t a = xxx, b = yyy;
When I do a*b
, how can I detect if the operation result in an overflow and in this case store the carry somewhere ?
当我做一个*b时,我怎么能检测到操作是否导致溢出,并且在这个案例中存储的是某个地方?
Please note that I don't want to use any large-number library since I have constraints on the way I store the numbers.
请注意,我不想使用任何大型库,因为我在存储数字的方式上有限制。
9 个解决方案
19
Although there have been several other answers to this question, I several of them have code that is completely untested, and thus far no one has adequately compared the different possible options.
虽然这个问题还有其他几个答案,但我有几个人的代码是完全未经测试的,到目前为止还没有人充分地比较不同的可能选项。
For that reason, I wrote and tested several possible implementations (the last one is based on this code from OpenBSD, discussed on Reddit here). Here's the code:
出于这个原因,我编写并测试了几个可能的实现(最后一个是基于OpenBSD的代码,在Reddit上讨论过)。这是代码:
/* Multiply with overflow checking, emulating clang's builtin function
*
* __builtin_umull_overflow
*
* This code benchmarks five possible schemes for doing so.
*/
#include
#include
#include
#include
#include
#ifndef BOOL
#define BOOL int
#endif
// Option 1, check for overflow a wider type
// - Often fastest and the least code, especially on modern compilers
// - When long is a 64-bit int, requires compiler support for 128-bits
// ints (requires GCC >= 3.0 or Clang)
#if LONG_BIT > 32
typedef __uint128_t long_overflow_t ;
#else
typedef uint64_t long_overflow_t;
#endif
BOOL
umull_overflow1(unsigned long lhs, unsigned long rhs, unsigned long* result)
{
long_overflow_t prod = (long_overflow_t)lhs * (long_overflow_t)rhs;
*result = (unsigned long) prod;
return (prod >> LONG_BIT) != 0;
}
// Option 2, perform long multiplication using a smaller type
// - Sometimes the fastest (e.g., when mulitply on longs is a library
// call).
// - Performs at most three multiplies, and sometimes only performs one.
// - Highly portable code; works no matter how many bits unsigned long is
BOOL
umull_overflow2(unsigned long lhs, unsigned long rhs, unsigned long* result)
{
const unsigned long HALFSIZE_MAX = (1ul <> LONG_BIT/2;
unsigned long lhs_low = lhs & HALFSIZE_MAX;
unsigned long rhs_high = rhs >> LONG_BIT/2;
unsigned long rhs_low = rhs & HALFSIZE_MAX;
unsigned long bot_bits = lhs_low * rhs_low;
if (!(lhs_high || rhs_high)) {
*result = bot_bits;
return 0;
}
BOOL overflowed = lhs_high && rhs_high;
unsigned long mid_bits1 = lhs_low * rhs_high;
unsigned long mid_bits2 = lhs_high * rhs_low;
*result = bot_bits + ((mid_bits1+mid_bits2) <> LONG_BIT/2) != 0
|| (mid_bits2 >> LONG_BIT/2) != 0;
}
// Option 3, perform long multiplication using a smaller type (this code is
// very similar to option 2, but calculates overflow using a different but
// equivalent method).
// - Sometimes the fastest (e.g., when mulitply on longs is a library
// call; clang likes this code).
// - Performs at most three multiplies, and sometimes only performs one.
// - Highly portable code; works no matter how many bits unsigned long is
BOOL
umull_overflow3(unsigned long lhs, unsigned long rhs, unsigned long* result)
{
const unsigned long HALFSIZE_MAX = (1ul <> LONG_BIT/2;
unsigned long lhs_low = lhs & HALFSIZE_MAX;
unsigned long rhs_high = rhs >> LONG_BIT/2;
unsigned long rhs_low = rhs & HALFSIZE_MAX;
unsigned long lowbits = lhs_low * rhs_low;
if (!(lhs_high || rhs_high)) {
*result = lowbits;
return 0;
}
BOOL overflowed = lhs_high && rhs_high;
unsigned long midbits1 = lhs_low * rhs_high;
unsigned long midbits2 = lhs_high * rhs_low;
unsigned long midbits = midbits1 + midbits2;
overflowed = overflowed || midbits HALFSIZE_MAX;
unsigned long product = lowbits + (midbits < 0 && (SIZE_MAX / rhs) = MUL_NO_OVERFLOW || rhs >= MUL_NO_OVERFLOW) &&
rhs > 0 && SIZE_MAX / rhs
Here are the results, testing with various compilers and systems I have (in this case, all testing was done on OS X, but results should be similar on BSD or Linux systems):
这里是测试结果,测试使用了不同的编译器和系统(在本例中,所有测试都是在OS X上完成的,但是在BSD或Linux系统上的结果应该是类似的):
+------------------+----------+----------+----------+----------+----------+
| | Option 1 | Option 2 | Option 3 | Option 4 | Option 5 |
| | BigInt | LngMult1 | LngMult2 | Div | OptDiv |
+------------------+----------+----------+----------+----------+----------+
| Clang 3.5 i386 | 1.610 | 3.217 | 3.129 | 4.405 | 4.398 |
| GCC 4.9.0 i386 | 1.488 | 3.469 | 5.853 | 4.704 | 4.712 |
| GCC 4.2.1 i386 | 2.842 | 4.022 | 3.629 | 4.160 | 4.696 |
| GCC 4.2.1 PPC32 | 8.227 | 7.756 | 7.242 | 20.632 | 20.481 |
| GCC 3.3 PPC32 | 5.684 | 9.804 | 11.525 | 21.734 | 22.517 |
+------------------+----------+----------+----------+----------+----------+
| Clang 3.5 x86_64 | 1.584 | 2.472 | 2.449 | 9.246 | 7.280 |
| GCC 4.9 x86_64 | 1.414 | 2.623 | 4.327 | 9.047 | 7.538 |
| GCC 4.2.1 x86_64 | 2.143 | 2.618 | 2.750 | 9.510 | 7.389 |
| GCC 4.2.1 PPC64 | 13.178 | 8.994 | 8.567 | 37.504 | 29.851 |
+------------------+----------+----------+----------+----------+----------+
Based on these results, we can draw a few conclusions:
基于这些结果,我们可以得出以下结论:
- Clearly, the division-based approach, although simple and portable, is slow.
- 显然,基于分区的方法虽然简单且便于携带,但是速度很慢。
- No technique is a clear winner in all cases.
- 在所有情况下,任何技术都不是赢家。
- On modern compilers, the use-a-larger-int approach is best, if you can use it
- 在现代编译器中,如果您可以使用,则使用-大-整数方法是最好的方法。
- On older compilers, the long-multiplication approach is best
- 在旧的编译器中,长乘法的方法是最好的。
- Surprisingly, GCC 4.9.0 has performance regressions over GCC 4.2.1, and GCC 4.2.1 has performance regressions over GCC 3.3
- 令人惊讶的是,GCC 4.9.0对GCC 4.2.1有性能回归,而GCC 4.2.1有性能回归问题。
1
I've been working with this problem this days and I have to say that it has impressed me the number of times I have seen people saying the best way to know if there has been an overflow is to divide the result, thats totally inefficient and unnecessary. The point for this function is that it must be as fast as possible.
我这几天一直在研究这个问题,我不得不说它给我留下了深刻的印象,我看到人们说,最好的方法是知道是否有溢出来划分结果,这是完全没有效率的,没有必要的。这个函数的重点是它必须尽可能快。
There are two options for the overflow detection:
溢出检测有两种选择:
1º- If possible create the result variable twice as big as the multipliers, for example:
1 .如果可能的话,创建一个比乘数大两倍的结果变量,例如:
struct INT32struct {INT16 high, low;};
typedef union
{
struct INT32struct s;
INT32 ll;
} INT32union;
INT16 mulFunction(INT16 a, INT16 b)
{
INT32union result.ll = a * b; //32Bits result
if(result.s.high > 0)
Overflow();
return (result.s.low)
}
You will know inmediately if there has been an overflow, and the code is the fastest possible without writing it in machine code. Depending on the compiler this code can be improved in machine code.
如果出现溢出,您将立即知道,并且代码是最快的,而不是在机器代码中编写。根据编译器的不同,这段代码可以在机器代码中得到改进。
2º- Is impossible to create a result variable twice as big as the multipliers variable: Then you should play with if conditions to determine the best path. Continuing with the example:
2º-是不可能创建一个结果变量两倍乘数变量:那么你应该玩如果条件来决定最好的路径。继续这个示例:
INT32 mulFunction(INT32 a, INT32 b)
{
INT32union s_a.ll = abs(a);
INT32union s_b.ll = abs(b); //32Bits result
INT32union result;
if(s_a.s.hi > 0 && s_b.s.hi > 0)
{
Overflow();
}
else if (s_a.s.hi > 0)
{
INT32union res1.ll = s_a.s.hi * s_b.s.lo;
INT32union res2.ll = s_a.s.lo * s_b.s.lo;
if (res1.hi == 0)
{
result.s.lo = res1.s.lo + res2.s.hi;
if (result.s.hi == 0)
{
result.s.ll = result.s.lo <<16 + res2.s.lo;
if ((a.s.hi >> 15) ^ (b.s.hi >> 15) == 1)
{
result.s.ll = -result.s.ll;
}
return result.s.ll
}else
{
Overflow();
}
}else
{
Overflow();
}
}else if (s_b.s.hi > 0)
{
//Same code changing a with b
}else
{
return (s_a.lo * s_b.lo);
}
}
I hope this code helps you to have a quite efficient program and I hope the code is clear, if not I'll put some coments.
我希望这段代码能帮助您实现一个非常高效的程序,我希望代码是清晰的,如果不是的话,我将会添加一些内容。
best regards.
致以最亲切的问候。
0
Here is a trick for detecting whether multiplication of two unsigned integers overflows.
这里有一个技巧,用来检测两个无符号整数的乘法是否会溢出。
We make the observation that if we multiply an N-bit-wide binary number with an M-bit-wide binary number, the product does not have more than N + M bits.
我们观察到,如果我们将一个N位宽的二进制数乘以一个M-位宽的二进制数,这个乘积就不会有超过N + M位。
For instance, if we are asked to multiply a three-bit number with a twenty-nine bit number, we know that this doesn't overflow thirty-two bits.
例如,如果要求我们将一个3位的数字与一个29位的数字相乘,我们知道这不会溢出32位。
#include
#include
int might_be_mul_oflow(unsigned long a, unsigned long b)
{
if (!a || !b)
return 0;
a = a | (a >> 1) | (a >> 2) | (a >> 4) | (a >> 8) | (a >> 16) | (a >> 32);
b = b | (b >> 1) | (b >> 2) | (b >> 4) | (b >> 8) | (b >> 16) | (b >> 32);
for (;;) {
unsigned long na = a <<1;
if (na <= a)
break;
a = na;
}
return (a & b) ? 1 : 0;
}
int main(int argc, char **argv)
{
unsigned long a, b;
char *endptr;
if (argc <3) {
printf("supply two unsigned long integers in C form\n");
return EXIT_FAILURE;
}
a = strtoul(argv[1], &endptr, 0);
if (*endptr != 0) {
printf("%s is garbage\n", argv[1]);
return EXIT_FAILURE;
}
b = strtoul(argv[2], &endptr, 0);
if (*endptr != 0) {
printf("%s is garbage\n", argv[2]);
return EXIT_FAILURE;
}
if (might_be_mul_oflow(a, b))
printf("might be multiplication overflow\n");
{
unsigned long c = a * b;
printf("%lu * %lu = %lu\n", a, b, c);
if (a != 0 && c / a != b)
printf("confirmed multiplication overflow\n");
}
return 0;
}
A smattering of tests: (on 64 bit system):
少量测试:(64位系统):
$ ./uflow 0x3 0x3FFFFFFFFFFFFFFF
3 * 4611686018427387903 = 13835058055282163709
$ ./uflow 0x7 0x3FFFFFFFFFFFFFFF
might be multiplication overflow
7 * 4611686018427387903 = 13835058055282163705
confirmed multiplication overflow
$ ./uflow 0x4 0x3FFFFFFFFFFFFFFF
might be multiplication overflow
4 * 4611686018427387903 = 18446744073709551612
$ ./uflow 0x5 0x3FFFFFFFFFFFFFFF
might be multiplication overflow
5 * 4611686018427387903 = 4611686018427387899
confirmed multiplication overflow
The steps in might_be_mul_oflow
are almost certainly slower than just doing the division test, at least on mainstream processors used in desktop workstations, servers and mobile devices. On chips without good division support, it could be useful.
might_be_mul_oflow中的步骤几乎肯定比仅仅进行划分测试要慢,至少在桌面工作站、服务器和移动设备中使用的主流处理器上是如此。在没有良好部门支持的芯片上,它可能是有用的。
It occurs to me that there is another way to do this early rejection test.
我突然想到还有另外一种方法来做这个早期的拒绝测试。
We start with a pair of numbers arng
and brng
which are initialized to 0x7FFF...FFFF
and 1
.
我们从一对数字arng和brng开始,它们被初始化为0x7FFF…飞行符和1。
If a <= arng
and b <= brng
we can conclude that there is no overflow.
如果a <= arng和b <= brng,我们可以得出结论:没有溢出。
Otherwise, we shift arng
to the right, and shift brng
to the left, adding one bit to brng
, so that they are 0x3FFF...FFFF
and 3
.
否则,我们将arng移到右边,将brng移到左边,向brng中添加一点,以便它们是0x3FFF…飞行符和3。
If arng
is zero, finish; otherwise repeat at 2.
如果arng为零,完成;否则重复在2。
The function now looks like:
这个函数现在看起来是这样的:
int might_be_mul_oflow(unsigned long a, unsigned long b)
{
if (!a || !b)
return 0;
{
unsigned long arng = ULONG_MAX >> 1;
unsigned long brng = 1;
while (arng != 0) {
if (a <= arng && b <= brng)
return 0;
arng >>= 1;
brng <<= 1;
brng |= 1;
}
return 1;
}
}