【问题标题】:How to use parallelism to improve performance of my code? (tried a few things)如何使用并行性来提高我的代码的性能? (尝试了一些东西)
【发布时间】:2021-02-03 16:07:08
【问题描述】:

所以问题来了:我有一个自定义对象的向量,我需要用向量中每个唯一的对象组合计算一些东西,如果结果是某个值,我需要构建一个 2-way关联列表。这是我按顺序完成的工作:

#include <itertools/combinations.hpp> //I'm using cppitertools library (installed through vcpkg)

class MyClass{
private:
    int x;
    std::string abc;
public:
    MyClass();
    GetX();
}

...

std::vector<MyClass> _myObjects;
std::unordered_map<const MyClass*, std::vector<const MyClass*>> _relationships;//list all other objects in relation with a specific object, based on custom computation on all possible combination

...
int ComputeRelation(const MyObject* obj0, const MyObject* obj1){
    //some random stuff
    return obj0->GetX() + obj1->GetX();
}
...

void BuildRelationship(){
    for(auto& combination : iter::Combination(_myObject,2)){
        //combination is a pair of MyObject
        if(ComputeRelation(combination[0],combination[1]) == 1){
            //build 2-way associative list
            _relationships[combination[0]].push_back(combination[1]);
            _relationships[combination[1]].push_back(combination[0]);
        }
    }
}

我最初到处都有引用而不是指针,但是 unordered_map 在使用引用时会抛出错误。

现在,这很好,但我想通过并行化循环来加速它,因为组合的数量会很快变得非常大(对于 100 万个对象的数据集,5000 亿个组合

这是我尝试过的:

  • std::for_each 与并行执行策略:我收到 Parallel algorithms require forward iterators or stronger 错误 --> 我无法控制迭代器类型,因为我是从 cppitertools 库中获取的
  • taskflow.for_each(来自 cpptaskflow 库):结果不一致。一开始我以为是因为并发访问_relationships,所以我尝试了互斥体但结果相同,所以我认为这又与迭代器类型有关。
  • 也尝试了变换/减少,但我遇到了与 std::for_each 相同的问题

我没有找到一种方法来简单地拆分组合集,而不必先将所有组合推入另一个容器(我认为这是低效的)

我觉得我错过了一些明显、简单的方法,但我的大脑有点卡住了。

谢谢!

编辑: 我最初只想保留一张包含 ComputeRelation 的所有结果的地图,但是由于有 5000 亿个元素,我的 RAM 使用量达到了疯狂的水平(>>100G),这就是为什么我决定只保留具有特定价值的关系感兴趣的

更新#1: 所以我想我找到了解决方法。我向 MyClass 添加了一个成员属性,用作 ID 或键。我使用了一个简单的散列库来散列我的类的一些属性(将字符串转换为 int)。然后,我没有构建指针容器,而是构建 int 容器,并在需要时使用哈希值从映射中检索实际对象,如下所示:

#include <itertools/combinations.hpp> //I'm using cppitertools library (installed through vcpkg)

class MyClass{
private:
    int x;
    std::string abc;
    uint64_t hash;
public:
    MyClass(int x, string abc){
        this.hash=SomeHashFn(abc);
    };
    GetX();
}

...

std::vector<uint64_t> _myHashes;//keeps order and duplicates
std::unordered_map<uint64_t, MyObject> _myObjects;
std::unordered_map<uint64_t, std::vector<uint64_t>> _relationships;//list all other objects in relation with a specific object, based on custom computation on all possible combinations

...
int ComputeRelation(const MyObject& obj0, const MyObject& obj1){
    //some random stuff
    return obj0.GetX() + obj1.GetX();
}
...

void BuildRelationship(){
    auto combinations = iter::Combination(_myHashes,2);
    std::for_each(std::execution::par, combinations.begin(), combinations.end(), [&](auto combination){
        //combination is a pair of hash
        MyObject obj0=_myObjects[combination[0]];
        MyObject obj1=_myObjects[combination[1]];
        if(ComputeRelation(obj0, obj1) == 1){
            //build 2-way associative list using hashes
            _relationships[combination[0]].push_back(combination[1]);
            _relationships[combination[1]].push_back(combination[0]);
        }
    }
}

更新 #2: 没关系 Update#1,由于iter::Combination 返回的迭代器,我仍然无法并行处理组合(对于std::for_each,我需要向前或更强)。所以我尝试创建一个包含所有组合的向量,但又遇到了 RAM 问题。快速计算告诉我,我需要大约 80Gb 来存储这个向量。

【问题讨论】:

  • 你真的需要_relationships 在一张巨大的地图上吗?之后你会用它做什么?
  • 我使用地图来检查对象 X 是否与 Y 有关系(很多时间)并找到 n 度关系(如遍历图表,两个对象之间所需的步长)

标签: c++ multithreading iterator combinations


【解决方案1】:

因为您的组合生成器在适当的位置工作,并返回对计算组合的引用,所以您需要复制组合以允许多个线程同时处理它们。

实现这一点的一种方法是对您的代码进行最小的更改,即运行并生成固定数量的组合(1000、65536、1000000),同时将它们存储在某个容器中(例如,您的向量)已通过调用reserve 预先分配)。然后您可以使用for_each 或其他一些并行化方法来处理该组。组处理完成后,您循环,构建下一个组(到已分配的向量中),然后重复,直到处理完所有组合。

这不会充分利用并发性。为了接近完全并发,您可能需要编写更复杂的代码,可能需要手动管理线程。例如,您可以让一个线程生成组合并将它们传递给工作线程,或者让每个工作线程调用一个共享组合生成器,该生成器将传递回每个组合(它需要通过同步保护,并且可能是潜在的瓶颈)。

一个更简单的实现可能是可能的,这取决于每个组合需要多少处理。有两个线程可能就足够了,一个生成组合,另一个处理它们。这将使用两个存储的组合并在它们之间切换(一个被写入,另一个被读取)。

【讨论】:

  • 谢谢,我认为如果并行运行,固定块可以提供一些效率。当每个线程更新它时,我仍然必须锁定_relationships。我会尝试并报告结果
【解决方案2】:
  1. std::unordered_map 可能非常慢(在当今大多数可用的实现中)。尝试另一个容器,例如dense_hash_map.

  2. 您可以通过键的某些属性“分片”结果映射。例如,给定MyClass* 类型的键,您可以散列指针值并使用hash % n,其中n 是分片数,作为子映射向量的索引。然后您将能够在n 并行线程中填充n 子映射(理想情况下n = 可用处理器的数量)。
    缺点是查找变得有点复杂——您还需要在查找期间计算hash % n。您可以使用散列函数来找到性能和均匀性之间的最佳平衡。例如,(uintptr_t)key &gt;&gt; 4 可能是一个足够好但速度非常快的哈希函数。

【讨论】:

  • 感谢您的建议。我一定会调查dense_hash_map。
  • dense_hash_map 根据文档“可以使用比其他哈希映射实现更多的空间”。该问题已经抱怨所需的空间。如果有的话,你会想要一个空间优化的版本。
猜你喜欢
  • 2021-07-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-08-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多