【问题标题】:OpenMP: copying vector using ' multithreading'OpenMP:使用“多线程”复制向量
【发布时间】:2021-12-09 19:21:00
【问题描述】:

对于某个编码应用程序,我需要复制一个由大对象组成的向量,所以我想让它更有效率。我将在下面给出旧代码,尝试使用 OpenMP 来提高效率。

std::vector<Object> Objects, NewObjects;
Objects.reserve(30);
NewObjects.reserve(30);
// old code
Objects = NewObjects;

// new code
omp_set_num_threads(30);
#pragma omp parallel{
    Objects[omp_get_thread_num()] = NewObjects[omp_get_thread_num()];
}

这会产生相同的结果吗?或者自从我访问向量'对象'以来是否存在问题。我认为它可能会起作用,因为我不访问相同的索引/对象。

【问题讨论】:

  • omp_set_num_threads(30); 不保证您获得 30 个线程,您可能会获得更少,您的代码将无法正常工作。使用循环复制对象,可以轻松实现并行化。
  • @Laci 感谢您的评论。但是,我真的不明白如何并行化循环以将对象从单个向量复制到另一个向量。那不只是给出一个正常的for循环吗?很抱歉提出这样一个菜鸟问题。
  • 如果您所做的只是将字节从主内存中的一个位置复制到另一个位置,那么使用更多处理器可能对加快速度几乎没有作用。在任何传统类型的计算机中,只有一条主内存总线,不同的处理器缓存必须轮流访问它。当 CPU 在自己的本地缓存中逐字复制数据时,也许有一个很小的加速机会,但是与从主内存中获取缓存行并将它们写回主内存相比,缓存访问相对较快。我预测内存将成为瓶颈。
  • 您使用的是哪个版本的 C++?这些大对象中的一些数据是只读的吗?类定义是什么?这些大物体有多大?你的基准时间是多少?你的目标时间是什么时候?
  • 请记住,reserve() 不会改变向量的大小,而是为将来保留内存。您在示例中需要的是resize()

标签: c++ multithreading openmp


【解决方案1】:

omp_set_num_threads(30) 不保证您获得 30 个线程,您可能会获得更少,您的代码将无法正常工作。您必须使用循环并通过 OpenMP 将其并行化:

#pragma omp parallel for
for(size_t i=0;i<NewObjects.size(); ++i)
{
    Objects[i] = NewObjects[i];
}

请注意,它可能不会比串行版本快,因为并行执行有很大的开销。

如果您使用 C++17 编译器,最好的办法是使用 std::copy 并使用并行执行策略:

std::copy(std::execution::par, NewObjects.begin(), NewObjects.end(), Objects.begin());

