为啥单个线程比多个线程快,即使它们本质上具有相同的开销?
Posted
技术标签:
【中文标题】为啥单个线程比多个线程快,即使它们本质上具有相同的开销?【英文标题】:Why is a single thread faster than multiple threads even though they essentially have the same overhead?为什么单个线程比多个线程快,即使它们本质上具有相同的开销? 【发布时间】:2013-03-28 03:03:34 【问题描述】:我在 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:为什么会发生这种情况
【问题讨论】:
这看起来像是多个线程同时修改一个变量的雷区。 是的。缓存行争用。 多个线程在不同步的情况下修改同一个对象:未定义的行为。 【参考方案1】:您的代码本质上是错误的。
count++
是一个三步操作,它读取值、递增值,然后将其存储回变量中。
如果两个线程同时在同一个变量上运行count++
,其中一个将覆盖另一个的更改。
因此,多线程版本最终会做额外的工作,因为每个线程都会破坏其他线程的进度。
如果您将count
设为局部变量,则时间应该看起来更正常。
或者,您可以使用互锁增量,这是线程安全的,但需要额外开销来跨线程同步。
【讨论】:
啊,谢谢你聪明的先生。你会说锁定增量和使用多线程比单线程快吗? @lululoo:不,因为一次只有一个线程会更新count
。这就是同步的全部意义所在。递增整数需要加载、递增和存储。它不是原子操作。这不是多线程解决方案的好选择。您应该考虑一个更现实的问题,即可以分解为离散和独立任务的任务。【参考方案2】:
正如您最初问题的一些评论者指出的那样,您存在正确性和性能问题。首先,您的所有线程都在同时访问 count。这意味着无法保证线程实际上将全部计数到 3 亿。您可以通过在 poop 函数中声明 count 来解决此正确性错误
void poop(void* params)
int count = 0;
while(count < 300000000)
count++;
cout<< time(NULL) - t <<" \n";
请注意,这对 t 来说不是问题,因为它只能由线程读取而不是写入。但是,cout 存在问题,因为您也是从多个线程写入的。
此外,正如 cmets 中所指出的,您的所有线程都在访问一个内存位置。这意味着当线程更新 count 时,必须刷新并重新加载保存它的缓存行。这是非常低效的内存访问。通常,当您访问数组中的连续元素而不是单个变量时会发生这种情况(坏主意,见上文)。对此的解决方案是填充您的阵列以确保每个条目都是您的 L1 缓存行大小的精确倍数,这显然是特定于您的目标处理器的。另一种选择是重组您的算法,以便:每个线程处理一个大块的连续元素,或者每个线程访问元素的方式使得线程不访问相邻的位置。
当您使用 Windows 时,您可能需要考虑为您的代码使用更高级别的抽象,而不是 Win32 线程函数。 Parallel Patterns Library 符合此处的要求(Intel's Threaded Building Blocks 也是如此)。
concurrency::parallel_invoke(
[=] poop(nullptr); ,
[=] poop(nullptr);
);
这允许 PPL 将您的任务安排在线程池上,而不是您的应用程序必须显式创建线程。
您可能还认为,对于非常小的任务,启动额外线程的开销可能超过并行运行的收益。
【讨论】:
以上是关于为啥单个线程比多个线程快,即使它们本质上具有相同的开销?的主要内容,如果未能解决你的问题,请参考以下文章