【问题标题】:Higher part of multiply and division in C or C++?C或C++中乘法和除法的较高部分?
【发布时间】:2020-07-04 19:43:59
【问题描述】:

当我在汇编中将一对 4 字节整数相乘时,结果的下半部分在 EAX 中,而上半部分在 EDX 中。如果我在 C 或 C++ 中并且我想获得更高的部分,是否可以不使用内联汇编?

是否可以以同样的方式从 EAX 获得整数除法结果和从 EDX 获得模数结果,而无需在 C 或 C++ 中重复除法?我实际上只知道先做a/b 然后a%b,而在汇编程序中,两个结果都在同一个操作中给出。

【问题讨论】:

  • 我反对这个前提。在我的 DS9k 机器上,乘法的输出被写入与时间相关的 157 位 I/O 端口。
  • 回答您的第二个问题(希望如此):stackoverflow.com/questions/34008997/… TL;DR 是,如果您只执行a/ba%b,它们最终可能会优化为一条除法指令,否则你可以试试std::div
  • 提示 #2:div.
  • @KerrekSB:以 x86 寄存器为例。来自我的链接:“在许多平台上,单个 CPU 指令同时获得商和余数,这个函数可以利用这一点,尽管编译器通常能够在合适的地方合并附近的 / 和 %。”
  • @Jongware:是的,答案当然是使用div,但这与任何类型的寄存器无关,也不需要对寄存器做任何假设。

标签: c++ c


【解决方案1】:

您可以通过这种方式在 C 中轻松做到这一点:

#include <stdint.h>

uint32_t a, b;  // input
uint64_t val = (uint64_t)a * b;
uint32_t high = val >> 32, low = val;

让编译器生成尽可能好的代码。现代优化器真的很擅长。手工编码的程序集通常看起来更好,但性能更差。

正如 Pete Becker 所说,上述内容依赖于 uint32_tuint64_t 类型的可用性。如果你坚持绝对的可移植性(比如你在 DS9K 上编程),你可以改用类型 uint_least32_tuint_least64_tuint_fast32_tuint_fast64_t 在 C99 下总是可用的,但你需要一个额外的掩码,如果不需要,将被优化:

#include <stdint.h>

uint_fast32_t a, b;  // input
uint_fast64_t val = (uint_fast64_t)a * b;
uint_fast32_t high = (val >> 32) & 0xFFFFFFFF, low = val & 0xFFFFFFFF;

关于除法,您可以使用 C99 库函数 divldivlldiv 在一次调用中执行有符号除法和余数运算。如果可能的话,除法/模数组合将在特定操作数类型的目标架构上实现。

编写两个表达式并依靠编译器检测模式并生成使用单个 IDIV 操作码的代码可能更有效:

struct divmod_t { int quo, rem; };
struct divmod_t divmod(int num, int denom) {
    struct divmod_t r = { num / denom, num % denom };
    return r;
}

Matt Godbolt's compiler explorer 的测试显示clang 和gcc 都在-O3 为该代码生成了一条idiv 指令。

您可以将这些除法之一变成乘法:

struct divmod_t { int quo, rem; };
struct divmod_t divmod2(int num, int denom) {
    struct divmod_t r;
    r.quo = num / denom;
    r.rem = num - r.quo * denom;
    return r;
}

请注意,上述函数不会检查潜在的溢出,这会导致未定义的行为。如果denom = 0num = INT_MINdenom = -1 发生溢出。

【讨论】:

  • 比 C++ 简单一点 ;-)
  • 希望有人可以在 C++ 中做到这一点 - 为什么 C++ 必须如此复杂?!! :P
  • @PaulEvans 因为你应该在之后使用它已经改进的语言(即++C),而不是之前;)
  • 这也是有效的 C++11
  • @PeteBecker:你是对的,这个答案是假设的。然而,需要一些非常奇异的硬件才能没有这样的类型:C99 指定 long long 至少有 64 位。
【解决方案2】:

您无需处理 C 或 C++ 中的实现细节。这就是重点。如果您想要最重要的字节,只需使用该语言。右移&gt;&gt; 就是为此而设计的。比如:

uint64_t i;
uint32_t a;
uint32_t b;
// input a, b and set i to a * b
// this should be done with (thanks to @nnn, pls see comment below):
// i = a; i *= b;
uint64_t msb = i >> 32;

【讨论】:

  • 为了避免任何不确定性,需要注意的是,i = a * b; 是不行的,因为只会存储 32 个 LSBit。任何一个操作数都应该转换为uint64_t,或者类似i = a; i *= b;
【解决方案3】:

对于乘法,只有广为人知的语言(高于汇编语言)中的 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 都会将其翻译为最佳形式。我希望其他编译器甚至标准最终会采用这一点。

【讨论】:

  • 这个比较棘手:-2147483648 / -1 可以,因为2147483648 不是 32 位整数常量。它可以是intlonglong long,具体取决于具有超过 31 个值位的类型。 long long 保证至少有 63 个值位。要展示问题,请写INT32_MAX / -1(int32_t)-2147483648 / -1。您还可以使用具有适当值的int32_t 变量。
  • @chqrlie 是的,编译器试图在编译时进行尽可能多的计算。可靠地禁用此类优化的最简单方法是使用 volatile 变量。
【解决方案4】:

对于除法,完全可移植的解决方案使用库函数 divldivlldiv 之一。

【讨论】:

  • 这些库例程仅支持有符号除法。原始海报未指定所需操作的签名。
猜你喜欢
  • 2011-09-02
  • 2014-07-14
  • 1970-01-01
  • 2023-02-24
  • 2011-07-21
  • 1970-01-01
  • 2020-11-28
  • 2016-07-31
  • 1970-01-01
相关资源
最近更新 更多