【讨论】:

    【解决方案2】:

    我创建了一个benchmark 来查看我的测试机器复制对象的速度:

    #include <benchmark/benchmark.h>
    #include <omp.h>
    #include <vector>
    
    constexpr int operator "" _MB(unsigned long long v) { return v * 1024 * 1024; }
    
    class CopyableBigObject
    {
    public:
        CopyableBigObject(const size_t s) : vec(s) {}
        CopyableBigObject(const CopyableBigObject& other) = default;
        CopyableBigObject(CopyableBigObject&& other) = delete;
        ~CopyableBigObject() = default;
    
        CopyableBigObject& operator =(const CopyableBigObject&) = default;
        CopyableBigObject& operator =(CopyableBigObject&&) = delete;
    
        char& operator [](const int index) { return vec[index]; }
        size_t size() const { return vec.size(); }
    
    private:
        std::vector<char> vec;
    };
    
    // Force some work on the objects so they are not optimized away
    int calculated_value(std::vector<CopyableBigObject>& vec)
    {
        int sum = 0;
    
        for (int x = 0; x < vec.size(); ++x)
        {
            for (int index = 0; index < vec[x].size(); index += 100)
            {
                sum += vec[x][index];
            }
        }
    
        return sum;
    }
    
    static void BM_copy_big_objects(benchmark::State& state)
    {
        const size_t number_of_objects = state.range(0);
        const size_t data_size = state.range(1);
    
        for (auto _ : state)
        {
            std::vector<CopyableBigObject> src{ number_of_objects, CopyableBigObject(data_size) };
            std::vector<CopyableBigObject> dest;
    
            state.counters["src"] = calculated_value(src);
            dest = src;
            state.counters["dest"] = calculated_value(dest);
        }
    }
    
    static void BM_copy_big_objects_in_parallel(benchmark::State& state)
    {
        const size_t number_of_objects = state.range(0);
        const size_t data_size = state.range(1);
        const int number_of_threads = state.range(2);
    
        for (auto _ : state)
        {
            std::vector<CopyableBigObject> src{ number_of_objects, CopyableBigObject(data_size) };
            std::vector<CopyableBigObject> dest{ number_of_objects, CopyableBigObject(0) };
    
            state.counters["src"] = calculated_value(src);
    
    #pragma omp parallel num_threads(number_of_threads)
            {
                if (omp_get_thread_num() == 0)
                {
                    state.counters["number_of_threads"] = omp_get_num_threads();
                }
    
    #pragma omp for
                for (int x = 0; x < src.size(); ++x)
                {
                    dest[x] = src[x];
                }
            }
    
            state.counters["dest"] = calculated_value(dest);
        }
    }
    
    BENCHMARK(BM_copy_big_objects)
        ->Unit(benchmark::kMillisecond)
        ->Args({   30, 16_MB })
        ->Args({ 1000,  1_MB })
        ->Args({  100,  8_MB });
    
    BENCHMARK(BM_copy_big_objects_in_parallel)
        ->Unit(benchmark::kMillisecond)
        ->Args({ 100, 1_MB, 1 })
        ->Args({ 100, 8_MB, 1 })
        ->Args({ 800, 1_MB, 1 })
        ->Args({ 100, 8_MB, 2 })
        ->Args({ 100, 8_MB, 4 })
        ->Args({ 100, 8_MB, 8 });
    
    BENCHMARK_MAIN();
    

    这些是我在我的测试机器上得到的结果,一台旧 Xeon 工作站:

    Run on (4 X 2394 MHz CPU s)
    CPU Caches:
      L1 Data 32 KiB (x4)
      L1 Instruction 32 KiB (x4)
      L2 Unified 4096 KiB (x4)
      L3 Unified 16384 KiB (x1)
    Load Average: 0.25, 0.14, 0.10
    --------------------------------------------------------------------------------------------------------
    Benchmark                                              Time             CPU   Iterations UserCounters...
    --------------------------------------------------------------------------------------------------------
    BM_copy_big_objects/30/16777216                     30.9 ms         30.5 ms           24 dest=0 src=0
    BM_copy_big_objects/1000/1048576                   0.352 ms        0.349 ms         1987 dest=0 src=0
    BM_copy_big_objects/100/8388608                     4.62 ms         4.57 ms          155 dest=0 src=0
    BM_copy_big_objects_in_parallel/100/1048576/1      0.359 ms        0.355 ms         2028 dest=0 number_of_threads=1 src=0
    BM_copy_big_objects_in_parallel/100/8388608/1       4.67 ms         4.61 ms          151 dest=0 number_of_threads=1 src=0
    BM_copy_big_objects_in_parallel/800/1048576/1      0.357 ms        0.353 ms         1983 dest=0 number_of_threads=1 src=0
    BM_copy_big_objects_in_parallel/100/8388608/2       5.29 ms         5.23 ms          132 dest=0 number_of_threads=2 src=0
    BM_copy_big_objects_in_parallel/100/8388608/4       5.32 ms         5.25 ms          133 dest=0 number_of_threads=4 src=0
    BM_copy_big_objects_in_parallel/100/8388608/8       5.57 ms         3.98 ms          175 dest=0 number_of_threads=8 src=0
    

    正如我所料,并行复制不会提高性能。但是,复制大对象的速度比我预期的要慢。

    鉴于您声明您使用 C++14,您可以尝试一些可以提高性能的方法:

    1. 使用移动构造器/移动分配组合或unique_ptr移动对象,而不是复制。
    2. 使用Copy-On-Write 推迟复制成员变量,直到您真正需要它们。
      1. 这将使复制变得便宜,直到您必须更新一个大对象。
      2. 如果大部分对象在复制后没有更新,那么您应该会获得性能提升。
    3. 确保您的类定义使用最紧凑的表示。我已经看到类的大小不同,具体取决于它是发布版本还是调试版本,因为编译器对发布版本使用填充而不是调试版本。
    4. 可能会重写,因此完全避免了复制。

    在不知道你的对象的具体细节的情况下,是不可能给出具体答案的。但是,这应该指向一个完整的解决方案。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-07-13
      • 2016-08-17
      • 2012-02-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多