2个线程比1慢?

时间:2013-06-16 19:56:26

标签: c++ multithreading performance c++11 stdthread

我正在玩std::thread并且出现了一些奇怪的东西:

#include <thread>

int k = 0;

int main() {
    std::thread t1([]() { while (k < 1000000000) { k = k + 1; }});
    std::thread t2([]() { while (k < 1000000000) { k = k + 1; }});

    t1.join();
    t2.join();

    return 0;
}

当使用clang ++编译上面的代码而没有优化时,我得到了以下基准:

real 0m2.377s  
user 0m4.688s  
sys  0m0.005s

然后我将代码更改为以下内容:(现在仅使用1个线程)

#include <thread>

int k = 0;

int main() {
    std::thread t1([]() { while (k < 1000000000) { k = k + 1; }});

    t1.join();

    return 0;
}

这些是新的基准:

real 0m2.304s
user 0m2.298s
sys  0m0.003s

为什么使用2个线程的代码比使用1的代码慢?

4 个答案:

答案 0 :(得分:15)

你有两个线程争夺同一个变量k。因此,你花时间在处理器上说“处理器1:嘿,你知道k有什么价值吗?处理器2:当然,你走了!”,每次更新都来回乒乓。由于k不是原子的,因此也无法保证thread2不会写入k的“旧”值,以便下次线程1读取该值时,它会跳回1,2,10或100步,并且必须重新做 - 理论上可能导致每次完成都没有循环,但这将需要相当多的坏运气。

答案 1 :(得分:4)

这应该是对Mats Petersson回答的回复,但我想提供代码示例。

问题是特定资源和高速缓存行的争用。

备选方案1:

#include <cstdint>
#include <thread>
#include <vector>
#include <stdlib.h>

static const uint64_t ITERATIONS = 10000000000ULL;

int main(int argc, const char** argv)
{
    size_t numThreads = 1;
    if (argc > 1) {
        numThreads = strtoul(argv[1], NULL, 10);
        if (numThreads == 0)
            return -1;
    }

    std::vector<std::thread> threads;

    uint64_t k = 0;
    for (size_t t = 0; t < numThreads; ++t) {
       threads.emplace_back([&k]() { // capture k by reference so we all use the same k.
           while (k < ITERATIONS) {
               k++;
           }
       });
    }

    for (size_t t = 0; t < numThreads; ++t) {
        threads[t].join();
    }
    return 0;
}

这里的线程争用一个变量,执行读取和写入,这会强制它进行乒乓,从而导致争用并使单线程案例最有效。

#include <cstdint>
#include <thread>
#include <vector>
#include <stdlib.h>
#include <atomic>

static const uint64_t ITERATIONS = 10000000000ULL;

int main(int argc, const char** argv)
{
    size_t numThreads = 1;
    if (argc > 1) {
        numThreads = strtoul(argv[1], NULL, 10);
        if (numThreads == 0)
            return -1;
    }

    std::vector<std::thread> threads;

    std::atomic<uint64_t> k = 0;
    for (size_t t = 0; t < numThreads; ++t) {
       threads.emplace_back([&]() {
           // Imperfect division of labor, we'll fall short in some cases.
           for (size_t i = 0; i < ITERATIONS / numThreads; ++i) {
               k++;
           }
       });
    }

    for (size_t t = 0; t < numThreads; ++t) {
        threads[t].join();
    }
    return 0;
}

在这里,我们确定性地划分劳动力(我们认为numThreads不是ITERATIONS的除数但它足够接近这个示范的情况)。不幸的是,我们仍然遇到争用内存中共享元素的争用。

#include <cstdint>
#include <thread>
#include <vector>
#include <stdlib.h>
#include <atomic>

static const uint64_t ITERATIONS = 10000000000ULL;

int main(int argc, const char** argv)
{
    size_t numThreads = 1;
    if (argc > 1) {
        numThreads = strtoul(argv[1], NULL, 10);
        if (numThreads == 0)
            return -1;
    }

    std::vector<std::thread> threads;
    std::vector<uint64_t> ks;

    for (size_t t = 0; t < numThreads; ++t) {
       threads.emplace_back([=, &ks]() {
           auto& k = ks[t];
           // Imperfect division of labor, we'll fall short in some cases.
           for (size_t i = 0; i < ITERATIONS / numThreads; ++i) {
               k++;
           }
       });
    }

    uint64_t k = 0;
    for (size_t t = 0; t < numThreads; ++t) {
        threads[t].join();
        k += ks[t];
    }
    return 0;
}

