2个线程比1慢

2 threads slower than 1?

本文关键字:线程 2个      更新时间:2023-10-16

我正在玩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个线程的代码慢?

您有两个线程争夺同一个变量k。所以你在处理器说"处理器1:嘿,你知道k的值是多少吗?"处理器2:当然,给你!",每隔几次更新就会来回乒乓声。由于k不是原子的,因此也不能保证thread2不会写入k的"旧"值,因此下一次线程1读取该值时,它会跳回1、2、10或100步,并且必须重新执行—理论上,这可能导致两个循环都没有完成,但这需要相当多的坏运气。

这实际上应该是对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不是迭代的除数的情况,但对于本演示来说它已经足够接近了)。不幸的是,我们仍然遇到内存中共享元素访问的争用。

#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在外部作用域为每个线程提供自己的存储空间,并在连接之后总结结果,从而消除对互斥锁的需求。

在我看来,比"为什么这不起作用"更重要的问题是"我如何让它工作?"对于手头的任务,我认为std::async(尽管有明显的缺点)确实是比直接使用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;
}

我遇到了这个问题。我的观点是,对于某些类型的作业,管理线程的成本可能大于在线程中运行所带来的好处。下面是我的代码示例,在一个循环中做一些实际的工作,大量的迭代,所以我得到了非常一致的数字与时间命令。

   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

对于大的数,我忽略了秒的小数。我的代码只更新一个面向的共享变量。我还没有让它更新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