为什么单个线程比多线程更快,即使它们基本上具有相同的开销?

时间:2013-03-28 03:03:34

标签: c++ windows multithreading cpu metrics

我在8核处理器上运行64位Windows 7。我运行了以下内容:

    #include "stdafx.h"
    #include <iostream>
    #include <Windows.h>
    #include <process.h>
    #include <ctime>

    using namespace std;

    int count = 0;
    int t = time(NULL);

    //poop() loops incrementing count until it is 300 million.
    void poop(void* params) {
        while(count < 300000000) {
            count++;
        }


        cout<< time(NULL) - t <<" \n";
    }

    int _tmain(int argc, _TCHAR* argv[])
    {
        //_beginthread(poop, 0, NULL);      
        //_beginthread(poop, 0, NULL);
        poop(NULL);

        cout<<"done"<<endl;

        while(1);

        return 0;
    }

我将结果与取消注释beginThread时的结果进行了比较。事实证明,单线程版本以最快的速度实现了这一目标!实际上,添加更多线程会使该过程花费更长时间。计算3亿这个过程需要花费8+秒,我认为这足以排除函数调用beginThread +其他轻微的开销。

我做了一些研究,多线程进程变慢的一般结论就是开销。但在这种情况下,无论我是运行多个线程还是单个线程,变量计数(存在于数据段中,因为它是预先分配的变量afaik)的次数是相等的。所以基本上,开销(如果是开销问题)并不是因为访问全局变量而不是局部变量的成本更高。

查看我的任务管理器,使用单个线程的进程使用13%cpu(大约1/8内核),添加线程会以1/8左右的增量增加cpu使用率。因此就cpu功率而言,假设任务管理器准确地描述了这一点,添加线程使用更多的cpu。这进一步让我感到困惑..我是如何使用更多的整体cpu,使用单独的内核,但整体上需要更长时间来完成任务?

TLDR:为什么会发生这种情况

2 个答案:

答案 0 :(得分:5)

您的代码本质上是错误的。

count++是一个三步操作,它读取值,递增值,然后将其存储回变量。
如果两个线程在同一个变量上同时运行count++,其中一个将覆盖另一个变量。

因此,多线程版本最终将完成额外的工作,因为每个线程都会破坏其他线程的进度。

如果你使count为局部变量,那么时间应该看起来更正常。

或者,您可以使用互锁增量,这是线程安全的,但是在跨线程同步时会有额外的开销。

答案 1 :(得分:3)

正如您原始问题上的一些评论者指出您有正确性和性能问题。首先,所有线程同时访问 count 。这意味着没有保证线程实际上所有计数达到3亿。您可以通过在 poop 函数中声明 count 来解决此正确性错误

void poop(void* params) {
    int count  = 0;
    while(count < 300000000) {
        count++;
    }
    cout<< time(NULL) - t <<" \n";
}

请注意,这不是 t 的问题,因为它只是由线程读取而不是写入。但是, cout 是一个问题,因为您还要从多个线程写入。

此外,正如评论中所指出的,所有线程都在访问单个内存位置。这意味着当线程更新 count 时,必须刷新并重新加载保存它的缓存行。这是非常低效的内存访问。通常,当您访问数组中的连续元素而不是单个变量时会发生这种情况(不好的主意,请参见上文)。解决方案是填充阵列以确保每个条目都是L1缓存行大小的精确倍数,这显然对目标处理器有些特定。另一个选择是重构你的算法,以便;每个线程处理一大块连续元素,或者每个线程访问元素的方式使得线程不访问相邻位置。

当您使用Windows时,您可能需要考虑为代码使用更高级别的抽象,而不是Win32线程函数。 Parallel Patterns Library符合此处的费用(Intel's Threaded Building Blocks也是如此)。

    concurrency::parallel_invoke(
        [=] { poop(nullptr); },
        [=] { poop(nullptr); }
    );

这允许PPL在线程池上安排您的任务,而不是您的应用程序必须显式创建线程。

您可能还会认为,对于非常小的任务,启动其他线程的开销可能会超过并行运行的收益。