C ++优化if / else条件

时间:2012-12-12 03:44:58

标签: c++ performance assembly

我有一行代码,占用了我应用程序运行时的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相同。 每次提取最小值时都会有很多插入。我想过使用Fibonacci-Heaps,但我被告知它们理论上很好,但是它们受到高常数的影响并且实现起来非常复杂。并且由于insert在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,在多核的intel机器上有gcc 4.6(-stl = c ++ 0x)。只有可用的库是boost,openmp和tbb。 30秒的基准测试是在我4岁的笔记本电脑(核心2双核处理器)上进行的。

我真的被困在这个上,看起来很简单,但花了那么多时间。我几天以来一直在思考如何改进这条线......

你能给我一个如何改进这部分的建议,还是它已经处于最佳状态?

编辑1:在使用Jerrys建议后,我达到了~4.5秒的加速。 编辑2:在尝试提升斐波那契堆后,比较转为174 Mio调用低于功能。

5 个答案:

答案 0 :(得分:11)

我很难相信:

  

a)比较功能在30秒内运行1.8亿次

  

b)比较函数使用25%的cpu时间

都是真的。即使是Core 2 Duo也应该能够在不到一秒的时间内完成1.8亿次比较(毕竟,声称它可以做到12,000 MIPS,如果这实际上意味着什么)。所以我倾向于相信通过分析软件进行比较还有其他东西被混淆了。 (例如,为新元素分配内存。)

但是,您至少应该考虑std :: set不是您正在寻找的数据结构的可能性。如果在实际需要排序值(或最大值,偶数)之前进行数百万次插入,那么最好将值放入向量中,这是一种在时间和空间上都要便宜得多的数据结构,并且排序它随需应变。

如果你真的需要这套装,因为你担心碰撞,那么你可能会考虑使用unordered_set,这样稍微便宜但不像矢量那么便宜。 (正因为向量不能保证你的独特性。)但老实说,看看结构定义,我很难相信独特性对你很重要。

<强> “基准”

在我的小型Core 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 :: partial_sort,使用10个元素(基本上选择了前10个)和10%的元素(那个元素)重新编写了矢量基准。是,一百万)。更大的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来保存任何东西。它实际上为float分配了8个字节,因此你不会保存任何内存,并且你的cpu不会比单个双重比较更快地执行单个float比较。只是说'(即谨防过早优化)。

Downvoter,小心解释?

答案 1 :(得分:11)

一个简单的解决方案是预先计算一个排序标识符,其中包括最重要的成本和其余的成本。

如,

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明确表示他已经意识到这一点),请记住 MEASURE

答案 2 :(得分:10)

让我先说明这一事实,我在这里要概述的内容是脆弱的,而且不是完全可移植的 - 但在适当的情况下(这几乎是你所指定的)我有理由相信它应该正常工作。

它依赖的一点是IEEE浮点数经过精心设计,因此如果将它们的位模式视为整数,它们仍然会按正确的顺序排序(模数为NaNs,为此真的没有“正确的秩序”)。

为了充分利用它,我们所做的就是打包Entry,这样构成我们键的两个部分之间就没有填充。然后我们确保整个结构与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之间填充,再次打破比较。同样,如果某人删除了对齐的(8),代码可能会失去一些速度,因为它试图加载未与8字节边界对齐的8字节数量(在另一个处理器上,这可能会完全失败)。 [编辑:哎呀。 @rici让我想起了我打算在这里列出的内容,但是忘记了:只有当_idcost都是正面时,这才能正常工作。如果_cost为负数,则IEEE浮点使用带符号的幅度表示的事实将使比较混乱。如果_id为负数,则其符号位将被视为数字中间的正常位,因此负数_id将显示为大于正数_id。 ]

总结一下:这很脆弱。毫无疑问。尽管如此,它应该非常快 - 特别是如果你使用的是64位编译器,在这种情况下,我希望这种比较可以用于两次加载和一次比较。总而言之,您可能无法更快地进行比较 - 您可以做的就是尝试并行执行更多操作,优化内存使用模式等。

答案 3 :(得分:1)

  

每次提取最小值时都会有很多插入。我想过使用Fibonacci-Heaps,但我被告知它们理论上很好,但是它们受到高常数的影响并且实现起来非常复杂。并且由于insert在O(log(n))中,运行时增加几乎是恒定的,大n。所以我认为可以坚持下去。

这对我来说就像一个典型的优先级队列应用程序。你说你刚考虑使用Fibonacci堆,所以我猜这样的优先级队列实现就足以满足你的需求(推送元素,并一次提取一个min元素)。在您开始尝试优化比较函数中的一个或两个时钟周期之前,我建议您尝试一些现成的优先级队列实现。与std::priority_queueboost::d_ary_heap(或可变优先级队列的boost::d_ary_heap_indirect)或any other boost heap structure一样。

之前我遇到过类似的情况,我在类似A *的算法中使用std::set代替优先级队列(并尝试使用std::vector进行排序std::inplace_merge对于插入而言,切换到std::priority_queue是一个巨大的性能提升,然后转换到boost::d_ary_heap_indirect更加努力。如果你还没有,我建议你至少尝试一下。

答案 4 :(得分:0)

我本身没有答案 - 只是几个想法:

  1. 如果您正在使用GCC,我会使用parallel mode enabled
  2. 运行一些基准测试
  3. 你确定你没有处理成本组件的非规范化数字吗?