C++ 优化 if/else 条件
C++ Optimize if/else condition
我有一行代码,占用了应用程序运行时的 25% - 30%。它是 std::set 的小于比较器(该集合是用红-黑树实现的)。 它在28秒内被调用约1.8亿次。
struct Entry {
const float _cost;
const long _id;
// some other vars
Entry(float cost, float id) : _cost(cost), _id(id) {
}
};
template<class T>
struct lt_entry: public binary_function <T, T, bool>
{
bool operator()(const T &l, const T &r) const
{
// Most readable shape
if(l._cost != r._cost) {
return r._cost < l._cost;
} else {
return l._id < r._id;
}
}
};
条目应按成本排序,如果成本相同,则应按其 ID 排序。 对于最小值的每个提取,我都有很多插入。我想过使用斐波那契堆,但我被告知它们在理论上很好,但常数很高,实现起来非常复杂。由于插入在 O(log(n)) 中,因此对于较大的 n 来说,运行时间的增加几乎是恒定的。所以我认为坚持片场是可以的。
为了提高性能,我尝试用不同的形状来表达它:
return l._cost < r._cost || r._cost > l._cost || l._id < r._id;
return l._cost < r._cost || (l._cost == r._cost && l._id < r._id);
即使这样:
typedef union {
float _f;
int _i;
} flint;
//...
flint diff;
diff._f = (l._cost - r._cost);
return (diff._i && diff._i >> 31) || l._id < r._id;
但是编译器似乎已经足够聪明了,因为我无法改进运行时。
我也想到了SSE,但这个问题真的不太适用于SSE...
程序集看起来有点像这样:
movss (%rbx),%xmm1
mov $0x1,%r8d
movss 0x20(%rdx),%xmm0
ucomiss %xmm1,%xmm0
ja 0x410600 <_ZNSt8_Rb_tree[..]+96>
ucomiss %xmm0,%xmm1
jp 0x4105fd <_ZNSt8_Rb_[..]_+93>
jne 0x4105fd <_ZNSt8_Rb_[..]_+93>
mov 0x28(%rdx),%rax
cmp %rax,0x8(%rbx)
jb 0x410600 <_ZNSt8_Rb_[..]_+96>
xor %r8d,%r8d
我对汇编语言有一点点经验,但不是很多。
我认为挤出一些性能是最好的(唯一?)点,但这真的值得付出努力吗?你能看到任何可以节省一些周期的快捷方式吗?
代码将运行的平台是 ubuntu 12,在众核英特尔机器上装有 gcc 4.6 (-stl=c++0x)。只有可用的库是boost,openmp和tbb。30 秒基准测试是在我 4 年的旧笔记本电脑(酷睿 2 二重奏)上执行的。
我真的被困在这个上面,它看起来很简单,但需要那么多时间。几天来我一直在嘎吱嘎吱地想着如何改善这条线......
你能给我一个建议如何改进这部分,还是已经处于最佳状态?
编辑1:使用Jerrys的建议后,我实现了~4.5秒的速度。 编辑 2:在尝试提升斐波那契堆后,比较了 174 次 Mio 调用小于函数。
让我先说一下,我将在这里概述的内容是脆弱的,并非完全可移植 - 但在适当的情况下(这几乎是你指定的),我有理由确定它应该正常工作。
它所依赖的一点是,IEEE浮点数经过精心设计,因此,如果您将它们的位模式视为整数,它们仍然会按正确的顺序排序(模化一些东西,例如NaN,实际上没有"正确的顺序")。
为了利用这一点,我们要做的是打包条目,这样构成我们密钥的两个部分之间就没有填充。然后,我们确保整个结构与 8 字节边界对齐。我还将_id
更改为int32_t
,以确保它保持 32 位,即使在 64 位系统/编译器上也是如此(这几乎肯定会为这种比较生成最佳代码)。
然后,我们强制转换结构的地址,以便我们可以将浮点数和整数一起作为单个 64 位整数查看。由于您使用的是小端序处理器,为了支持这一点,我们需要将不太重要的部分(id
)放在第一位,将更重要的部分(cost
)放在第二位,因此当我们将它们视为 64 位整数时,浮点部分将成为最高有效位,整数部分将成为不太有效的位:
struct __attribute__ ((__packed__)) __attribute__((aligned(8)) Entry {
// Do *not* reorder the following two fields or comparison will break.
const int32_t _id;
const float _cost;
// some other vars
Entry(long id, float cost) : _cost(cost), _id(id) {}
};
然后我们有了丑陋的小比较函数:
bool operator<(Entry const &a, Entry const &b) {
return *(int64_t const *)&a < *(int64_t const *)&b;
}
一旦我们正确定义了结构,比较就变得相当简单:只需取每个结构的前 64 位,并将它们作为 64 位整数进行比较。
最后,一些测试代码至少可以保证它在某些值下可以正常工作:
int main() {
Entry a(1236, 1.234f), b(1234, 1.235f), c(1235, 1.235f);
std::cout << std::boolalpha;
std::cout << (b<a) << "n";
std::cout << (a<b) << "n";
std::cout << (b<c) << "n";
std::cout << (c<b) << "n";
return 0;
}
至少对我来说,这会产生预期的结果:
false
true
true
false
现在,一些可能的问题:如果这两个项目在它们之间重新排列,或者结构的任何其他部分放在它们之前或之间,比较肯定会中断。其次,我们完全依赖于剩余的 32 位项目的大小,因此当它们连接起来时,它们将是 64 位。第三,如果有人从结构定义中删除__packed__
属性,我们最终可能会在_id
和_cost
之间填充,再次破坏比较。同样,如果有人删除了aligned(8),代码可能会失去一些速度,因为它试图加载与8字节边界不一致的8字节数量(在另一个处理器上,这可能会完全失败)。[编辑:哎呀。 @rici让我想起了我打算在这里列出的东西,但忘记了:这只有在_id
和cost
都是正数时才正常工作。如果_cost
为负数,则比较将被IEEE浮点使用有符号幅度表示的事实所打乱。如果_id
为负数,则其符号位将被视为数字中间的普通位,因此负_id
将显示为大于正_id
。
总而言之:这是脆弱的。对此完全没有疑问。尽管如此,它应该非常快 - 特别是如果你使用的是64位编译器,在这种情况下,我希望比较结果为两个加载和一个比较。长话短说,您可能根本无法使比较本身更快 - 您所能做的就是尝试并行执行更多操作,优化内存使用模式等。
一个简单的解决方案是预先计算一个排序标识符,该标识符由最重要的成本和其余的 id 组成。
例如,
struct Entry
{
double cost_;
long id_;
long long sortingId_;
// some other vars
Entry( double cost, float id )
: cost_( cost ), id_( id ), sortingId_( 1e9*100*cost + id )
{}
};
根据您可以假设的值范围调整sortingId_
值。
然后,现在只需对sortingId_
进行排序.
或者作为相同想法的变体,如果您无法对数据做出合适的假设,请考虑专门为memcmp
准备数据。
对于更高级别的解决方案,请记住,std::set::insert
具有带有提示参数的重载。如果数据已经按接近排序顺序排列,则可能会严重减少对比较器函数的调用次数。
您可能会考虑std::unordered_set
是否足够? 即您是否需要按排序顺序列出数据。或者,如果排序只是元素插入的内部内容std::set
。
最后,对于其他读者(OP已经明确表示他知道这一点),请记住测量。
我很难相信:
a) 比较功能在 30 秒内运行 1.8 亿次
和
b) 比较功能占用 25% 的 CPU 时间
都是真的。即使是Core 2 Duo也应该能够在不到一秒的时间内轻松运行1.8亿次比较(毕竟,声称它可以做12,000 MIPS之类的事情,如果这真的意味着什么的话)。所以我倾向于相信还有其他东西被归入分析软件的比较中。(例如,为新元素分配内存。
但是,您至少应该考虑 std::set 不是您要查找的数据结构的可能性。如果您在实际需要排序值(或最大值,甚至)之前执行数百万次插入,那么最好将这些值放入向量中,这是一种在时间和空间上都便宜得多的数据结构,并根据需要对其进行排序。
如果你真的因为担心碰撞而需要这个集合,那么你可以考虑一个unordered_set,它稍微便宜一些,但不如矢量便宜。(正是因为向量不能保证你的唯一性。但老实说,看看这个结构定义,我很难相信独特性对你很重要。
"基准">
在我的小酷睿i5笔记本电脑上,我想它与OP的机器不在同一联盟中,我运行了一些测试,将1000万个随机的唯一条目(只有两个比较字段)插入到std::set和std::vector中。最后,我对向量进行排序。
我这样做了两次;一次是使用产生可能独特成本的随机生成器,一次是使用产生两种不同成本的生成器(这应该使比较速度变慢)。一千万次插入的结果比OP报告的略多。
unique cost discrete cost
compares time compares time
set 243002508 14.7s 241042920 15.6s
vector 301036818 2.0s 302225452 2.3s
为了进一步隔离比较时间,我使用 std::sort 和 std::p artial_sort 重做了向量基准测试,使用了 10 个元素(基本上是前 10 个元素的选择)和 10% 的元素(即 100 万个)。较大partial_sort的结果让我感到惊讶 - 谁会认为对向量的10%进行排序会比对所有向量进行排序慢 - 但它们表明算法成本比比较成本重要得多:
unique cost discrete cost
compares time compares time
partial sort 10 10000598 0.6s 10000619 1.1s
partial sort 1M 77517081 2.3s 77567396 2.7s
full sort 301036818 2.0s 302225452 2.3s
结论:比较时间越长是可见的,但容器操作占主导地位。在总共 52 秒的计算时间内,1000 万次设置插入的总成本肯定是显而易见的。一千万个载体插入的总成本就不那么明显了。
小纸条,值得一提:
我从那段汇编代码中得到的一件事是,你不会通过使成本成为float
来节省任何东西。它实际上是为浮点数分配八个字节,因此您不会节省任何内存,并且您的 CPU 执行单个浮点比较的速度不会比单个双精度比较快。只是说'(即,当心过早优化)。
反对者,愿意解释吗?
对于最小值的每个提取,我都有很多插入。我想过使用斐波那契堆,但我被告知它们在理论上很好,但常数很高,实现起来非常复杂。由于插入在 O(log(n)) 中,因此对于较大的 n 来说,运行时间的增加几乎是恒定的。所以我认为坚持片场是可以的。
在我看来,这听起来像是一个典型的优先级队列应用程序。你说你刚刚考虑使用斐波那契堆,所以我想这样的优先级队列实现足以满足你的需求(推送元素,一次提取一个最小元素)。在你不遗余力地从比较函数中优化一个或两个时钟周期之前,我建议你尝试一些现成的优先级队列实现。像std::priority_queue
一样,boost::d_ary_heap
(或可变优先级队列的boost::d_ary_heap_indirect
)或任何其他提升堆结构。
我之前遇到过类似的情况,我在类似 A* 的算法中使用std::set
代替优先级队列(并且还尝试了带有std::inplace_merge
的排序std::vector
进行插入),切换到std::priority_queue
是性能的巨大提升,然后切换到boost::d_ary_heap_indirect
加倍努力。我建议你至少试一试,如果你还没有的话。
我本身没有答案 - 只有几个想法:
- 如果您使用的是 GCC,我会在启用并行模式的情况下运行一些基准测试
- 您确定没有处理成本构成的非规范化数字吗?
- 为什么我的最后一个 ELSE 条件无法正确执行
- 如果条件不相关,我应该更喜欢两个 if 语句而不是 if-else 语句吗?
- 如何改进一堆在已知值范围内评估变量的 else-if 条件?
- 当我的 if 条件计算结果为 false 时,我的 else 块将不会执行
- 我们可以在第一个else-if条件结束后使用另一个else-if条件吗
- 如何实现IF-ELSE条件模板
- 循环中的IF-ELSE条件:未来的迭代覆盖以前的迭代结果
- 如何在没有'switch'和'if-else'条件的情况下重写以下代码?
- 有没有办法将派生类作为 if-else 语句中的条件传递?
- 同时执行 if 和 else 条件的逻辑是什么
- 为什么我的else条件从未执行
- C++ 优化 if/else 条件
- 条件运算符vs if then else
- 使用 "else if" 是否消除了在 while 循环中的每个条件后休息的需要?
- 使用try-Catch异常处理程序和if-else条件检查之间的区别
- 编译时已知条件下的标准if/else
- if/else语句中存在多个条件
- 嵌套的if-else语句只执行一个条件(c++)
- If then else条件被跳过
- If和Else条件都执行c++