为什么我的并行foreach循环实现比单线程慢?

时间:2019-02-15 18:47:19

标签: c++ multithreading

我正在尝试为std::vector实现并行的foreach循环,该循环以最佳线程数(内核数减去主线程的1)运行计算,但是,我的实现似乎不够快–实际运行速度比单线程慢了6倍!

经常将线程实例化归咎于瓶颈,因此我尝试使用更大的向量,但这似乎无济于事。

我目前仍在观看在单独线程中以13000-20000微秒执行的并行算法,而在主线程中以120-200微秒执行单线程的并行算法,却无法弄清我在做什么错。在运行13到20毫秒的并行算法中,通常使用8或9的并行算法来创建线程,但是,我仍然看不到std::for_each在单独线程中多次遍历向量1/3的理由比另一个std::for_each需要更长的时间来遍历整个向量。

#include <iostream>
#include <vector>
#include <thread>
#include <algorithm>
#include <chrono>

const unsigned int numCores = std::thread::hardware_concurrency();

const size_t numUse = numCores - 1;

struct foreach
{
    inline static void go(std::function<void(uint32_t&)>&& func, std::vector<uint32_t>& cont)
    {
        std::vector<std::thread> vec;
        vec.reserve(numUse);
        std::vector<std::vector<uint32_t>::iterator> arr(numUse + 1);
        size_t distance = cont.size() / numUse;
        for (size_t i = 0; i < numUse; i++)
            arr[i] = cont.begin() + i * distance;
        arr[numUse] = cont.end();
        for (size_t i = 0; i < numUse - 1; i++)
        {
            vec.emplace_back([&] { std::for_each(cont.begin() + i * distance, cont.begin() + (i + 1) * distance, func); });
        }
        vec.emplace_back([&] { std::for_each(cont.begin() + (numUse - 1) * distance, cont.end(), func); });
        for (auto &d : vec)
        {
            d.join();
        }
    }
};


int main()
{
    std::chrono::steady_clock clock;
    std::vector<uint32_t> numbers;
    for (size_t i = 0; i < 50000000; i++)
        numbers.push_back(i);
    std::chrono::steady_clock::time_point t0m = clock.now();
    std::for_each(numbers.begin(), numbers.end(), [](uint32_t& value) { ++value; });

    std::chrono::steady_clock::time_point t1m = clock.now();
    std::cout << "Single-threaded run executes in " << std::chrono::duration_cast<std::chrono::microseconds>(t1m - t0m).count() << "mcs\n";
    std::chrono::steady_clock::time_point t0s = clock.now();
    foreach::go([](uint32_t& i) { ++i; }, numbers);

    std::chrono::steady_clock::time_point t1s = clock.now();
    std::cout << "Multi-threaded run executes in " << std::chrono::duration_cast<std::chrono::microseconds>(t1s - t0s).count() << "mcs\n";
    getchar();
}

有没有一种方法可以优化它并提高性能?

我正在使用的编译器是Visual Studio 2017的编译器。 Config是版本x86。还建议我使用探查器,目前正在弄清楚如何使用探查器。

实际上,我设法使并行代码的运行速度比常规代码快,但是,这需要成千上万个包含五个元素的矢量。如果有人对如何提高性能或在哪里可以找到更好的实现以检查其结构提出建议,将不胜感激。

1 个答案:

答案 0 :(得分:2)

感谢您提供一些示例代码。

获得良好的指标(尤其是在并行代码上)可能非常棘手。您的指标已被污染。

  1. 使用high_resolution_clock代替steady_clock进行分析。
  2. 计时测量中不要包括线程启动时间。线程启动/连接比您在此处的实际工作长几个数量级。您应该创建一次线程,并使用条件变量使它们进入睡眠状态,直到发出信号通知它们正常工作为止。这并非微不足道,但是请不要测量线程启动时间。
  3. Visual Studio有一个探查器。您需要使用发行版优化来编译代码,但还需要包含调试符号(默认发行版配置中不包含这些符号)。我没有研究如何手动设置,因为我通常使用CMake,它会自动设置RelWithDebInfo配置。

与拥有良好指标相关的另一种问题是您的“工作”只是增加一个整数。这真的代表您的程序将要完成的工作吗?增量真的很快。如果您查看由顺序版本生成的程序集,则所有内容都会内联成一个非常短的循环。

Lambda很有可能被内联。但是,在您的go函数中,您是将lambda强制转换为std::functionstd::function内联的可能性很小。 因此,如果您想保留内联lambda的机会,则必须执行一些模板技巧:

template <typename FUNC>
inline static void go(FUNC&& func, std::vector<uint32_t>& cont)

通过手动内联代码(将go函数的内容移至main)并执行上面的步骤2,我可以获取并行版本(超线程双线程中有4个线程,核心)在大约75%的时间内运行。这并不是特别好的缩放比例,但是考虑到原始图像已经相当快了,这也不错。为了进行进一步的优化,我将使用SIMD aka“ vector”(不同于std::vector的意思是,除了它们都与数组有关之外)操作,该操作将在一次迭代中将增量应用于多个数组元素。

您在这里有比赛条件:

for (size_t i = 0; i < numUse - 1; i++)
{
    vec.emplace_back([&] { std::for_each(cont.begin() + i * distance, cont.begin() + (i + 1) * distance, func); });
}

由于将默认的lambda捕获设置为按引用捕获,因此i变量是一个引用,这可能导致某些线程检查错误的范围或范围太长。您可以这样做:[&, i],但是为什么要冒再次冒脚的风险呢? Scott Meyers建议不要使用默认捕获模式。只需[&cont, &distance, &func, i]

更新:

我认为将foreach移到自己的空间是个好主意。我认为您应该做的是将线程创建与任务分发分开。这意味着您需要某种信号系统(通常是条件变量)。您可以查看线程池。

添加线程池的一种简单方法是使用OpenMP,Visual Studio 2017支持(OpenMP 2.0)。需要注意的是,不能保证在并行段的进入/退出过程中不会创建/销毁线程(取决于实现)。因此,它在性能和易用性之间进行权衡。

如果可以使用C ++ 17,则它具有标准的并行for_eachExecutionPolicy重载)。大多数算法标准功能都可以做到。 https://en.cppreference.com/w/cpp/algorithm/for_each

对于使用std::function来说,您可以使用它,只是不想让您的基本操作(将被称为50,000,000次)是std::function

坏:

void go(std::function<...>& func)
{
    std::thread t(std::for_each(v.begin(), v.end(), func));
    ...
}

...
go([](int& i) { ++i; });

好:

void go(std::function<...>& func)
{
    std::thread t(func);
    ...
}

...
go([&v](){ std::for_each(v.begin(), v.end(), [](int& i) { ++i; })});

在好的版本中,较短的内部lambda(即++ i)会内联到for_each的调用中。这很重要,因为它被调用了5000万次。没有内联对较大的lambda的调用(因为它已转换为std::function),但是可以,因为每个线程仅被调用一次。