【问题标题】:Why C++ implementation using std::unordered_map is much slower than equivalent Python implementation using dictionary?为什么使用 std::unordered_map 的 C++ 实现比使用字典的等效 Python 实现慢得多?
【发布时间】:2022-11-01 11:16:24
【问题描述】:

为此,我使用坐标变换 (x,y)-> 1000*x+y 来提高效率。

了解代码在做什么并不是很重要,但对于这个问题: https://oeis.org/A337663

这只是将它们添加到板上,然后将它们作为性能指标删除:

##################

#1###1###1###1###1#

##################

并跟踪触摸板上数字的邻居的总和

#include <iostream>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <ctime>

using namespace std;
//I Know this is bad practice, but just for readability for now

void add_update_edges_and_used(int spot, unordered_map<int, unordered_set<int> > &edge_sums_to_locations, unordered_map<int, int> &edge_locations_to_sums,
                               unordered_set<int> &used_locations, int current_number) {
    used_locations.insert(spot);

    vector<int> neighbors { spot+1000,spot-1000,
                            spot+1,spot-1,
                            spot+1000+1,spot-1000+1,
                            spot+1000-1,spot-1000-1 };

    for (int neighbor : neighbors) {
        if (used_locations.count(neighbor) == 0) {
            if (edge_locations_to_sums.count(neighbor)) {
                edge_sums_to_locations.at(edge_locations_to_sums.at(neighbor)).erase(neighbor);
                edge_locations_to_sums.at(neighbor) += current_number;
            } else {
                edge_locations_to_sums.insert({neighbor, current_number});
            }

            int new_neighbor_sum = edge_locations_to_sums[neighbor];
            if (edge_sums_to_locations.count(new_neighbor_sum)) {
                edge_sums_to_locations.at(new_neighbor_sum).insert(neighbor);
            } else {
                unordered_set<int> new_edge_sum_locations;
                new_edge_sum_locations.insert(neighbor);
                edge_sums_to_locations.insert({new_neighbor_sum, new_edge_sum_locations});
            }

        }
    }
}

int main() {

    std::clock_t start_time = std::clock();

    unordered_map<int, unordered_set<int> > edge_sums_to_locations;
    unordered_map<int, int> edge_locations_to_sums;
    unordered_set<int> used_locations;


    for (int q=0; q<1000; q++) {
        edge_sums_to_locations.clear();
        edge_locations_to_sums.clear();
        used_locations.clear();

        for (int i=0; i<100; i++) {
            add_update_edges_and_used(i*4, edge_sums_to_locations, edge_locations_to_sums,
                                      used_locations, 1);
        }
    }

    std::clock_t tot_time = std::clock() - start_time;
    std::cout << "Time: "
              << ((double) tot_time) / (double) CLOCKS_PER_SEC
              << " seconds" << std::endl;

    return 0;
}

大约需要 1 秒

import time
def add_update_edges_and_used(spot, edge_sums_to_locations, edge_locations_to_sums, 
                              used_locations, current_number):
    used_locations.add(spot)
    
    neighbors = {spot+1000,spot-1000,
                spot+1,spot-1,
                spot+1000+1,spot-1000+1,
                spot+1000-1,spot-1000-1}
    unused_neighbors = neighbors.difference(used_locations)
    
    for neighbor in unused_neighbors:
        if neighbor in edge_locations_to_sums.keys():
            edge_sums_to_locations[edge_locations_to_sums[neighbor]].remove(neighbor)
            edge_locations_to_sums[neighbor] += current_number
        else:
            edge_locations_to_sums[neighbor] = current_number
        new_neighbor_sum = edge_locations_to_sums[neighbor]
        if new_neighbor_sum in edge_sums_to_locations.keys():
            edge_sums_to_locations[new_neighbor_sum].add(neighbor)
        else:
            edge_sums_to_locations[new_neighbor_sum] = {neighbor}
            
start_time = time.time()
start_cpu_time = time.clock()

