我是一般的编程新手所以请在回答我的问题时牢记这一点。
我有一个程序,它采用一个大型3D阵列(10亿个元素)并沿各个轴汇总元素,以生成数据每一侧投影的2D数组。这里的问题是它非常密集,因为程序不断地从ram获取信息,包括读写。
问题是,如果我多线程化程序或者最终会遇到RAM访问瓶颈,我会获得任何性能提升吗?当我说多线程时,我只是指2或4个核心的多线程,不再是。
如果有帮助,我目前的电脑配置为2.4ghz core2 quad,1033 fsb,4gb ram,667mhz。
提前致谢,
-Faken
编辑:
在我看来,这里的人对我最初期待的这个问题更感兴趣。我将扩展问题并为感兴趣的人发布一些代码。
首先,有一点关于我的背景,以便你了解我的来源。我是一名机械工程研究生,有些人设法选择一个与机械工程无关的话题。大约5年前,我在介绍性的java(强制)课程中学习了1门课程,直到大约一个月前,我才认真地开始学习论文。我还采取了(再次强迫,仍然不知道为什么)电子和计算机工程课程,我们处理微控制器(8位),它们的内部工作,以及一些ASM编码。除此之外,我对编程几乎一无所知。
以下是代码:
int dim = 1000;
int steps = 7 //ranges from 1 to 255
for (int stage = 1; stage < steps; stage++)
for (int j = 0; j < dim; j++)
for (int i = 0; i < dim; i++)
{
sum = 0;
for (int k = 0; k < dim; k++)
if (partMap[(((i * dim) + k) * dim) + j] >= stage)
sum++;
projection[(j*dim) + i] = sum;
}
此部分代码仅在z轴上运行。由于构建方式的原因,主要数据有一个奇怪的寻址系统,但您不必担心这一点。还有其他代码用于执行多维数据集的其他方面的投影,但它们做了非常不同的事情。
答案 0 :(得分:31)
跨多个核心进行多线程处理可以减少在轴上求和所需的时间,但需要特别小心。实际上,您可以通过对单线程代码进行的一些更改来获得更大的性能提升:
您只需要尽可能多的线程来匹配可用的核心数。这是一项CPU密集型操作,线程不太可能等待I / O.
如果整个阵列不适合RAM,则上述假设可能不成立。如果数组的某些部分被分页输入和输出,则某些线程将等待分页操作完成。在这种情况下,程序可能会受益于拥有比核心更多的线程。然而,由于上下文切换的成本太多,性能将下降。您可能需要尝试线程计数。一般规则是最小化就绪线程之间的上下文切换次数。
如果整个阵列不适合RAM,您希望最小化分页!每个线程访问内存的顺序很重要,所有正在运行的线程的内存访问模式也是如此。在可能的情况下,您可能希望在移动到下一个数组之前完成数组的一部分,而不是返回到覆盖区域。
每个核心都可以从必须访问完全独立的内存区域中受益。您希望避免因锁和总线争用而导致的内存访问延迟。至少对于立方体的一个维度来说,这应该是直截了当的:将每个线程设置为其自己的立方体部分。
每个核心也可以从其缓存中访问更多数据,而不是从RAM中获取。这意味着对循环进行排序,使内循环访问附近的单词,而不是跳过行。
最后,根据阵列中的数据类型,英特尔/ AMD处理器(SSE,各代)的SIMD指令可以通过一次求和多个单元来帮助加快单核性能。 VC ++有一些built in support。
如果您必须优先处理您的工作,您可能希望首先最小化磁盘分页,然后集中精力优化内存访问以利用CPU缓存,然后才处理多线程。
答案 1 :(得分:11)
只有一种方法可以优化代码:弄清楚你正在做什么这么慢,少做一些。 “少花钱多少”的一个特例就是做一些别的事情而不是更快。
首先,根据您发布的代码,我正在做的事情是这样的:
#include <fstream>
#include <sstream>
using std::ios_base;
template<typename Iterator, typename Value>
void iota(Iterator start, Iterator end, Value val) {
while (start != end) {
*(start++) = val++;
}
}
int main() {
const int dim = 1000;
const int cubesize = dim*dim*dim;
const int squaresize = dim*dim;
const int steps = 7; //ranges from 1 to 255
typedef unsigned char uchar;
uchar *partMap = new uchar[cubesize];
// dummy data. I timed this separately and it takes about
// a second, so I won't worry about its effect on overall timings.
iota(partMap, partMap + cubesize, uchar(7));
uchar *projection = new uchar[squaresize];
for (int stage = 1; stage < steps; stage++) {
for (int j = 0; j < dim; j++) {
for (int i = 0; i < dim; i++)
{
int sum = 0;
for (int k = 0; k < dim; k++)
if (partMap[(((i * dim) + k) * dim) + j] >= stage)
sum++;
projection[(j*dim) + i] = sum;
}
}
std::stringstream filename;
filename << "results" << stage << ".bin";
std::ofstream file(filename.str().c_str(),
ios_base::out | ios_base::binary | ios_base::trunc);
file.write((char *)projection, squaresize);
}
delete[] projection;
delete[] partMap;
}
(编辑:只是注意到“投影”应该是一个int的数组,而不是uchar。我的不好。这会对一些时间产生影响,但希望不会太大。)
然后我将result*.bin
复制到gold*.bin
,因此我可以按照以下方式检查我未来的更改:
$ make big -B CPPFLAGS="-O3 -pedantic -Wall" && time ./big; for n in 1 2 3 4 5
6; do diff -q results$n.bin gold$n.bin; done
g++ -O3 -pedantic -Wall big.cpp -o big
real 1m41.978s
user 1m39.450s
sys 0m0.451s
好的,此刻100秒。
因此,推测它正在跨越数十亿项数据阵列,我们只尝试一次,而不是每个阶段一次:
uchar *projections[steps];
for (int stage = 1; stage < steps; stage++) {
projections[stage] = new uchar[squaresize];
}
for (int j = 0; j < dim; j++) {
for (int i = 0; i < dim; i++)
{
int counts[256] = {0};
for (int k = 0; k < dim; k++)
counts[partMap[(((i * dim) + k) * dim) + j]]++;
int sum = 0;
for (int idx = 255; idx >= steps; --idx) {
sum += counts[idx];
}
for (int stage = steps-1; stage > 0; --stage) {
sum += counts[stage];
projections[stage][(j*dim) + i] = sum;
}
}
}
for (int stage = 1; stage < steps; stage++) {
std::stringstream filename;
filename << "results" << stage << ".bin";
std::ofstream file(filename.str().c_str(),
ios_base::out | ios_base::binary | ios_base::trunc);
file.write((char *)projections[stage], squaresize);
}
for (int stage = 1; stage < steps; stage++) delete[] projections[stage];
delete[] partMap;
它有点快:
$ make big -B CPPFLAGS="-O3 -pedantic -Wall" && time ./big; for n in 1 2 3 4 5
6; do diff -q results$n.bin gold$n.bin; done
g++ -O3 -pedantic -Wall big.cpp -o big
real 1m15.176s
user 1m13.772s
sys 0m0.841s
现在,steps
在这个例子中非常小,所以我们在“计数”数组中做了很多不必要的工作。在没有剖析的情况下,我猜测计数到256两次(一次清除数组和一次加总)与计数到1000(沿着我们的列运行)相比非常重要。所以让我们改变一下:
for (int j = 0; j < dim; j++) {
for (int i = 0; i < dim; i++)
{
// steps+1, not steps. I got this wrong the first time,
// which at least proved that my diffs work as a check
// of the answer...
int counts[steps+1] = {0};
for (int k = 0; k < dim; k++) {
uchar val = partMap[(((i * dim) + k) * dim) + j];
if (val >= steps)
counts[steps]++;
else counts[val]++;
}
int sum = counts[steps];
for (int stage = steps-1; stage > 0; --stage) {
sum += counts[stage];
projections[stage][(j*dim) + i] = sum;
}
}
}
现在我们只使用了实际需要的桶数。
$ make big -B CPPFLAGS="-O3 -pedantic -Wall" && time ./big; for n in 1 2 3 4 5
6; do diff -q results$n.bin gold$n.bin; done
g++ -O3 -pedantic -Wall big.cpp -o big
real 0m27.643s
user 0m26.551s
sys 0m0.483s
乌拉。代码几乎是第一个版本的4倍,并产生相同的结果。我所做的就是改变数学的顺序:我们甚至还没有看过多线程或预取。我没有尝试任何高技术循环优化,只是把它留给了编译器。所以这可以被认为是一个不错的开始。
然而,它仍然比iota运行的1s长一个数量级。所以仍有可能找到大的收益。一个主要区别是iota按顺序在1d阵列上运行,而不是在整个地方跳跃。正如我在第一个答案中所说,你的目标应该始终是在立方体上使用顺序。
所以,让我们进行一行更改,切换i和j循环:
for (int i = 0; i < dim; i++)
for (int j = 0; j < dim; j++) {
这仍然不是连续的顺序,但它确实意味着我们一次只关注我们的立方体的一百万字节切片。一个现代的CPU具有至少4MB的缓存,所以运气好的话,我们只会在整个程序中点击立方体的任何给定部分的主内存。通过更好的局部性,我们也可以减少进出L1缓存的流量,但主内存最慢。
它有多大差异?
$ make big -B CPPFLAGS="-O3 -pedantic -Wall" && time ./big; for n in 1 2 3 4 5
6; do diff -q results$n.bin gold$n.bin; done
g++ -O3 -pedantic -Wall big.cpp -o big
real 0m8.221s
user 0m4.507s
sys 0m0.514s
不错。事实上,仅这一变化就会使原始代码从100秒到20秒。所以这个因素是5,我做的其他事情都是另一个因素5(我认为上面'用户'和'实际'时间之间的差异主要是因为我的病毒扫描程序是运行,它不是更早.'user'是程序占用CPU的时间,'real'包括暂停的时间,等待I / O或给另一个进程时间运行)。
当然,我的桶排序依赖于这样一个事实,即我们对每列中的值所做的事情都是可交换的和关联的。减少桶的数量只能起作用,因为大的值都被视为相同。这可能不适用于您的所有操作,因此您必须依次查看每个操作的内部循环以找出如何处理它。
代码有点复杂。我们不是为每个阶段的数据执行“blah”,而是在数据的单次运行中同时计算所有阶段。如果你在一次通过中开始进行行和列计算,正如我在第一个答案中所建议的那样,这将变得更糟。您可能必须开始将代码分解为函数以使其可读。
最后,我的很多表现都来自优化“步骤”很小的事实。使用steps=100
,我得到:
$ make big -B CPPFLAGS="-O3 -pedantic -Wall" && time ./big; for n in 1 2 3 4 5
6; do diff -q results$n.bin gold$n.bin; done
g++ -O3 -pedantic -Wall big.cpp -o big
real 0m22.262s
user 0m10.108s
sys 0m1.029s
这不是那么糟糕。使用steps = 100,原始代码可能需要大约1400秒,尽管我不会运行它来证明这一点。但值得记住的是,我还没有完全消除对“步骤”的时间依赖性,只是让它变为亚线性。
答案 2 :(得分:5)
您的代码如何运作?它会这样吗?
for each row: add up the values
for each column: add up the values
for each stack: add up the values
如果是这样,您可能想要阅读“参考地点”。根据数据的存储方式,您可能会发现在执行堆栈时,必须为每个值提取整个高速缓存行,因为这些值在内存中彼此无法接近。实际上,有十亿个值,你可能会从磁盘中一路拉动东西。具有较长步幅(值之间的距离)的顺序访问是缓存的最坏可能用途。尝试进行性能分析,如果您发现堆叠的累加时间比累加行要长,这几乎就是原因。
我认为你可能会使内存总线(*)饱和,在这种情况下,多线程只有在core2 quad为不同内核使用不同总线时才有用。但是如果你没有使总线带宽饱和,即使你多线程也不能以这种方式获得最佳性能。你将有4个内核将所有时间都花在缓存未命中而不是一个上。
如果您是内存缓存绑定,那么您的目标应该是尽可能少地访问每个页面/内存行。因此,我会尝试一次运行数据,将每个值添加到三个不同的总计中。如果在单个核心上运行速度更快,那么我们就可以开展业务。下一步是使用1000x1000x1000多维数据集,您可以随时获得300万个总计。这也不适合缓存,因此您必须担心在阅读时写入相同的缓存未命中问题。
您希望确保当您在RAM中运行1000行相邻值时添加到它们共享的行总数时,您还要添加到列和堆栈的相邻总计(它们不会商店)。所以列总数的“平方”应该以适当的方式存储,就像堆栈的“正方形”一样。这样你就可以通过将大约12k的内存提取到缓存中来处理1000个十亿个值(1000个值为4k,1000个列总计加上4k,1000个堆栈总数加上4k)。与此相反,你通过一次集中注意力来做更多的商店(因此可以在注册表中)。
所以我不承诺任何事情,但我认为值得关注内存访问的顺序,无论你是否多线程。如果你只需要访问相对少量的内存就可以完成更多的CPU工作,那么你将加速单线程版本,但也可以让自己更好地适应多线程,因为内核共享一个有限的缓存,内存总线和主RAM。
(*)信封计算的背面:在互联网上的随机随机评论中,到目前为止我发现的Core2处理器的最高估计FSB带宽是12GB / s的Extreme,2个通道,每个4x199MHz)。缓存行大小为64字节,小于您的步幅。因此,总结一个列或堆栈的坏方法,每个值占用64个字节,只有在每秒执行2亿个值时才会使总线饱和。我猜它不像这么快(整件事10-15秒),或者你不会问如何加快速度。
所以我的第一个猜测可能就此消失了。除非您的编译器或CPU插入了一些非常聪明的预取,否则单个内核不能在每个周期使用2个通道和4个同时传输。就此而言,4个核心无法使用2个通道和4个同时传输。一系列请求的有效总线带宽可能远低于物理限制,在这种情况下,您希望看到多线程的良好改进,因为您有4个内核要求4个不同的高速缓存行,所有这些都可以是同时加载而不会麻烦FSB或缓存控制器。但是延迟仍然是杀手,所以如果每个值加总的加载少于一个缓存行,你会做得更好。
答案 3 :(得分:4)
一般来说,这是不可能的,因为您没有指定CPU和RAM的速度。很有可能它会改进一些东西,因为我无法想象即使4个并行求和的线程也会使RAM足够饱和,以至于它会成为瓶颈(而不是CPU)。
答案 4 :(得分:3)
我的直觉说你会看到适度的改进。但是,预测优化结果是一个出了名的错误事件。
尝试并对结果进行基准测试。
答案 5 :(得分:2)
如果,这是一个很大的IF,它被编码得恰当,你肯定会看到加速。现在正如我的一位教授总是指出的那样,人们经常尝试采用算法,对其进行线程化,最终它会变慢。这通常是因为同步效率低下。所以基本上如果你想深入研究线程(如果你是编程新手,我老实说不会建议)。
在您的特定情况下,同步可能非常简单。也就是说,您可以将每个线程分配给大型3维矩阵的象限,其中每个线程都保证可以唯一访问输入和输出矩阵的特定区域,因此没有必要'保护'来自多次访问/写入的数据。
总之,在这个特定的简单情况下,线程可能非常简单,但一般来说,如果执行不当会导致程序花费更长时间。这完全取决于。
答案 6 :(得分:2)
我认为即使多线程可以产生性能提升,但这也是采用优化的错误方法。多核心风靡一时,因为它们是CPU制造商以市场价格提供更快CPU速度的唯一途径 - 不一定是因为它们是一种了不起的编程工具(还有很多成熟需要发生)。 / p>
始终关注您正在使用的算法。你说你的程序非常密集RAM - 你可以做些什么来提高缓存命中率?有没有办法对数组进行排序,以便可以线性应用计算?您使用的是哪种编程语言,是否可以使用较低级别的语言进行优化?有没有办法可以使用动态编程来存储结果?
通常,将所有资源用于更高效的算法,数学和编译器优化,然后担心多核。当然,你可能已经处于那个阶段,在这种情况下,这个评论不是很有用; p
答案 7 :(得分:2)
您需要为特定应用程序回答的问题是众所周知的。
首先,工作是否可并行化? Amdahl's Law将为您提供多线程加速的上限。
第二,多线程解决方案是否会引入大量开销?你说程序是“RAM密集型的,因为程序不断从RAM中读取信息,包括读写。”因此,您需要确定读/写是否会导致重要coordination overhead。这并不容易。虽然每个CPU可以随时访问计算机的整个RAM(读取和写入),但这样做会减慢内存访问速度 - 即使没有锁定 - 因为各种CPU都保留了自己的缓存,需要协调缓存中的内容。彼此相对(CPU 1在缓存中有一个值,CPU 2在RAM中更新该值,CPU 2必须告诉CPU 1使其缓存无效)。如果你确实需要锁(这几乎是一个保证,因为你既“读写”记忆)那么你需要尽可能避免争用。
第三,你有记忆吗? “RAM密集型。”与“记忆束缚”不同。如果您当前受CPU限制,那么多线程将加快速度。如果你当前是内存绑定的,那么多线程甚至可能会减慢速度(如果一个线程对于内存来说太快,那么多个线程会发生什么?)。
第四,你是否因为其他原因而放慢速度?如果您在算法中new
或malloc
大量内存,您可能会看到单独的开销。 And on many platforms both new
and malloc
don't handle multithreading well,所以如果你现在很慢,因为malloc
很糟糕,多线程程序会更慢,因为malloc
会更糟。
总的来说,然而,如果没有看到你的代码,我会期望它受CPU限制,我希望多线程可以加快速度 - 实际上几乎与Amdahl定律所暗示的一样多。您可能希望查看OpenMP或英特尔的线程构建模块库,或某种线程队列来执行此操作。
答案 8 :(得分:2)
在进入多线程之前,您应该针对您的代码运行探查器。关于哪里可以找到一个好的(可能)免费的C ++探查器,这可能是一个不同的问题。
这将帮助您识别占用大量计算时间的代码中的任何位。在进行一些分析之后,这里和那里的调整有时会对性能产生巨大的差异。
答案 9 :(得分:2)
虽然如果你是编程新手,这对你来说可能是非常具有挑战性的,一种非常强大的加速方法就是使用GPU的强大功能。不仅VRAM比通常的RAM快得多,GPU还可以在128个或更多内核上并行运行代码。当然,对于这些数据量,您需要拥有一个非常大的VRAM。
如果你决定检查这种可能性,你应该查找nVidia CUDA。我自己没有检查过,但是这意味着这样的问题。
答案 10 :(得分:2)
Multithreading只会使代码更快。
修改强>
我上面说过(这几乎是一个自动响应),因为我看到许多开发人员花了很多时间在多线程代码上,根本没有性能提升。当然,他们最终会得到相同(甚至更慢的性能)以及管理多线程的额外复杂性。
是的,它会在再次阅读您的问题后出现,并考虑到您将从多线程中受益的特定情况。
RAM非常快,所以我认为除非你有很多很多线程,否则很难使内存带宽饱和。
答案 11 :(得分:1)
如果您正确地对数据进行分区,那么是的,您将获得性能提升。如果你现在检查你的cpu使用情况,一个核心将是100%,另外三个核心应该接近0%
这完全取决于你的线程和内存使用情况的结构。
另外,不要指望x4改进。 x4是可实现的最大值,它总是低于许多因素。
答案 12 :(得分:1)
您的计算机系统通常有一些限制粗略表现的元素。哪个部分是你的限制因素,取决于具体情况。通常,以下因素之一可能是导致性能问题的原因。
磁盘I / O带宽:在大多数企业应用程序中,处理的数据量庞大,需要将其存储在某个数据库中。接收这些数据可能会因以下两种原因而减慢:最大传输速度,但通常最大的影响是由于大量小磁盘访问在这里和那里读取一些块而引起的。您将看到磁盘磁头移动的延迟时间,甚至磁盘完全旋转所需的时间可能会限制您的应用程序。很久以前我有一个真正的问题,使用一些扩展的SUN E430安装,其性能优于我的小型NeXTstation ......这是我的数据库的常量fsync(),它被磁盘放慢而没有缓存写入访问(有充分理由) 。通常,您可以通过添加额外的磁盘来加速系统,以获得更高的每秒I / O.在某些情况下,将驱动器专用于特定任务甚至可能会做得更好。
网络延迟:对磁盘而言,几乎所有影响应用程序速度的内容都与网络I / O相同。
RAM:如果您的RAM不够大,无法存储完整的应用程序映像,则需要将其存储在外部磁盘上。因此,磁盘I / O减速会再次让你感到厌烦。
CPU处理速度(整数或浮点):CPU处理能力是CPU密集型任务限制的下一个因素。 CPU具有无法外展的物理速度限制。加快速度的唯一方法是增加更多CPU。
这些限制可以帮助您找到针对特定问题的答案。
您是否需要更多处理能力,而您的系统有多个CPU或核心?在这种情况下,多线程将提高您的性能。
您是否观察到重要的网络或磁盘延迟?如果你看到这一点,你宝贵的CPU可能会丢弃等待一些慢速I / O的CPU周期。如果多一个线程处于活动状态,则该线程可能会在内存中找到处理所需的所有数据,并且可能会获取这些浪费的CPU周期。
因此,您需要观察现有的应用程序。尽量扩大数据的内存带宽。如果应用程序在低于100%的一个CPU上处于活动状态,则可能已达到内存带宽限制。在这种情况下,额外的线程对你没有好处,因为这不会给你带来内存的带宽。
如果CPU为100%,请尝试一下,但请查看算法。多线程将增加额外的同步开销(以及复杂性,大量复杂性),这可能会略微降低内存带宽。更喜欢可以实现的算法,避免细粒度同步。
如果您看到I / O等待时间,请考虑巧妙的分区或缓存,然后再考虑线程。 GNU-make在90年代支持并行构建是有原因的: - )
你所描述的问题领域让我首先看一下聪明的算法。尝试尽可能在主存储器上使用顺序读/写操作,以尽可能地支持CPU和内存子系统。保持操作“本地”和数据结构尽可能小并且尽可能优化,以减少在切换到第二个核心之前需要改组的内存量。
答案 13 :(得分:1)
这是多个核心相互阻塞的地方,试图读取或更新共享相同块缓存的不同内存地址。处理器高速缓存锁定是每个块,并且只有一个线程可以一次写入该块。
Herb Sutter有一篇关于虚假分享的非常好的文章,如何发现它以及如何在你的并行算法中避免它。
显然,他也有大量关于并发编程的优秀文章,请参阅他的blog。
答案 14 :(得分:1)
这是一个矩阵问题?
英特尔和AMD都有针对各种重大数学问题的超级优化库。这些库使用线程,排列数据以获得最佳缓存使用,缓存预取,SSE向量指令。一切。
我相信你必须支付图书馆的费用,但他们非常物有所值。
答案 15 :(得分:0)
如果你可以以线程不在数组中的相同位置写入/读取的方式划分数组,那么它应该提高你的速度。
答案 16 :(得分:0)
我想如果你只是处理你可能不需要分页或使用交换文件的位,那么YES多线程会有所帮助。
如果您无法一次将所有内容加载到内存中,则需要更具体地了解您的解决方案 - 它需要针对线程进行定制。
例如: 假设您将数组加载到较小的块中(大小可能无关紧要)。如果你要加载一个1000x1000x1000的立方体,你可以总结一下。结果可以临时存储在他们自己的三个平原上,然后加到你的3个“最终结果”平面上,然后1000 ^ 3的区块可以扔掉,永远不会被再次阅读。
如果你做这样的事情,你就不会耗尽内存,你不会强调交换文件,你不必担心任何线程同步,除非在一些非常小的特定区域(如果在全部)。
唯一的问题是确保您的数据格式可以直接访问单个1000 ^ 3多维数据集 - 而无需在整个地方寻找硬盘磁头。
编辑:评论是正确的,我错了 - 他完全有道理。
从昨天开始,我意识到整个问题可以在读入时解决 - 读入的每个数据都可以立即汇总到结果中并丢弃。当我这样思考时,你是对的,除非线程可以同时读取两个流而不会发生冲突,否则不会有太多帮助。
答案 17 :(得分:0)
试试这段代码:
int dim = 1000;
int steps = 7 //ranges from 1 to 255
for (int stage = 1; stage < steps; stage++)
for (int k = 0; k < dim; k++)
for (int i = 0; i < dim; i++)
{
sum = 0;
for (int j = 0; j < dim; j++)
if (partMap[(((i * dim) + k) * dim) + j] >= stage)
projection[i*dim + j] ++ ;
// changed order of i and j
}
transponse(projection)
我改变了循环的顺序,使代码缓存友好... 你可以获得一个巨大的性能提升顺序......真是太棒了。
这是您尝试进入多线程之前应该执行的步骤
答案 18 :(得分:-1)
绝对。至少让一个线程上的每个核心同时处理您的问题将有所帮助。目前尚不清楚是否有更多线程会有所帮助,但这是可能的。