将多个 int 值存储到一个变量中 - C++
Posted
技术标签:
【中文标题】将多个 int 值存储到一个变量中 - C++【英文标题】:Storing multiple int values into one variable - C++ 【发布时间】:2021-02-28 14:06:56 【问题描述】:我正在参加算法竞赛,并且正在尝试优化我的代码。也许我想做的是愚蠢和不可能的,但我想知道。
我有这些要求:
可以包含 4 种不同类型的物品的库存。此库存不能包含超过 10 件商品(包括所有类型)。有效库存示例:1 / 1 / 1 / 0。无效库存示例:11 / 0 / 0 / 0 或 5 / 5 / 5 / 0 我有一些收据可以消耗或添加物品到我的库存中。这 recepe 不能添加或消费超过 10 个项目,因为库存 不能超过 10 个项目。有效收据示例:-1 / -2 / 3 / 0. 无效recepe示例:-6 / -6 / +12 / 0现在,我将库存和收据存储为 4 个整数。然后我可以执行一些操作,例如:
ApplyRecepe: Inventory(1/1/1/0).Apply(Recepe(-1/1/0/0)) = Inventory(0/2/1/0) CanAfford: Iinventory(1/1/0/0).CanAfford(Recepe(-2/1/0/0)) = False我想知道是否可以(如果可以,如何)将库存/收据的 4 个值存储到一个整数中,并对其执行比比较/添加 4 个更快的操作我现在正在做的整数。
我想到了类似这样的库存:
int32: XXXX(第一类项数)-YYYY(第二类项数)-ZZZ(第三类项数)-WWW(第四类项数)
但我有两个问题:
-
我不知道如何处理可能的负值
在我看来,这比仅添加 4 个整数要慢得多,因为我必须对库存和收据进行移位以获得我想要的值,然后继续添加。
【问题讨论】:
像这样的位打包很少会产生更快的代码。为什么不只是四个独立的int8_t
s?
您正在寻找 矢量化 又名 array programming.
这被称为SIMD,尽管您的用例似乎太简单而无法真正利用它。
bitpacking 主要是为了存储,一旦你要对它进行计算,你大多需要解压它。
仅供参考:我在SO: Atomic operations - C 中提出了类似的想法,但出于不同的原因:使多个计数器的修改成为原子。
【参考方案1】:
特别是如果您正在学习,那么尝试为vectorization 实现您自己的帮助器类并不是一个糟糕的机会,从而加深您对 C++ 中数据的理解,即使您的用例可能不支持该技术。
您想要利用的洞察力是算术运算似乎对位移是不变的,如果考虑到讨厌的进位和标志的影响(例如二进制补码)。但正是由于后面的这些因素,正如@Botje 所建议的那样,使用一些标准化的底层类型(如int8_t[]
)要好得多。
首先,实现以下功能。 (我的 C++ 生锈了,考虑一下这个伪代码。)
int8_t* add(int8_t[], int8_t[], size_t);
int8_t* multiply(int8_t[], int8_t[], size_t);
int8_t* zeroes(size_t); // additive identity
int8_t* ones(size_t); // multiplicative identity
同时考虑:
您希望如何处理上溢和下溢?让他们成为并要求开发人员谨慎吗?还是抛出异常? 也许您想确定数组的大小并避免处理动态 size_t? 也许您想要重载运算符?这样的练习的最终结果,但概括和完善,类似于Armadillo。但是,通过首先自己进行练习,您会在完全不同的层面上理解它。此外,如果到目前为止所有这些都有意义,您现在可以查看 How to vectorize my loop with g++? — 在某些情况下,甚至编译器也可以为您进行矢量化。
@Botje 提到的 Bitpacking 是除此之外的另一个步骤。您甚至不会拥有像 int8_t 或 int4_t 这样的整数类型的安全性和便利性。这也意味着您编写的代码可能不再独立于平台。我建议在深入研究之前至少完成矢量化练习。
【讨论】:
【参考方案2】:将多个 int 值存储到一个变量中
这里有两种选择:
一个数组。这样做的好处是您可以迭代元素:
int variable[]
1,
1,
1,
0,
;
或者一堂课。这样做的好处是可以命名成员:
struct
int X;
int Y;
int Z;
int W;
variable
1,
1,
1,
0,
;
然后我可以执行一些操作,例如:
那些看起来像 SIMD 向量操作(单指令多数据)。在这种情况下,数组是要走的路。由于在您的描述中操作数的数量似乎是恒定的并且很小,因此执行它们的有效方法是在 CPU 1 上进行向量操作。
在 C++ 中没有直接使用 SIMD 操作的标准方法。为了让编译器有最佳机会使用它们,需要遵循以下步骤:
确保您使用的 CPU 支持您需要的操作。 AVX-2 指令集及其扩展广泛支持整数向量运算。 确保告诉编译器程序应该针对该架构进行优化。 确保告诉编译器执行矢量化优化。 确保整数按照操作要求充分对齐。这可以通过alignas
来实现。
确保在编译时知道整数个数。
如果您担心依赖优化器的前景,那么您可能更愿意使用编译器可能提供的向量扩展。语言扩展的使用自然会以移植到其他编译器为代价。以下是 GCC 的示例:
constexpr int count = 4;
using v4si = int __attribute__ ((vector_size (sizeof(int) * count)));
#include <iostream>
int main()
v4si inventory 1, 1, 1, 0;
v4si recepe -1, 1, 0, 0;
v4si applied = inventory + recepe;
for (int i = 0; i < count; i++)
std::cout << applied[i] << ", ";
1如果操作数的数量很大,那么专用的矢量处理器(例如 GPU)可能会更快。
【讨论】:
【参考方案3】:这将是一个非答案,只是为了说明如果你进行 bitpacking 会遇到什么问题。
为简单起见,假设配方只能从库存中移除,并且只包含正值(您可以使用二进制补码表示负数,但它会占用更多位,并且会增加使用位压缩的复杂性数字)。
然后,您有 11 个可能的项目值,因此每个项目需要 4 位。然后可以在一个 uint16 中表示四个项目。
因此,假设您的库存有 10、4、6、9 件物品;这将是uint16_t inv = 0b1010'0100'0110'1001
。
然后,一个包含 2,2,2,2 项的食谱或uint16_t rec = 0b0010'0010'0010'0010
。
inv - rec 将为 8,2,4,7 项提供 0b1000'0010'0100'0111
。
到目前为止,一切都很好。在进行计算之前,无需在此处移动和屏蔽以获取各个值。耶。
现在,一个包含 6,6,6,6 项的配方,即 0b0110'0110'0110'0110
,给出 inv - rec = 0b0011'1110'0000'0011
3,14,0,3 项。
糟糕。
算术将起作用,但仅如果您事先检查单个 4 位结果没有超出范围;在此示例中,这意味着您事先知道库存中有足够的物品来填充配方。
例如,您可以通过以下方式获得库存中的第三项:(inv >> 4) & 0b1111
或 (inv << 8) >> 12
进行检查。
为了测试,你会得到如下表达式:
if ((inv >> 4) & 0b1111 >= (rec >> 4) & 0b1111)
或者,比较“就地”的 4 位:
if (inv & 0b0000000011110000 >= rec & 0b0000000011110000)
每个 4 位部分。
所有这些事情都是可行的,但你想要吗?在编译器完成其工作后,它可能不会比其他答案中建议的更快,而且它肯定不会更具可读性。
当你在配方中允许负数(二进制补码或其他)时,它变得更加可怕,特别是如果你想对它们进行位移。
因此,位打包非常适合存储,在极少数情况下,您甚至可以在不解包位的情况下进行数学运算,但我不会尝试去那里(除非您的性能和内存非常受限)。
话虽如此,尝试让它工作可能会很有趣;总是这样。
【讨论】:
"0b 1010 0100 0110 1001 (spaces for clarity).
",C++14 允许在文字中使用引号:0b1010'0100'0110'1001
。以上是关于将多个 int 值存储到一个变量中 - C++的主要内容,如果未能解决你的问题,请参考以下文章