寻找除以 2 的最快方法

Looking for the fastest way to divide by 2

本文关键字:方法 寻找      更新时间:2023-10-16

我搜索了半天,发现了一些非常有趣的事情,关于使用定点数据类型和位移C++来完成除法运算,同时避免浮点数学。但是,我只能理解其中的一小部分,而且我似乎无法获得任何工作。

我想做的只是取两个整数,将它们相加,然后除以 2 得到平均值。不过,我需要能够非常快速地完成此操作,因为我正在Arduino上插入相机像素数据,并且我还有其他操作要做。

所以我对一般的转变感到困惑。假设我要除以 2 的整数是 27。27 的一半是 13.5。但是无论我尝试哪种定点数据类型,我都只能得到 13 作为输出。例如:

uint8_t  x = 27;
Serial.println(  x >> 1 );

返回 13

一定有一些简单的方法可以做到这一点,对吧?

不动点确实为您提供了一种表示 13.5 的方法。维基百科关于Q数字格式的文章内容丰富:https://en.wikipedia.org/wiki/Q_(number_format)

可以这样想:你继续使用整数,但不是以面值来理解它们,而是将它们全部隐式除以 2 的幂以获得它们的语义值。

因此,如果使用无符号字节作为基类型(介于 0 和 255 之间的值,包括 0 和 255),则可以隐式除以 2**3 (8)。现在要表示 27,您需要一个设置为 27*8=>216 的整数。要除以二,您将一个向右移动;现在您的整数是 108,除以隐式分母 8 时得到 13.5,即您期望的值。

当然,你必须意识到定点数系统(以及浮点数系统,尽管它不太明显)仍然有限制;无论你做什么,某些操作都会溢出,有些操作会导致精度损失。这是使用有限大小类型的正常结果。

假设我要除以 2 的整数是 27。27 的一半是 13.5。但 无论我尝试哪种定点数据类型,我都只能得到 13 作为 输出。

来自维基百科定点算术:

比例因子通常为 10 的幂(为了人类的方便)或 2 的幂(计算效率)。

您实际上提到了定点数据类型,我认为这是最好的方法。 但不管你试过什么? 也许我们对定点算术有不同的理解。

同时避免浮点数学。

另一个有价值的目标,虽然价值降低。 即使在嵌入式系统中,我也很少需要处理没有浮点部分的处理器。 浮点硬件已经变得相当不错。

无论如何,使用定点都避免了对浮点的任何需求。 甚至用于显示目的。

我想我需要继续举几个例子。


定点示例 1:美元和便士

美国货币的单位是基于美元的。美元是一种定点数据类型。

那么,如果你有 27 美元,你如何与你的兄弟姐妹分享?

你们都知道的一种(几种)方法是将 27 美元转换为 2700 便士。将此值除以 2 是微不足道的。现在你和你的兄弟姐妹每人可以得到1350便士。 (即便士是一种固定点数据类型,很容易与美元和 vice-vesa 进行转换)

请注意,这完全是整数算术。 添加 2 个整数,然后除以 2(任何现代编译器都会选择最快的实现......要么是整数除法,要么是右移除 2),在我的桌面上,这两个操作只需不到一微秒即可完成。

您不应该再浪费时间测量这两个选项(除法与右移)的相对性能,只需在代码测试正确时启用 -O3 即可。 编译器应该能够正确选择。

任何问题中的单位选择都基于涵盖值范围(在您的问题中)的比例因子以及可理解且快速实现的单位转换。 请注意,uint64_t可以描述大量现金,即使是几分钱。 (对学生的挑战。


一般来说,关于定点:

鉴于

uint8_t  x = 27;  

以及均匀快速地除以 2 的愿望......任何比例因子都可以满足您的需求吗? 我说是的。


示例 2 - 50 美分硬币和一美元

例如,我们尝试一个简单的比例因子 2,即单位是 hu 或半单位。(类似于50美分硬币)

uint8_t  x = 27 * 1/hu;   (hu = 1/2)

这意味着 54 hu 代表 27 个单位。(即需要 54 个 50 美分硬币才能加起来 27 美元)

定点解决方案是缩放整数值以实现所需的算术。 如果缩放到偶数值,则所有整数将均匀地除以 hu 单位。


示例 3 - 尼克尔斯和一美元

另一个可能的小数位数可能是 20,包括十进制(用于可读性)和二进制(用于性能)。(注意一美元有20个镍币)

uint16  x = 27 * 1/tu;  (tu = 1/20)

现在 540 代表缩放后的 27。 即 540 尼克尔斯


所有示例都是完全整数的,提供确切的答案,并且有一个简单的机制来转换值以呈现给用户。 即使用哪个固定点,转换为便士的类似物,因此 1350 便士。

以美元显示便士计数

std::cout << (pennyCount / 100) << "." << (pennyCount % 100) << std::endl;

我认为这应该看起来像(未经测试)

13.50

现在你的挑战是让它在输出上看起来不错。

你只得到 13 的原因是因为你实际上在位移位时切断了最不重要的位。由于您要切断它们,因此无需检查剩余部分。如果你对你的剩余部分感兴趣,你可以做这样的事情:

uint8_t x = 27;
Serial.println((x - (x >> 1) - (x >> 1));

(x - (x>> 1)) 在这里应该给出 14。

一旦确定余数是否为 1,将 .5 添加到数字上将非常简单。

以下内容应该有效并且应该很快:

float y = (x >> 1) + (0.5 * (x & 0x01))

它的作用

  • (x >> 1)使用位移除以 2
  • (0.5 * (x & 0x01))如果最后一位1(奇数),则加 0.5