C++ 似乎比 Project Euler 的 Python Ruby 慢得多

Posted

技术标签:

【中文标题】C++ 似乎比 Project Euler 的 Python Ruby 慢得多【英文标题】:C++ appears to be significantly slower than both Python Ruby for Project Euler 【发布时间】:2015-02-08 01:25:54 【问题描述】:

我有来自 Project Euler 的以下问题的 3 个解决方案。

如果 p 是一个整数长度的直角三角形的周长 边,a,b,c,对于 p = 120,恰好有三个解。

20,48,52,24,45,51,30,40,50

对于 p ≤ 1000 的哪个值,解的数量是否最大?

我为每种语言提供的三种解决方案如下。

C++:

boost::chrono::steady_clock::time_point start_time = boost::chrono::steady_clock::now();
map<int, int> square_lookup;
for(int i=0; i<= 1500; i++) 
    square_lookup[i*i] = i ;

auto end_time = boost::chrono::steady_clock::now();

Python2:

start = time.time()
res = range(1, 1501)
squares = 
#square_lookups = dict(zip([x*x for x in res], res))
square_lookups = 
for x in range(1, 1501):
    square_lookups[x*x] = x
end = time.time()

鲁比:

start_time = Time.now

square_lookup = 
(1 .. 1500).map |x| x*x.each_with_index do |square, root|
    square_lookup[square] = root+1
end

end_time = Time.now

四核 i5 的时序:

> lookup gen time: 0.00141787528992 
> Python Result: 840 Time:
> 0.282248973846
> 
> Lookup gen time 4640960 nanoseconds 
> C++: Result: 840 : Time: 695301578 nanoseconds
> 
> 
> Lookup gen time 0.000729416
> Ruby: Result: 840 Time: 0.149393345

查找生成时间是构造一个包含 1500 个元素的哈希表所花费的时间,其中键是完美的平方,值是它们各自的根。

即使在这方面,C++ 仍然比 Python 和 RubySLOWER。我意识到对于每种语言,我可能拥有总体上最有效的解决方案,但使用相同类型的操作仍然表明 C++ 非常慢。

重要编辑我将map 更改为使用unordered_map 作为C++ 解决方案,但它仍然较慢!

修改后的 C++ 文件:http://pastebin.com/2YyB6Rfm

lookup gen time: 0.00134301185608
Python Result: 840 Time: 0.280808925629

Lookup gen time 2021697 nanoseconds
C++: Result: 840 : Time: 392731891 nanoseconds

Lookup gen time 0.000729313
Ruby: Result: 840 Time: 0.148183345

【问题讨论】:

不,问题解决了,我在问为什么C++实现比较慢。 既然已经有chronos库,为什么还要使用boost/chrono库? 你为什么评论与我的问题无关的无用的cmets? 你用什么优化设置编译的? @ViktorChynarov:C++ 标准库在没有优化的情况下是出了名的慢。 (就像我编写的大多数 C++ 代码一样。) 【参考方案1】:

您的代码还有另一个严重的问题——比mapunordered_map 严重得多(至少在IMO)。

尤其是你在哪里做的:

int result = square_lookup[(i*i) + (j*j)];

if(result)  
    int perimeter = i + j + result;
    if(perimeter <= 1000) 
        occurences[perimeter] += 1;
    

此代码不只是在现有地图中查找值i*i+j*j。相反,如果映射中不存在键,它会在映射中插入一个节点,其中i*i+j*j 作为键,0(或者更具体地说,映射的 value_type 的值初始化对象,在这种情况下是int)进入地图。

在地图中为您不关心的所有值插入节点非常慢。您在这里尝试做的实际上只是检查该值是否已经在地图中。为此,您可以使用如下代码:

auto result = square_lookup.find(i*i + j*j);

if (result!=square_lookup.end())  
    int perimeter = i + j + result->second;
    if (perimeter <= 1000) 
        ++occurences[perimeter];                

这使用find 来查找键是否在映射中。然后如果(且仅当)键在映射中,它会查找当前与该键关联的值。

这大大提高了速度 - 使用 VC++ 或 g++ 大约为 20-30 毫秒。