这再次确定了工作负载的分布,最后我们花了很少的精力来整理结果。但是,我们没有做任何事情来确保计数器的分配有利于健康的CPU分配。为此:

#include <cstdint>
#include <thread>
#include <vector>
#include <stdlib.h>

static const uint64_t ITERATIONS = 10000000000ULL;
#define CACHE_LINE_SIZE 128

int main(int argc, const char** argv)
{
    size_t numThreads = 1;
    if (argc > 1) {
        numThreads = strtoul(argv[1], NULL, 10);
        if (numThreads == 0)
            return -1;
    }

    std::vector<std::thread> threads;
    std::mutex kMutex;
    uint64_t k = 0;

    for (size_t t = 0; t < numThreads; ++t) {
       threads.emplace_back([=, &k]() {
           alignas(CACHE_LINE_SIZE) uint64_t myK = 0;
           // Imperfect division of labor, we'll fall short in some cases.
           for (uint64_t i = 0; i < ITERATIONS / numThreads; ++i) {
               myK++;
           }
           kMutex.lock();
           k += myK;
           kMutex.unlock();
       });
    }

    for (size_t t = 0; t < numThreads; ++t) {
        threads[t].join();
    }
    return 0;
}

这里我们避免线程之间的争用到缓存行级别,除了我们使用互斥锁控制同步的末端的单个案例。对于这个微不足道的工作量,互斥量将会有一个相对成本的地狱。或者,您可以使用alignas为每个线程在外部作用域提供自己的存储,并在连接后汇总结果,从而无需使用互斥锁。我将此作为练习留给读者。

答案 2 :(得分:2)

对我而言,似乎更重要的问题是“为什么这不起作用?”是“我如何让这个工作?”对于手头的任务,我认为std::async(尽管significant shortcomings)确实是比直接使用std::thread更好的工具。

#include <future>
#include <iostream>

int k = 0;
unsigned tasks = std::thread::hardware_concurrency();
unsigned reps = 1000000000 / tasks;

int main() {
    std::vector<std::future<int>> f;

    for (int i=0; i<tasks; i++)
        f.emplace_back(std::async(std::launch::async, 
                                  [](){int j; for (j=0; j<reps; j++); return j;})
                      );

    for (int i=0; i<tasks; i++) {
        f[i].wait();
        k += f[i].get();
    }

    std::cout << k << "\n";
    return 0;
}

答案 3 :(得分:0)

我遇到了这个问题。我的观点是,对于某种类型的工作,管理线程的成本可能不仅仅是你在线程中运行所获得的好处。这是我的代码示例,在循环大量迭代中做了一些真正的工作,所以我用time命令得到了非常一致的数字。

   pair<int,int> result{0,0};
#ifdef USETHREAD
      thread thread_l(&Myclass::trimLeft, this, std::ref(fsq), std::ref(oriencnt), std::ref(result.first));
      thread thread_r(&Myclass::trimRight, this, std::ref(fsq), std::ref(oriencnt), std::ref(result.second));
      thread_l.join();
      thread_r.join();
#else
      // non threaded version faster
      trimLeft(fsq, oriencnt, result.first);
      trimRight(fsq, oriencnt, result.second);
#endif

   return result;

时间结果

Thead          No_thread
===========================    
Real  4m28s          2m49s
usr   0m55s          2m49s
sys   0m6.2s         0m0.012s

我忽略大秒的小数秒。我的代码只更新了一个共享变量 oriencnt 。我还没有让它更新 fsq 。它看起来在线程版本中,系统正在做更多的工作,这导致更长的时钟时间(实时)。我的编译器标志是默认的-g -O2,不确定这是否是关键问题。使用-O3编译时,差异很小。还有一些互斥锁控制的IO操作。我的实验表明,这并没有造成差异。我正在使用gcc 5.4和C ++ 11。一种可能性是库未优化。

这是用O3编译的

       Thead    No_thread
=========================
real   4m24        2m44s
usr    0m54s       2m44s
sys    0m6.2s      0m0.016s