使用双包装器类进行位操作(C++、clang)修复性能下降问题

Fixing performance decrease using double wrapper class for bit manipulations (C++, clang)

本文关键字:clang 性能 问题 C++ 包装 位操作      更新时间:2023-10-16

我的问题是"我是否可以编写一个既可以与内部无符号dynamic_bitset表示一起使用,也可以与内部表示一起使用的位集类型类,而不会丢失独占无符号位集类的性能?

为了提供一些上下文,我正在尝试创建一个充当位集的类,其中实现了我需要的频繁操作。此类的初稿在内部使用无符号长表示形式。

但是,在某些情况下,我需要超过 64 或 128 位,这需要使用 boost 的动态位集或无符号长整型数组作为内部表示。

现在,我的初稿与仅使用裸无符号长整型的代码一样高效(为编译器使用 -O3 标志(。我完全意识到,在使用动态位集的情况下,我无法保持这种性能。但是,我只想使用我的类编写一次算法,而不是使用无符号表示编写一个代码,使用动态位集编写一个代码。所以我创建了一个 bitsetwrapper 类,它有一个指向抽象位集的指针,该抽象位集可以是具有内部无符号长位集的位集,也可以是具有内部动态位集的位集。然后,它将指向哪个派生类由您需要使用的位数决定。

这样,我就不必担心使用指向抽象类的指针,因为这些指针被限制在我的包装器中。举个例子:

class BitsetBase{}
class UnsignedBitset : public BitsetBase{
unsigned long representation;
}
class DynamicBitsetBitset : public BitsetBase{
dynamic_bitset<> representation;
}
class BitsetWrapper{
*BitsetBase bitset;
}

现在我遇到了一些性能问题,到目前为止我还没有完全解决。

初始性能基准如下(相对比较(:

Unsinged long code : 1s
UnsingedBitset code : 1s
BitsetWrapper code (using UnsingedBitset) : 4s

为了给您一些额外的上下文,在所有 3 个实例中都制作了许多副本。这就是导致BitsetWrapper增加到4s的原因。因为在我最初的尝试中,我使用"new"来初始化 Bitset 实例。

现在,我设法通过外部初始化UnsingedBitset并将它们作为我的包装器构造函数的参数传递来完全规避new。
显著提高性能。

Unsinged long code : 1s
UnsingedBitset code : 1s
BitsetWrapper code (using UnsingedBitset) : 2.4s

然而,达到 1s 性能至关重要。我很惊讶 UnsignedBitset 版本与原始 Unsigned 长代码具有相同的性能。我的猜测是编译器可以以某种方式优化它,但不能再为"双"包装器这样做。有没有人知道为什么性能如此糟糕,以及是否有其他方法可以解决我的问题?(附言。我也尝试过 boost::variant 这也慢了 3 倍(

代码示例:

for(1000000 loops){                
AnyClass bitset(random_input) 
while(!bitset.equalsZero()){
bitset.removeLeastSignificantBit()
AnyClass bitset2 = bitset
bitset2.invert()
while(!bitset2.equalsZero()){
result += bitset2.someManipulation();
}
}
}

更清楚地提出问题。我是否可以围绕一个表示创建一个包装器,该表示可以在内部选择它应该使用的表示(基于某些参数(,而不会损失性能,如果内部表示与具有固定无符号长表示的包装器相比是无符号长。

调用的代码示例是:

void invert(){
representation = ~representation;
)

(无性能损失( 然后变成:

void invert(){
bitset_instance->invert();
}

在位集包装器中(性能损失(。

没有看到更多代码,我只能推测,但虚拟函数调用和间接寻址的成本可能超过位操作的成本。如果每个位翻转都是通过虚函数调用完成的,这可能尤其可想而知。这是最有可能的,因为您消除了分配作为额外的性能开销 - 但同样,为了确定,您需要更多地了解您的使用模式。

您真的需要灵活性吗?如果要删除抽象,有一些可能性:

  1. 一直使用dynamic_bitset
  2. 使用其他实现,例如std::vector<bool>(很可能性能不超过 1(
  3. 使用不同的调度机制。如果您在编译时知道这些情况,模板是一个显而易见的选择。其他运行时机制(if/else,switch(最有可能产生与虚函数相同的性能。

附言不确定是否只是为了简洁而省略它,但多态基类应该有一个虚拟析构函数。这甚至是很好的做法,如果你在堆栈上分配它们并只传递指针。