对于乘法,只有广为人知的语言(高于汇编语言)中的 Forth 具有 N*N 位到 2N 位结果的显式乘法(单词M*、UM*)。 C、Fortran 等没有。是的,这有时会导致错误优化。例如,在 x86_32 上,获得 64 位产品需要将数字转换为 64 位数字(可能导致库调用而不是 mul 命令),或显式内联汇编调用(在 gcc 和克隆中简单高效,但并不总是在 MSVC 和其他编译器中)。
在我对 x86_32 (i386) 的测试中,现代编译器能够像这样转换代码
#include <stdint.h>
int64_t mm(int32_t x, int32_t y) {
return (int64_t) x * y;
}
到没有库调用的简单“imull”指令; clang 3.4(-O1 或更高版本)和 gcc 4.8(-O2 或更高版本)满足了这一点,我想这永远不会停止。 (使用较低的优化级别,添加了第二个无用的乘法。)但是如果没有真正的测试,对于任何其他编译器都不能保证这一点。在 x86 上使用 gcc,即使没有优化,以下内容也可以工作:
int64_t mm(int32_t x, int32_t y) {
int64_t r;
asm("imull %[s]" : "=A" (r): "a" (x), [s] "bcdSD" (y): "cc");
return r;
}
几乎所有现代 CPU 都有相同的趋势和类似的命令。
对于除法(如 64 位除数除以 32 位除数到 32 位商和余数),这更复杂。有像 'lldiv' 这样的库函数,但它们仅用于有符号除法;没有无符号的等价物。此外,它们是具有所有相应成本的库调用。但是,这里的问题是,许多现代建筑没有这种划分。例如,它被明确排除在 ARM64 和 RISC-V 之外。对于他们来说,必须使用较短的除法来模拟长除法(例如,将 2**(N-1) 除以被除数,然后将结果加倍并调整余数)。对于具有混合长度划分(x86、M68k、S/390 等)的那些,如果您确定它不会溢出,则单行汇编内联是相当不错的 :)
某些架构根本不支持分区(旧 Sparc、Alpha),这是支持此类操作的标准库任务。
无论如何,标准库提供所有需要的操作,除非您需要最高精度(例如 x86_64 可以将 128 位被除数除以 64 位除数,但 C 库不支持这一点)。
我认为这些方法针对不同架构的最详尽和易于理解的示例是GMP library。它比您的问题要先进得多,但是您可以为不同的架构挖掘单肢除法的示例,即使架构不直接支持它,它也可以实现正确的链接。尽管有一些开销,但它也足以满足任意长数算术的大多数需求。
注意,如果您明确调用div-like 指令,则检查溢出是您的责任。签名的情况比未签名的情况更棘手;例如,将 -2147483648 除以 -1 会使基于 x86 的程序崩溃,即使是用 C 编写的也是如此。
更新[2020-07-04]:使用 GCC Integer overflow builtins,可以使用混合精度的乘法,例如:
#include <stdint.h>
int64_t mm(int32_t x, int32_t y) {
int64_t result;
__builtin_mul_overflow(x, y, &result);
return result;
}
在大多数情况下,GCC 和 Clang 都会将其翻译为最佳形式。我希望其他编译器甚至标准最终会采用这一点。