随着这种变化的发生,mapunordered_map 之间的差异也缩小了。使用 map 的代码仍然可以在 ~20-30 毫秒内运行。平均而言,使用unordered_map 的代码可能会稍微快一点,但我的系统时钟只有 10 毫秒的粒度,所以我真的必须用更多的数据进行测试才能确定。

作为参考,这是我运行时的代码(请注意,我对代码进行了一些其他常规清理,但其他任何事情都不会对速度产生任何重大影响):

#include <iostream>
#include <unordered_map>
#include <chrono>
#include <iterator>
#include <algorithm>
#include <utility>
#include <map>

using namespace std;

int main() 
    auto start_time = chrono::steady_clock::now();
    map<int, int> square_lookup;
    int ctr = 0;
    generate_n(inserter(square_lookup, square_lookup.end()),
        1500,
        [&]()  ++ctr;  return make_pair(ctr*ctr, ctr); );

    auto end_time = chrono::steady_clock::now();

    cout << "Lookup gen time "
        << chrono::duration_cast<chrono::milliseconds>(end_time - start_time).count() << "\n";

    map<int, int> occurences;
    typedef std::pair<int, int> const &map_t;

    for (int i = 0; i <= 1000; i++) 
        for (int j = i; j <= 1000; j++) 
            auto result = square_lookup.find(i*i + j*j);

            if (result != square_lookup.end())  
                int perimeter = i + j + result->second;
                if (perimeter <= 1000)
                    ++occurences[perimeter];
            
        
    

    auto it = std::max_element(occurences.begin(), occurences.end(), 
        [](map_t a, map_t b)  return a.second < b.second; );

    end_time = chrono::steady_clock::now();
    cout << "C++: Result: " << it->first << " : Time: "
        << chrono::duration_cast<chrono::milliseconds>(end_time - start_time).count() << "\n";

总结:在 C++ 中,map 上的 [] 运算符将插入一个不存在的项目。这可能很方便,但并不总是您想要的。如果您只想检索一个已经存在的值,那么它不是适合这项工作的工具——.find 可以大大加快速度。

一旦你纠正了这个问题,mapunordered_map 之间的区别(至少大部分情况下)就消失了。

【讨论】:

make_pair(ctr, ctr*ctr) - 不应该是相反的顺序吗? @T.C.:糟糕——是的,确实应该。谢谢。 @ViktorChynarov:不幸的是,这个答案所指的原始问题中的代码不再可见(没有进入问题的编辑历史记录,然后点击 pastebin 链接)。跨度> 【参考方案2】:

你声称你在计时

查找生成时间是构造一个包含 1500 个元素的哈希表所需的时间,其中键是一个完美的正方形,值是它们各自的根。

对于 Python 和 Ruby 解决方案来说确实如此,但在 C++ 示例中,您正在构建一个 std::map&lt;int, int&gt;。那是不是哈希表——它是一棵红黑树。插入和查找是O(lg N),而不是O(1)

为了公平比较,您希望使用std::unordered_map&lt;int, int&gt; 作为您的类型。那是一个真正的哈希表。

【讨论】:

谢谢,我把map的实例改成了unordered_map,但是还是比较慢。 C++:结果:840:时间:392731891 纳秒 你编译优化了吗? (即,您是否将-O2-O3 传递给编译器?)此外,为了使用std::unordered_map 获得最佳结果,请在使用前将最大负载因子设置为合理的值。例如,yourmap.max_load_factor(0.75); 我现在同时使用 -O2 和 -O3 进行编译,它让它变得更快了。大约 0.12 秒,将 max_load_factor 更改为 0.75 会有所不同(从 0.15 不变)。

以上是关于C++ 似乎比 Project Euler 的 Python Ruby 慢得多的主要内容,如果未能解决你的问题,请参考以下文章

来自初学者的 Project Euler #8 c++

project euler做题记录

project euler 169

用 Javascript 解决 Project Euler 16

Project Euler——13.Larger Sum总结

Project-Euler (Make/Source) 的有用文件夹结构? [关闭]