我有点无聊所以我想尝试使用std :: thread并最终测量单线程和多线程控制台应用程序的性能。这是一个两部分问题。所以我开始使用一个大量的整数向量(800000英寸)的单线程总和。
int sum = 0;
auto start = chrono::high_resolution_clock::now();
for (int i = 0; i < 800000; ++i)
sum += ints[i];
auto end = chrono::high_resolution_clock::now();
auto diff = end - start;
然后我添加了基于范围的迭代器和基于for循环的迭代器,并用chrono :: high_resolution_clock以相同的方式测量。
for (auto& val : ints)
sum += val;
for (auto it = ints.begin(); it != ints.end(); ++it)
sum += *it;
此时控制台输出看起来像:
index loop: 30.0017ms
range loop: 221.013ms
iterator loop: 442.025ms
这是一个调试版本,所以我改为发布,差异是〜1ms支持基于索引。没什么大不了的,只是出于好奇:这三个for循环之间的调试模式有这么大差异吗?或者甚至在发布模式下1ms的差异?
我转向线程创建,并尝试使用此lambda对数组进行并行求和(通过引用捕获所有内容,以便我可以使用int的向量和先前声明的互斥量),使用基于索引的。
auto func = [&](int start, int total, int index)
{
int partial_sum = 0;
auto s = chrono::high_resolution_clock::now();
for (int i = start; i < start + total; ++i)
partial_sum += ints[i];
auto e = chrono::high_resolution_clock::now();
auto d = e - s;
m.lock();
cout << "thread " + to_string(index) + ": " << chrono::duration<double, milli>(d).count() << "ms" << endl;
sum += partial_sum;
m.unlock();
};
for (int i = 0; i < 8; ++i)
threads.push_back(thread(func, i * 100000, 100000, i));
基本上每个线程总计是总数组的1/8,最终的控制台输出是:
thread 0: 6.0004ms
thread 3: 6.0004ms
thread 2: 6.0004ms
thread 5: 7.0004ms
thread 4: 7.0004ms
thread 1: 7.0004ms
thread 6: 7.0004ms
thread 7: 7.0004ms
8 threads total: 53.0032ms
所以我猜这个问题的第二部分是在这里发生了什么? 2个线程的解决方案也以~30ms结束。缓存ping pong?别的什么?如果我做错了什么,那么这样做的正确方法是什么?另外,如果相关,我在8个线程的i7上尝试这个,所以是的,我知道我没有计算主线程,但尝试了7个独立的线程,并且几乎得到了相同的结果。
编辑:抱歉忘了提到这是在Windows 7上使用Visual Studio 2013和Visual Studio的v120编译器或其他任何名称。
EDIT2:这是整个主要功能: http://pastebin.com/HyZUYxSY
答案 0 :(得分:2)
在未打开优化的情况下,在幕后执行的所有方法调用都可能是真正的方法调用。内联函数可能没有内联但真的被调用。对于模板代码,您确实需要打开优化以避免所有代码都按字面意思进行。例如,你的迭代器代码可能会调用iter.end()800,000次,而operator!=用于比较800,000次,调用operator ==等等等等。
对于多线程代码,处理器很复杂。操作系统很复杂。您的代码并非仅在计算机上。您的计算机可以更改其时钟速度,切换到turbo模式,切换到热保护模式。将时间四舍五入到毫秒并不是很有帮助。可能是一个线程到6.49毫秒而另一个线程也是6.51并且它的舍入方式不同。
答案 1 :(得分:2)
这三个for循环之间的调试模式有这么大差异吗?
是。如果允许,一个体面的编译器可以为3个不同的循环中的每一个生成相同的输出,但是如果未启用优化,则迭代器版本具有更多的函数调用,并且函数调用具有一定的开销。
甚至在发布模式下1ms的差异?
您的测试代码:
start = ...
for (auto& val : ints)
sum += val;
end = ...
diff = end - start;
sum = 0;
根本不使用循环的结果,因此在优化时,编译器应该只是选择丢弃代码,如下所示:
start = ...
// do nothing...
end = ...
diff = end - start;
适用于所有循环。
1ms的差异可能是由标准库的使用实现中的“high_resolution_clock
”的高粒度以及执行期间的进程调度的差异产生的。我测量的指数基于0.04毫秒慢,但结果毫无意义。
答案 2 :(得分:1)
不知道如何实现这些std :: thread类,53ms的一个可能的解释可能是:
线程在实例化后立即启动。 (我看不到thread.start()或threads.StartAll()或类似的东西)。因此,在第一个线程实例变为活动状态期间,主线程可能(或可能不)被抢占。毕竟,无法保证线程会在各个内核上生成(线程关联)。
如果你仔细看看POSIX API,就会出现&#34;应用程序上下文&#34;和#34;系统上下文&#34;,这基本上意味着可能存在一个OS策略,它不会将所有核心用于1个应用程序。
在Windows上(这是你正在测试的地方),也许线程不会直接产生,而是通过线程池产生,可能还有一些额外的std :: thread功能,这可能会产生开销/延迟。 (如完成端口等)。
不幸的是我的机器非常快,所以我不得不增加处理的数据量以产生大量时间。但从好的方面来说,这提醒我要指出,通常情况下,当计算时间超过时间片(经验法则)的时候,它开始得到平行的回报。
在这里,我的&#34;本地人&#34; Windows实现,对于足够大的数组,最终使线程赢得单个线程计算。
#include <stdafx.h>
#include <nativethreadTest.h>
#include <vector>
#include <cstdint>
#include <Windows.h>
#include <chrono>
#include <iostream>
#include <thread>
struct Range
{
Range( const int32_t *p, size_t l)
: data(p)
, length(l)
, result(0)
{}
const int32_t *data;
size_t length;
int32_t result;
};
static int32_t Sum(const int32_t * data, size_t length)
{
int32_t sum = 0;
const int32_t *end = data + length;
for (; data != end; data++)
{
sum += *data;
}
return sum;
}
static int32_t TestSingleThreaded(const Range& range)
{
return Sum(range.data, range.length);
}
DWORD
WINAPI
CalcThread
(_In_ LPVOID lpParameter
)
{
Range * myRange = reinterpret_cast<Range*>(lpParameter);
myRange->result = Sum(myRange->data, myRange->length);
return 0;
}
static int32_t TestWithNCores(const Range& range, size_t ncores)
{
int32_t result = 0;
std::vector<Range> ranges;
size_t nextStart = 0;
size_t chunkLength = range.length / ncores;
size_t remainder = range.length - chunkLength * ncores;
while (nextStart < range.length)
{
ranges.push_back(Range(&range.data[nextStart], chunkLength));
nextStart += chunkLength;
}
Range remainderRange(&range.data[range.length - remainder], remainder);
std::vector<HANDLE> threadHandles;
threadHandles.reserve(ncores);
for (size_t i = 0; i < ncores; ++i)
{
threadHandles.push_back(::CreateThread(NULL, 0, CalcThread, &ranges[i], 0, NULL));
}
int32_t remainderResult = Sum(remainderRange.data, remainderRange.length);
DWORD waitResult = ::WaitForMultipleObjects((DWORD)threadHandles.size(), &threadHandles[0], TRUE, INFINITE);
if (WAIT_OBJECT_0 == waitResult)
{
for (auto& r : ranges)
{
result += r.result;
}
result += remainderResult;
}
else
{
throw std::runtime_error("Something went horribly - HORRIBLY wrong!");
}
for (auto& h : threadHandles)
{
::CloseHandle(h);
}
return result;
}
static int32_t TestWithSTLThreads(const Range& range, size_t ncores)
{
int32_t result = 0;
std::vector<Range> ranges;
size_t nextStart = 0;
size_t chunkLength = range.length / ncores;
size_t remainder = range.length - chunkLength * ncores;
while (nextStart < range.length)
{
ranges.push_back(Range(&range.data[nextStart], chunkLength));
nextStart += chunkLength;
}
Range remainderRange(&range.data[range.length - remainder], remainder);
std::vector<std::thread> threads;
for (size_t i = 0; i < ncores; ++i)
{
threads.push_back(std::thread([](Range* range){ range->result = Sum(range->data, range->length); }, &ranges[i]));
}
int32_t remainderResult = Sum(remainderRange.data, remainderRange.length);
for (auto& t : threads)
{
t.join();
}
for (auto& r : ranges)
{
result += r.result;
}
result += remainderResult;
return result;
}
void TestNativeThreads()
{
const size_t DATA_SIZE = 800000000ULL;
typedef std::vector<int32_t> DataVector;
DataVector data;
data.reserve(DATA_SIZE);
for (size_t i = 0; i < DATA_SIZE; ++i)
{
data.push_back(static_cast<int32_t>(i));
}
Range r = { data.data(), data.size() };
std::chrono::system_clock::time_point singleThreadedStart = std::chrono::high_resolution_clock::now();
int32_t result = TestSingleThreaded(r);
std::chrono::system_clock::time_point singleThreadedEnd = std::chrono::high_resolution_clock::now();
std::cout
<< "Single threaded sum: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(singleThreadedEnd - singleThreadedStart).count()
<< "ms." << " Result = " << result << std::endl;
std::chrono::system_clock::time_point multiThreadedStart = std::chrono::high_resolution_clock::now();
result = TestWithNCores(r, 8);
std::chrono::system_clock::time_point multiThreadedEnd = std::chrono::high_resolution_clock::now();
std::cout
<< "Multi threaded sum: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(multiThreadedEnd - multiThreadedStart).count()
<< "ms." << " Result = " << result << std::endl;
std::chrono::system_clock::time_point stdThreadedStart = std::chrono::high_resolution_clock::now();
result = TestWithSTLThreads(r, 8);
std::chrono::system_clock::time_point stdThreadedEnd = std::chrono::high_resolution_clock::now();
std::cout
<< "std::thread sum: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(stdThreadedEnd - stdThreadedStart).count()
<< "ms." << " Result = " << result << std::endl;
}
这是我的机器上此代码的输出:
Single threaded sum: 382ms. Result = -532120576
Multi threaded sum: 234ms. Result = -532120576
std::thread sum: 245ms. Result = -532120576
Press any key to continue . . ..
最后,我要提醒的是,提到编写此代码的方式是内存IO性能基准,而不是核心CPU计算基准。 更好的计算基准将使用少量本地数据,适合CPU缓存等。
尝试将数据拆分为范围可能会很有趣。如果每个帖子都是&#34;跳跃&#34;从开始到结束的数据与ncores的差距?线程1:0 8 16 ...线程2:1 9 17 ...等?也许然后是&#34; locality&#34;记忆可以获得额外的速度。
答案 3 :(得分:1)
除了如何在Windows上实现std :: thread之外,我还要提醒您注意可用的执行单元和上下文切换。
i7 不有8个实际执行单位。它是一款具有超线程功能的四核处理器。无论广告如何,HT都没有神奇地将可用线程数增加一倍。这是一个非常聪明的系统,尽可能尝试从额外的管道中获取指令。但最终所有指令只通过四个执行单元。 所以运行8(或7)个线程仍然比你的CPU可以真正同时处理。这意味着你的CPU必须在8个热线之间切换,要求计算时间。最重要的是来自操作系统的数百个线程,不可否认大多数是睡着了,需要时间,你的测量中存在很大的不确定性。
使用单线程for循环,操作系统可以将单个内核专用于该任务,并将半睡眠线程分散到其他三个上。这就是为什么你看到1个线程和8个线程之间存在这样的差异。
至于您的调试问题:您应该检查Visual Studio是否在调试时启用了Iterator检查。每当使用迭代器时启用它,它都会被绑定检查等。请参阅:https://msdn.microsoft.com/en-us/library/aa985965.aspx
最后:看一下-openmp开关。如果启用它并将OpenMP #pragmas应用于for循环,则可以取消所有手动创建的线程。我玩弄了类似的线程测试(因为它很酷。:))和OpenMPs的性能非常好。
答案 4 :(得分:1)
对于第一个问题,关于范围,迭代器和索引实现之间的性能差异,其他人已经指出,在非优化的构建中,通常内联的很多内容可能不是。
但是还有一个问题:默认情况下,在Debug版本中,Visual Studio will use checked iterators。检查通过已检查迭代器的访问是否安全(迭代器是否引用了有效元素?),因此使用它们的操作(包括基于范围的迭代)会受到严重惩罚。
对于第二部分,我不得不说这些持续时间似乎异常漫长。当我在本地运行代码,在核心i7-4770(Linux)上使用g ++ -O3编译时,我得到每个方法的亚毫秒时序,实际上比运行之间的抖动更少。改变代码以迭代每次测试1000次,得到更稳定的结果,索引和范围循环的每次测试时间为0.33 ms,没有额外的调整,并行测试大约为0.15 ms。
并行线程总共执行相同数量的操作,而且使用所有四个内核限制了CPU动态提高其时钟速度的能力。那么如何减少总时间呢?
我敢打赌,通过更好地利用每核心L2缓存来获得收益,共计四个。实际上,使用四个线程而不是八个线程可以将总并行时间减少到0.11 ms,这与更好的L2缓存使用一致。
浏览英特尔处理器文档时,所有Core i7处理器(包括移动处理器)都至少有4 MB的L3缓存,可以很好地容纳80万个4字节的整数。所以我很惊讶原始时间比我看到的要大100倍,并且8线程时间总计要大得多,正如你所推测的那样,这是一个强烈暗示它们正在颠覆缓存。我假设这表明Debug构建代码有多么不理想。你可以发布优化版本的结果吗?