for q in range(1000):
    edge_sums_to_locations = {} #unordered map of ints to unordered set of ints
    edge_locations_to_sums = {} #unordered map of ints to ints
    used_locations = set() #unordered set of ints

    for i in range(100):
        add_update_edges_and_used(i*4, edge_sums_to_locations, edge_locations_to_sums, 
                                  used_locations, 1)

print(f'CPU time {time.clock() - start_cpu_time}')
print(f'Wall time {time.time() - start_time}')

大约需要 0.4 秒

这个问题在放大后仍然存在并且与擦除功能无关,而是基于分析的插入和删除。

我一直听说 C++ 通常更快,所以我希望通过从 python 转换为 C++ 来提高这个函数的速度。

【问题讨论】:

  • 您是否在打开编译器优化的情况下进行编译?
  • 此外,虽然unordered_set 的复杂度为O(1),但它基本上需要实现为std::vector&lt;std::list&lt;T&gt;&gt;,它具有糟糕的缓存局部性,
  • 顺便说一句,在 C++ 版本中,您不需要 2 次查找 (count+insert),只需 insert 就足够了:if (auto [it, inserted] edge_locations_to_sums.insert({neighbor, current_number}); !inserted) { edge_sums_to_locations.at(it-&gt;second).erase(neighbor); it-&gt;second += current_number; }。
  • @NathanOliver 优化似乎只对它进行了最低限度的改进,仍然比 python 差。我可以尝试查看其他类型的字典,但我认为 C++ 几乎总是更快

标签: python c++ performance


【解决方案1】:

大部分执行时间应该花在缓存未命中,分配并且在间接费用容器实现(即低级细节)。这就是为什么启用优化应该产生很小的影响(如果甚至可以忽略不计)。编译器不能轻易优化散列表容器。 CPython 和目标 C++ STL 实现之间的差距来自于容器的不同实现。

C++ unordered_map 和 unordered_set 实现 uses a separate chaining method for the hash-table。这是由于 C++ 标准中的几个限制(如提供的答案中所述)。简而言之,该实现基本上是一组链接列表(即vector&lt;list&lt;pair&lt;Key,Value&gt;&gt;&gt;),众所周知,它的效率非常低。此外,一些实现还使用模数将散列转换为桶中的索引,并且已知此操作在大多数平台上非常慢。

3.6+ CPython 实现使用不断增长的整数索引数组来引用包含条目的不断增长的数组。每个条目基本上是一个包含散列、键和相关值的元组。有关这方面的更多信息,请参阅here。 CPython 使用open-addressing method and more specifically a quadratic probing,只要负载系数特别小而且没有很多碰撞(否则,表需要很小)。

让我们比较两个实现。在 C++ 实现中,每个桶可能非常大,因为它通常包含一个指向条目(即键值对)的指针,并且指针在主流 64 位平台上占用 8 个字节。在 CPython 实现中,索引数组包含可变大小的整数:从 1 字节到 8 字节,与字典中的项目数有关。这是有效的,因为表在内存中可以更加紧凑,并且还降低了导致缓存未命中的风险(因为当每个项目使用 1 个字节时,表可以包含在少数缓存行中)。条目数组要大得多,但它的好处是可以打包在内存中(并有序)。最后,数据局部性(最近的)CPython 实现往往比 C++ 实现更好。

请尝试替代 C++ 哈希映射实现.您可以在 Internet 上找到使用开放寻址的非常有效的非标准 C++ 哈希映射实现。例如,Tessil 的 robin-map 和 hopscotch-map 以及 ankerl::unordered_dense::map 都特别高效且设计得非常好(请注意,还应调整散列以通过某些散列映射实现获得正确的性能) .你可以找到一些很棒的基准here、here 和there。注意没有一个哈希映射来统治它们:它们每个都有优点和缺点(关于用例的可变速度和内存占用)。尽管如此,在大多数用例中,它们中的许多通常可以显着优于其中一种 STL 实现。关于基准和您的特定用例,我建议您尝试使用 emhash7::HashMap 和 mumx 哈希函数。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-11-17
    • 1970-01-01
    • 2021-09-09
    • 2017-08-22
    • 2015-09-26
    • 1970-01-01
    • 2021-05-21
    • 2013-10-09
    相关资源
    最近更新 更多