为啥 C++ 线程/未来的开销如此之大

Posted

技术标签:

【中文标题】为啥 C++ 线程/未来的开销如此之大【英文标题】:Why is the C++ thread/future overhead so big为什么 C++ 线程/未来的开销如此之大 【发布时间】:2018-06-01 05:33:19 【问题描述】:

我有一个工作例程(下面的代码),当我在单独的线程中运行它时运行速度较慢。据我所知,worker 代码和数据完全独立于其他线程。工作人员所做的只是将节点附加到树上。目标是让多个工人并行种植树木。

有人可以帮我理解为什么在单独的线程中运行工作线程时会有(显着的)开销吗?

编辑: 最初我测试了 WorkerFuture 两次,我更正了这一点,现在我在无线程和延迟异步情况下获得了相同(更好)的性能,并且在涉及额外线程时会产生相当大的开销。

编译命令(linux):g++ -std=c++11 main.cpp -o main -O3 -pthread

这是输出(以毫秒为单位的时间):

Thread     : 4000001 size in 1861 ms
Async      : 4000001 size in 1836 ms
Defer async: 4000001 size in 1423 ms
No thread  : 4000001 size in 1455 ms

代码:

#include <iostream>
#include <vector>
#include <random>
#include <chrono>
#include <thread>
#include <future>

struct Data

    int data;
;

struct Tree

    Data data;
    long long total;
    std::vector<Tree *> children;

    long long Size()
    
        long long size = 1;
        for (auto c : children)
            size += c->Size();
        return size;
    

    ~Tree()
    
        for (auto c : children)
            delete c;
    
;

int
GetRandom(long long size)

    static long long counter = 0;
    return counter++ % size;


void
Worker_(Tree *root)

    std::vector<Tree *> nodes = root;
    Tree *it = root;
    while (!it->children.empty())
    
        it = it->children[GetRandom(it->children.size())];
        nodes.push_back(it);
    
    for (int i = 0; i < 100; ++i)
        nodes.back()->children.push_back(new Tree10, 1, );
    for (auto t : nodes)
        ++t->total;


long long
Worker(long long iterations)

    Tree root = ;
    for (long long i = 0; i < iterations; ++i)
        Worker_(&root);
    return root.Size();


void ThreadFn(long long iterations, long long &result)

    result = Worker(iterations);


long long
WorkerThread(long long iterations)

    long long result = 0;
    std::thread t(ThreadFn, iterations, std::ref(result));
    t.join();
    return result;


long long
WorkerFuture(long long iterations)

    std::future<long long> f = std::async(std::launch::async, [iterations] 
        return Worker(iterations);
    );

    return f.get();


long long
WorkerFutureSameThread(long long iterations)

    std::future<long long> f = std::async(std::launch::deferred, [iterations] 
        return Worker(iterations);
    );

    return f.get();


int main()

    long long iterations = 40000;

    auto t1 = std::chrono::high_resolution_clock::now();
    auto total = WorkerThread(iterations);
    auto t2 = std::chrono::high_resolution_clock::now();
    std::cout << "Thread     : " << total << " size in " << std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count() << " ms\n";

    t1 = std::chrono::high_resolution_clock::now();
    total = WorkerFuture(iterations);
    t2 = std::chrono::high_resolution_clock::now();
    std::cout << "Async      : " << total << " size in " << std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count() << " ms\n";

    t1 = std::chrono::high_resolution_clock::now();
    total = WorkerFutureSameThread(iterations);
    t2 = std::chrono::high_resolution_clock::now();
    std::cout << "Defer async: " << total << " size in " << std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count() << " ms\n";

    t1 = std::chrono::high_resolution_clock::now();
    total = Worker(iterations);
    t2 = std::chrono::high_resolution_clock::now();
    std::cout << "No thread  : " << total << " size in " << std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count() << " ms\n";

【问题讨论】:

@SidS,不,我在 3 种较慢的情况下创建了一个线程,在最后一种情况下没有线程。 我没有看到超过一个线程被创建;但是,我认为您已经暗示了答案:没有其他工作正在发生,因此在没有线程的情况下完成它会比创建一个线程来完成它更快。 @Tas,我明白,目标是并行处理几个这样的线程,我希望性能几乎线性增长,直到核心数量。我不明白为什么我要花 0.3 秒在一个线程中完成工作。 你的“Defer async”看起来和你的“Asyc”一样。 我想他是在指出main 你有两次WorkerFuture() 而缺少WorkerFutureSameThread() 【参考方案1】:

问题似乎是由动态内存管理引起的。当涉及多个线程时(即使主线程什么都不做),C++ 运行时必须同步访问动态内存(堆),这会产生一些开销。我用 GCC 做了一些实验,你的问题的解决方案是使用一些可扩展的内存分配器库。例如,当我使用tbbmalloc 时,例如,

export LD_LIBRARY_PATH=$TBB_ROOT/lib/intel64/gcc4.7:$LD_LIBRARY_PATH
export LD_PRELOAD=libtbbmalloc_proxy.so.2

整个问题都消失了。

【讨论】:

如果我没记错的话,在我的代码中,当Worker退出时会调用析构函数,并且在所有情况下都会考虑节点删除。我删除了析构函数,时间稍微下降了一点,但差异仍然存在,仍然是 ~300 毫秒。 @kaspersky 我根据我所做的更多实验完全重写了我的答案。 g++ 的默认分配器是否如此糟糕以至于导致在单独线程上的执行增加 20%-30%?这似乎不应该是正确的,但我想不出还有什么可以解释的。 哇,太好了,非常感谢。我通过编写自定义分配器(未同步)确认了这一点,差异消失了:) @vu1p3n0x 这取决于 glibc 版本,更新更好,但仍然比 malloc 的特殊可扩展版本差,例如来自 TBB、tcmalloc、...的那些。【参考方案2】:

原因很简单。您不会以并行方式执行任何操作。 当额外的线程正在做某事时,主线程什么都不做(等待线程作业完成)。

如果是线程,你有额外的事情要做(处理线程和同步),所以你需要权衡。

要看到任何收益,您必须同时做至少两件事。

【讨论】:

我知道有一个权衡,我简直不敢相信它可以是 300 毫秒。并且差异随着节点数量的增加而增加。

以上是关于为啥 C++ 线程/未来的开销如此之大的主要内容,如果未能解决你的问题,请参考以下文章

为啥“快速排序”算法的这两种变体在性能上差别如此之大?

为啥 GridPane 列的间距如此之大

为啥这两种变体之间的速度差异如此之大?

为啥数组声明的顺序对性能影响如此之大?

为啥使用 XGBoost 的 rmse 和 mse 如此之大?

OpenCL:为啥这两种情况的性能差异如此之大?